Cloning Objects in Javascript

Joel Oliveira
Joel Oliveira
Mar 11 2022
Posted in Engineering & Technology

Evaluating our options in 2022

Cloning Objects in Javascript

For a long time, to clone objects in javascript we have been resorting to hacks or external libraries to get the job done. All because javascript objects are reference values and you can't just copy them using =.

For example, consider the following:

const original = { key1: 1, key2: 2 };

const clone = original;

console.log(clone, original);
// { key1: 1, key2: 2 } { key1: 1, key2: 2 } // 🆗

clone.key3 = 3;

console.log(clone);
// { key1: 1, key2: 2, key3: 3 } // ✅

console.log(original);
// { key1: 1, key2: 2, key3: 3 } // 😕

As you can see, as soon as we attempt to modify the copied object, those changes will affect the original object too. To understand what is happening, you need to know that when you use =, it will copy the pointer to the memory space and these don't hold any values, they just simply point to the value in memory. Any changes in those values will affect all its references.

Now that we've described the problem, let's see what are our options to effectively make a clone of javascript objects.

Shallow Copies

Most of the times, copying a value in JavaScript will use a shallow copy. This means that changes to deeply nested values will be visible in the copy as well as the original. There are 2 options you can use today to create shallow copies. These 2 options will always create shallow copies. They will be just fine for simple objects, but as soon as you change multi-dimensional objects, changes will also affect the original object.

For example, consider the following:

Spread Operator

const original = { key1: 1, key2: 2, keys: { key1: 1, key2: 2 } };

let clone = { ... original };

clone.keys.key1 = 2;

console.log(clone);
// { key1: 1, key2: 2, keys: { key1: 2, key2: 2 } } // ✅

console.log(original);
// { key1: 1, key2: 2, keys: { key1: 2, key2: 2 } } // 😕

Object.assign

const original = { key1: 1, key2: 2, keys: { key1: 1, key2: 2 } };

const clone = Object.assign({}, original);

clone.keys.key1 = 2;

console.log(clone);
// { key1: 1, key2: 2, keys: { key1: 2, key2: 2 } } // ✅

console.log(original);
// { key1: 1, key2: 2, keys: { key1: 2, key2: 2 } } // 😕

The reason behind this, is that shallow copies will iterate over all enumerable properties and assign them one by one to a fresh new object but only primitive data types (string, number, bigint, boolean, undefined, symbol, and null) are handled as expected. Non-primitives are handled as references and only a copy of its reference in memory is assigned to the new object.

Deep Copies

If your use case requires you to create a clone of a multi-dimensional object, then you will need to use deep copies. Many of us will still use, to this day, external libraries to achieve this. More notably the Lodash cloneDeep. This library uses deep cloning algorithms that will also copy objects one by one but it invokes itself when it finds a reference to another object, effectively creating a new object as well. This might actually be the safest way to make sure 2 different pieces of code do not share any objects and unknowingly change their values.

Another well known solution for deep copies is to workaround using a JSON hack:

const original = { key1: 1, key2: 2, keys: { key1: 1, key2: 2 } };

let clone = JSON.parse(JSON.stringify(original));

clone.keys.key1 = 2;

console.log(clone);
// { key1: 1, key2: 2, keys: { key1: 2, key2: 2 } } // ✅

console.log(original);
// { key1: 1, key2: 2, keys: { key1: 1, key2: 2 } } // ✅

This hack became so popular that V8 (Google's open source high-performance JavaScript and WebAssembly engine) was optimized to speed up JSON.parse(). And while it is fast, it does not work for all situations:

Recursive data structures

JSON.stringify() will throw and error for recursive data structures.

Built-in types

JSON.stringify() will throw an error if the value contains a Map, Set, Date, RegExp or ArrayBuffer.

Functions

JSON.stringify() will ignore functions.

Structured cloning

Until recently this was not available for developers, it was used internally to store and retrieve values in IndexedDB or when using postMessage() in a WebWorker. In Chrome 98, Firefox 94, Edge 98 and Safari 15.4, this has changed and the HTML spec was amended to expose a function called structuredClone(). This allows developers to use the same algorithm to create deep copies of objects.

const original = { key1: 1, key2: 2, keys: { key1: 1, key2: 2 } };

let clone = structuredClone(original);

clone.keys.key1 = 2;

console.log(clone);

// { key1: 1, key2: 2, keys: { key1: 2, key2: 2 } } // ✅

console.log(original);

// { key1: 1, key2: 2, keys: { key1: 1, key2: 2 } } // ✅

And although this new method is more robust, faster and addressed some of the shortcomings of JSON.stringify() (supports data structures and built-in data types) it does not solve all the problems you might face when creating deep copies:

Prototypes

A class instance will result in plain object, as structured cloning discards the object’s prototype chain.

Functions

Functions will also be ignored.

Non-cloneables

Non-cloneable objects like Error and DOM nodes will cause structuredClone() to throw an error.

If your requirements include some of these limitations, then you should still resort to Lodash, which provides custom deep-cloning algorithms that might be best suited for your case.

Conclusion

Depending on your use case, using shallow or deep copies with the methods available for you in Javascript might just get the job done. However, there are still valid reasons to reach out to external libraries to solve very specific problems.

As always, we hope you liked this article and if you have anything to add, we are available via our Support Channel.

Keep up-to-date with the latest news