JavaScript: Shallow Copy vs Deep Copy

While working through the exercises in Eloquent JavaScript, there was an exercise where we had to implement a prepend function for a list object. It appeared simple but there was a small detail that would completely change the output of my function. Can you spot the difference between the two functions below?

const prependShallow = (element, list) => {
  return { value: element, rest: Object.assign({}, list) };
}

const prependDeep = (element, list) => {
  return { value: element, rest: list };
}

The first function returns a completely new list object with no internal references. The second function also returns a list object but the value in the key rest references the parameter list. Is either answer more correct that the other? I would say it depends on your objective and how you will be using the object. The shallow implementation follows the concept of creating a “pure” function. This has the benefit of not having any side-effects and making testing easier. However, there can be cases where performance is of upmost importance and using a reference instead of creating a copy can save us processing time and memory.

Primitive and Composite/Complex Data Types

Before going into shallow vs deep copying let’s quickly review data types as they provide us with the necessary framework to process the why’s of shallow and deep copying.

A primitive data type is usually built-in to the language and is part of the building blocks of the language. These values are typically stored directly into a computer memory address and are often passed by value. JavaScript has seven primitive types:

  • Boolean
  • null
  • undefined
  • Number
  • BigInt
  • String
  • Symbol

A composite/complex data type consists of a grouping of primitive data types as seen in arrays or objects. These values typically contain a reference (a memory address) to the actual physical location where the grouping begins (i.e. arr[0]). JavaScript has one composite/complex type which is Object. Why do composite data types store a reference instead of a value? Imagine having an array with 10,000 elements, now imagine having to pass that array by value to a function. Passing these values by reference allows us to better use computer space (memory) and time (processor).

It is worth noting that in JavaScript the composite/complex data type Object is the ancestor of most non-primitive entities. The prototype of an array is the Array object whose prototype is Object. The prototype of a function is also Object.

Shallow Copy vs. Deep Copy

The concept of shallow and deep copying only applies to composite/complex data types as these entities are passed by reference.

A deep copy is when two objects, our original object and the copy object, point to the same memory location. This means any change to either object will be reflected in the other. Since they reference the same memory location, they will have the same keys and values.

A shallow copy may contain the same keys and values as the original but it points to its own memory location. It has no tie internally to the object it has copied. Therefore, a change in either object will not be reflected in the other.

Let’s follow through an example. We have a binding dog that will be our original object and two copies a dogDeepCopy and a dogShallowCopy. The image below illustrates how our bindings may look in memory. Note that dog and dogDeepCopy are pointing to the same memory address (rectangle).

let dog = {
  name: "Cookie",
  age: 5
};
let dogDeepCopy = dog;
let dogShallowCopy = Object.assign({}, dog);

JavaScript defines the use of the equality operator, ==, on two objects to test whether two objects are referencing the same memory location. An expression with this operator and two objects will return true if they point to the same memory location and false if they don’t. It does not do a comparison between the keys and values of the objects (more on that later).

console.log("dog == dogDeepCopy -> " + (dog == dogDeepCopy));
console.log("dog == dogShallowCopy -> " + (dog == dogShallowCopy));

/*
'dog == dogDeepCopy -> true'
'dog == dogShallowCopy -> false'
*/

Again since dogDeepCopy points to the same location as dog, any changes to either object will be reflected on the other object. However, since our shallow copy dogShallowCopy is operating on its own own memory block we do not exhibit that behavior. Try working through the statements below before seeing their output for a small exercise!

//Change dogDeepCopy name
dogDeepCopy.name = "Cookie";
console.log("dog.name -> " + dog.name);
console.log("dogDeepCopy.name -> " + dogDeepCopy.name);
console.log("dogShallowCopy.name ->" + dogShallowCopy.name);

//Change dog name
dog.name = "Brownie";
console.log("dog.name -> " + dog.name);
console.log("dogDeepCopy.name -> " + dogDeepCopy.name);
console.log("dogShallowCopy.name ->" + dogShallowCopy.name);

//Change dogShallowCopy name
dogShallowCopy.name = "Ice";
console.log("dog.name -> " + dog.name);
console.log("dogDeepCopy.name -> " + dogDeepCopy.name);
console.log("dogShallowCopy.name ->" + dogShallowCopy.name);

