The shallow and deep end of copying #JavaScript #arrays

In JavaScript, arrays are passed around by reference. What this means is that if you assign a variable so that it equals another variable which is defined as an array then a pointer to the original array is assigned to the new variable. You will now have two variables pointing to the exact same array object. This is unlike primitive types in JavaScript where when you assign them to a variable the value is copied to the new variable and allocated its own memory.

Below is an example showing the same reference being used in two different variables. It also shows what happens to the array when a new value is added to one of the variables pointing to the array.

<script>
   // Create the default array that will hold our standard names
   var defaultNames = ['Jack', 'Jill', 'James'];
   // Write all of the default names
   console.log('Default Names: ' + defaultNames.join());
   
   // Create another variable that will be initialized with default names 
   // and will hold more names
   var moreNames = defaultNames;

   moreNames.splice(0, 0, 'Jerry');
   // Write all of the more names
   console.log('More Names: ' + moreNames.join());
   // Write all of the default names
   console.log('Default Names: ' + defaultNames.join());
</script>

The result of this script is the following output:

Default Names: Jack,Jill,James
More Names: Jerry,Jack,Jill,James
Default Names: Jerry,Jack,Jill,James

Instead of the variable defaultNames remaining with its original assignment of three names it now includes the name Jerry because the reference to the array was assigned to the moreNames variable. Since both variables are pointing to the same array this is expected behavior. What we would really want is to have the values of defaultNames assigned to the moreNames variable so that we could manipulate one of the arrays without changing the other. In this post I’ll go over a few operations native to JavaScript that we can use. The first method is the slice() operation.

The slice() method goes through the array and creates a shallow copy of each element in the array. This means that if you have objects, not primitive types like numbers, then the reference to the object will be copied. The result is a new array object being created which will allow you to add and remove objects from each array without worry of impacting the other one.

So instead of doing the assignment:

var moreNames = defaultNames;

We instead use the slice() method:

var moreNames = defaultNames.slice();

This will result in the following output:

Default Names: Jack,Jill,James
More Names: Jerry,Jack,Jill,James
Default Names: Jack,Jill,James

Now the defaultNames array is not impacted when we add the name Jerry to the moreNames array.

Something new in ES6 is the spread operator […] which can be used to perform a shallow copy like the slice() method.

var moreNames = [...defaultNames];

The result will be the same as what was output when we used the slice() method. One of the neat features that I like with the spread operator is the ability to easily join of two arrays.

var moreNames = [...defaultNames, ['Gina', 'Greta', 'George'];
// Now moreNames contains ['Jack', 'Jill', 'James', 'Gina', 'Greta', 'George']

This would result in moreNames consisting of all the values from defaultNames plus the values in the second array. Again, all values are shallow copied into a new array object before they are assigned to the variable.

As mentioned earlier both of those methods only perform a shallow copy of the array objects. That means if your array contains references to objects then modifying the object in the one array will also modify it in the other array. In some cases this is desired but for the cases where it isn’t a quick way to perform a deep copy is to utilize the JSON object. We can use the stringify() method to convert the array into a JSON string and then the parse() method to convert it back into a JavaScript object.

<script>
   var defaultNames = [
     {first: 'Amelia', last: 'Smith'}, 
     {first: 'Christopher', last: 'Conner'}
   ];
   var modifiedNames = JSON.parse(JSON.stringify(defaultNames));
   newNames[0].first = 'Jennifer';
   console.log('Default Names: ' + JSON.stringify(defaultNames));
   console.log('Modified Names: ' + JSON.stringify(modifiedNames));
</script>

Thanks to the array object being converted into a string and then back into its native objects the two variables have completely different references for all objects in the array. This results in the following output:

Default Names: [{“first”:”Amelia”,”last”:”Smith”},{“first”:”Christopher”,”last”:”Conner”}]
Modified Names: [{“first”:”Jennifer”,”last”:”Smith”},{“first”:”Christopher”,”last”:”Conner”}]

In this case the first name in the modifiedNames output changed from Amelia to Jennifer. Had we used the slice() or […] operations to make the copy then both arrays would have had their first element change to Jennifer. With one, possibly two if you split out the function calls you are able to create a true deep copy of the array. This simplicity of the JSON object to create a deep copy makes it a great option for most use cases.

There is always the option, and sometimes it is warranted, of rolling your own code to create copies of arrays but is it really needed? If the language is already providing you with the tools to complete the operation then use them. This will keep your code maintainable, the method calls will most likely be better documented, and there will be a much lower risk of bugs in the native operation as compared to your own.