JavaScript: Shallow vs Deep Copy

João Manuel Gomes
Webtips
Published in
4 min readSep 4, 2020

Copying values seems somewhat trivial. Nevertheless, it’s almost impossible to find a developer that has never had an issue with some wrong reference, or pointer in the case of C-like languages. In this article, I’m going to focus on how to copy variables/values in JavaScript.

JavaScript: Shallow vs Deep Copy

Primitive vs Reference Values

Primitive values

In JavaScript, a primitive (primitive value, primitive data type) is data that is not an object and has no methods. They are the following:

  • String
  • Number
  • BigInt
  • Boolean
  • undefined
  • Symbol
  • null (which indeed is a special case for every Object)

Primitive values are immutable — instead of changing them directly, we’re modifying a copy, without affecting the original. They are passed by value. They’re assigned to variables and passed into functions by copying their value.

let example = '0';
let example_copy = example;
example = '1';console.log('example'); // '1'
console.log('example_copy'); // '0'

Reference Values

Reference values, objects, and arrays, which are a special type of an object, are a bit different. Contrary to primitives these are mutable. This means they have properties that we can access and change

They are passed by reference and no copies are made before passing them. This article is focused on these and different ways of copying them.

Shallow Copy

Shallow copy means we just pass just the reference meaning only the memory addresses are copied.

Let’s create a basket object indicating we have 1 keyboard and 2 monitors as default values and trying to copy it.

let basket = { keyboard: 1, monitor: 2 };
let basketShallow = basket;
basket.keyboard = 3;console.log(basket); // { keyboard: 3, monitor: 2 }
console.log(basketShallow); // { keyboard: 3, monitor: 2 }

Notice that the basket_shallow.keyboard value has changed. This happened because the value was copied by the reference and changing in one place will change for all variables sharing that object reference.

Deep Copy

Deep copy means not passing the element by reference but passing the actual values.

A deep copy will duplicate every object it encounters. The copy and the original object will not share anything, so it will be a clone of the original.

Option 1: Object.assign()

Mostly used before the spread operator was around. The first argument will be modified and returned so, in most cases, we want to pass an empty object, the second argument should be the object we want to copy.

let basket = { keyboard: 1, monitor: 2 };
let basketDeep = Object.assign({}, basket)
basket.keyboard = 3;console.log(basket); // { keyboard: 3, monitor: 2 }
console.log(basketDeep); // { keyboard: 1, monitor: 2 }

Option 2: Spread Operator

Introduced in ES6, It copies its own enumerable properties from a provided object onto a new object, and it’s short and simple. ‘Spreads’ out all of the values into a new object. You can use it as follows

let basket = { keyboard: 1, monitor: 2 };
let basketDeep = { ...basket };
basket.keyboard = 3;console.log(basket); // { keyboard: 3, monitor: 2 }
console.log(basketDeep); // { keyboard: 1, monitor: 2 }

Notice: if you’re using an array, spread operator should be used with a square bracket, e.g. […array].

Nested Deep Copy

Both Object.assign() and the spread operator make deep copies of data if the data is not nested.

When you use them to copy a nested object, they will create a deep copy of the topmost data and a shallow copy of the nested data.

let basket = { keyboard: { quantity: 1 } , monitor: { quantity: 2 } };
let basketDeep = { ...basket };
basket.keyboard.quantity = 3;console.log(basket);
// { keyboard: { quantity: 3}, monitor: { quantity: 2 } }
console.log(basketDeep);
// { keyboard: { quantity: 3}, monitor: { quantity: 2 } }

Notice the previous example. keyboard.quantity was affected in both variables because the spread operator will copy values by reference when the variables are more than 1 dimension deep.

JSON.parse( JSON.stringify() )

The easiest solution for this issue is to encode the object into a JSON string and then build it again into an object. See the example below:

let basket = {keyboard: { quantity: 1 } , monitor: { quantity: 2 }};
let basketDeep = JSON.parse( JSON.stringify(basket) );
basket.keyboard.quantity = 3;console.log(basket);
// { keyboard: { quantity: 3}, monitor: { quantity: 2 } }
console.log(basketDeep);
// { keyboard: { quantity: 1}, monitor: { quantity: 2 } }

Dynamic Nested Deep Copy Function

Alternatively, you can build a custom copy of a specific object structure and adapting the function to your object structure. This might be a tightly coupled approach to a single object and if you change anything for that object, you will, most probably, need to change that function as well. I don’t recommend it.

With that in mind, a dynamic implementation for deep copy of an object is presented below.

function deepCopy(objToCopy) {
let res = {} ;
for (const property in objToCopy) {
if (isPrimitiveValue(objToCopy[property])) {
res[property] = objToCopy[property];
} else {
res[property] = deepCopy(objToCopy[property]);
}
}
return res;
}
function isPrimitiveValue(value) {
return !(typeof value === 'object' && value !== null);
}

Conclusion

Every developer should be aware of shallow and deep copy concepts and know exactly when each implementation is more appropriated.

The spread operator is quite simple and powerful but for nested objects, as we have seen, is not a suitable solution. Consider each scenario and choose the best approach to your case.

Hope this article helps to understand these concepts.

--

--