/*
'dog.name -> Cookie'
'dogDeepCopy.name -> Cookie'
'dogShallowCopy.name ->Brownie'
'dog.name -> Brownie'
'dogDeepCopy.name -> Brownie'
'dogShallowCopy.name ->Brownie'
'dog.name -> Brownie'
'dogDeepCopy.name -> Brownie'
'dogShallowCopy.name ->Ice'
*/

Object.create() vs Object.assign()

A newbie mistake I made when first learning this concept with objects was using the Object.create() and Object.assign() functions interchangeably. JavaScript will go look for a property in it’s prototype (and so forth) if it does not directly find it in it’s own direct properties. I initially believed I had created a shallow copy with Object.create() however, a closer inspection showed I had no direct properties and my “copy’s” prototype contained a reference to my original object. This meant changing the original object reflected the change on my “copy’s” prototype which led me to become aware of my mistake. (A true shallow copy would have not exhibited this behavior.)

Object.create() is to be used when you want to create a new object and have it’s prototype be an existing object. Object.assign() is used to copy the properties of a source object into a target object.

The code below is erroneous. It uses Object.create() to try and create a shallow copy but we can see that the original object is copied into the copied object’s prototype. Note that we can still access the property name in dogCopy even though it is part of it’s prototype and not a direct property.

let dog = {
  name: 'Brownie',
  age: 5
};
let dogCopy = Object.create(dog);

console.log("dog.name -> " + dog.name);
console.log("dogCopy.name -> " + dogCopy.name);

// Change dog name, notice the error: we didn't want dogCopy to change name
dog.name = 'Ice';
console.log("dog.name -> " + dog.name);
console.log("dogCopy.name -> " + dogCopy.name);

// See object's direct properties
console.log("dog keys -> " + Object.keys(dog));
console.log("dogCopy keys -> " + Object.keys(dogCopy));
console.log("dogCopy prototype property 'name' -> " + dogCopy.__proto__.name);

/*
'dog.name -> Brownie'
'dogCopy.name -> Brownie'
'dog.name -> Ice'
'dogCopy.name -> Ice'
'dog keys -> name,age'
'dogCopy keys -> '
'dogCopy prototype property 'name' -> Ice'
*/

Arrays

The concept of shallow and deep copying is also relevant to arrays. Recall arrays are also of type Object in JavaScript. You can create a shallow copy of an existing array with the spread operator: ‘…’. The example below shows the same properties described above with arrays.

let arr = [1, 2, 3];
let arrDeepCopy = arr;
let arrShallowCopy = [...arr];

console.log("arr == arrDeepCopy -> " + (arr == arrDeepCopy));
console.log("arr == arrShallowCopy -> " + (arr == arrShallowCopy));

console.log("Push '4' to arr");
arr.push(4);
console.log("arr -> " + arr);
console.log("arrDeepCopy -> " + arrDeepCopy);
console.log("arrShallowCopy -> " + arrShallowCopy);

console.log("Push '5' to arr");
arrShallowCopy.push(5);
console.log("arr -> " + arr);
console.log("arrDeepCopy -> " + arrDeepCopy);
console.log("arrShallowCopy -> " + arrShallowCopy);

/*
'arr == arrDeepCopy -> true'
'arr == arrShallowCopy -> false'
'Push '4' to arr'
'arr -> 1,2,3,4'
'arrDeepCopy -> 1,2,3,4'
'arrShallowCopy -> 1,2,3'
'Push '5' to arr'
'arr -> 1,2,3,4'
'arrDeepCopy -> 1,2,3,4'
'arrShallowCopy -> 1,2,3,5'
*/

Shallow/Deep Copy vs Shallow/Deep Comparison

The concepts of shallow copy and deep copy is to be separated from the concept of shallow and deep comparison. These types of comparison check whether the contents of two objects are the same, that is they contain the same keys and the same values. A deep comparison will delve deeper by following through a reference until it reaches a value. A shallow comparison will not delve deeper if it encounters a reference. JavaScript does not have the built-in functionality to do these types of comparisons but you can write your own function or use an existing library. Remember the built-in functionality of using the equality operator, ==, with two objects is to check if the objects refer to the same memory location.

Last Words

If you are interested in reading through the book, Eloquent JavaScript, yourself it is available for free. Much gratitude to the author Marijn Haverbeke for creating this resource. The book is also available in print for those that prefer to handle a physical book (me).

Leave a Reply

Your email address will not be published.