Shallow/Deep copying Arrays and Objects in JavaScript

Shallow/Deep copying Arrays and Objects in JavaScript

Every beginner JavaScript developer at some point during his language basics studies is met with a task of copying an array or an object into another array or an object. As easy as it may sound, it does not always lead to the expected result, which led me to a point of writing this blog.

Rubber Ducks

What is happening when we type a code like this?

const arrayOne = ['tabby', 'red', 'black']
const arrayTwo = arrayOne

Let's turn to a theory for a bit. There are nine types of data in JavaScript: Number, String, Boolean, BigInt, undefined, Symbol, null, Object and Function. The first 7 of them are called primitives. When we create a variable with a primitive value, normally it goes into a region of your computer's memory called Stack (you may want to look into this concept at later stages of learning). The important thing to know is that the variable holds a value itself, and if we copy a variable, we copy its value, too.

Stack cat

let a = 'cat'
let b = a
a === b // true
console.log(b) // 'cat'

But with Object (such as Array, for example) the story is a little different. Functions are actually a special kind of Objects, too. We call these types of data reference types. When an Object variable is created, its data goes into a Heap region of computer's memory, which is like a storage for variables. At the same time, the variable holds only a pointer (a reference) to that data, not its value. So, when we copy an Object like we did in the first example, we copy only a pointer to it, and the data stays where it was.

Heap cats

const arrayOne = ['tabby', 'red', 'black']
const arrayTwo = arrayOne
console.log(arrayTwo) // ['tabby', 'red', 'black']
arrayOne === arrayTwo // true, same data

The problem is, if we change any value in an object variable, it will also changes for all other variables referencing to the same data.

const arrayOne = ['tabby', 'red', 'black']
const arrayTwo = arrayOne
arrayOne[0] = 'white'
console.log(arrayTwo[0]) // ['white']

What can we do?

When we are working with array, it is pretty easy to make a shallow copy of it by using slice(), spread syntax (...) or Array.from() method.

const arrayOne = ['tabby', 'red', 'black']
const arrayTwo = arrayOne.slice()
const arrayThree = [...arrayOne]
const arrayFour = Array.from(arrayOne)
console.log(arrayTwo) // ['tabby', 'red', 'black']
arrayOne === arrayTwo // false, pointers to different data
arrayOne === arrayThree // false
arrayOne === arrayFour // false

On a side note, we can see the reason of why two arrays or objects with equal values created by let or const are always not equal: even if the values are the same, the pointers are different because they reference different memory regions.

Shallow copy is also achieved for objects by spread syntax or Object.assign() method. Object.assign() can also accept multiple arguments.

const objectOne = {'tabby': 1, 'red': 2, 'black': 3}
const objectTwo = {...objectOne}
const objectThree = Object.assign({}, objectOne)
console.log(objectTwo) // { 'tabby': 1, 'red': 2, 'black': 3 }
objectOne === objectTwo // false, pointers to different data
objectOne === objectThree // false

Cloned cats

But the problem arises when we are trying to clone an array or an object which holds an array or an object as one of its elements - nested arrays/objects. As you can guess, we are cloning only the first layer, and inner arrays and objects would still hold the references to the same data.

const objectOne = {'tabby': 1, 'red': 2, others: {'black': 3}}
const objectTwo = {...objectOne}
objectOne.others.black = 10
console.log(objectTwo.others.black) // 10

What to do next?

To solve the problem, we need a deep copying. One of the solutions would be using a cycle while copying an object. We are checking if the copying value is a primitive, copy it if the condition is true, and if it is false, we are using a cycle, but this time - on the value itself. On a basic level, we can do it manually. On an advanced level, we can use recursion.

const objectOne = {'tabby': 1, 'red': 2, others: {'black': 3}}
const objectTwo = {...objectOne}
objectTwo.others = {...objectTwo.others}
objectOne.others.black = 10
console.log(objectTwo.others.black) // 3

The other simple solution would be using JSON.parse(JSON.stringify(object)). It works great with nested arrays and objects, but you will meet complications with functions, undefined, Infinity and other complex data types inside your object.

const objectOne = {'tabby': 1, 'red': 2, others: {'black': 3}}
const objectTwo = JSON.parse(JSON.stringify(objectOne))
objectOne.others.black = 10
console.log(objectTwo.others.black) // 3

The professional solution would be using a library with cloning functionality, but talking about this is way too far from my article's goals.

Junior Senior Cats

Thank you for reading! Any feedback is appreciated! You can find me mostly on Twitter.



Photo credits: