Diving into Object Cloning: Exploring Alternatives and Limitations

Ángel Cereijo
4 min readJan 2, 2024

--

Objects are essential building blocks for data structures and complex calculations in JavaScript. When dealing with intricate data, we often need to create a copy of an existing object. This process, called object cloning, helps preserve the structure, properties, and values of the original object.

Why clone objects?

There are several reasons to perform object cloning, we could expose them here but this is not the intention of this post. For today, we are just thinking of two things:

  • Immutability is a key concept in modern web development, particularly in the context of composable applications like Polymer, React, and other component-based architectures. It refers to the property of an object that remains unchanged over time.
  • data safety: we want to avoid other components changing our data.

A quick recap of how memory works for variables in JavaScript: value vs reference

Javascript has 8 types, the primitive types: String, Number, Bigint, Boolean, undefined, null, Symbol and Object (this includes all of the other types: Date, Arrays, Map, Set, WeakMaps, WeakSets).

When we send one of these types to a function (pass a parameter):

  • primitive types are passed by value. The value is copied and the value is assigned to a new memory space pointed by the new variable.
  • objects are always passed by reference. The new variable is pointed to the original memory space. Now, both variables are referencing the same memory space.

You can check this article for reference.

Alternatives

The most common alternatives are:

  • JSON.parse(JSON.stringify(...)) — This is a “trick”. It has poor performance and It has problems with some types like Date (it becomes a string), Map, Set (they end as {} )
  • Object.assign(...) (see doc)— It only clones the primitive types. Objects are still pointing at the original value.
  • Object destructuring {...object} — It works similarly to Object.assign and it has the same problems.
  • structuredClone — A new native method (see compatibility). It throws an error if the object has a method.
  • cloneDeep from lodash — Well known and the most complete. The drawback is that we need to add this dependency.

Performance

There is no doubt that most performance methods are destructuring and Object.assign , but they don’t do deep cloning.

Also, depending on if the object to clone has Map and Set properties, the performance will change. Let’s take a look at some performance tests.

Running in node.js

Using this simple code. A loop of 100000 iterations to reasign the same object each time.

const _ = require('lodash');

const person = {
name: 'John Doe',
age: 34,
address: {
country: 'USA',
city: 'Boston',
},
hobbies: ['Reading', 'Cooking'],
active: true,
signDate: new Date(),
superPowers: new Set(['strength', 'fly']),
knwoledge: new Map([['history', 'good'], ['math', 'bad']]),
};

console.time('destructuring');
let shallowCopy;
for (let i = 0; i < 100000; i++) {
shallowCopy = { ...person };
}
console.timeEnd('destructuring');

console.time('assign');
let assignCopy;
for (let i = 0; i < 100000; i++) {
assignCopy = Object.assign({}, { ...person });
}
console.timeEnd('assign');

console.time('structuredClone');
let structure;
for (let i = 0; i < 100000; i++) {
structure = structuredClone(person);
}
console.timeEnd('structuredClone');

console.time('stringify');
let deepCopy;
for (let i = 0; i < 100000; i++) {
deepCopy = JSON.parse(JSON.stringify(person));
}
console.timeEnd('stringify');

console.time('lodash');
let lodashCopy;
for (let i = 0; i < 100000; i++) {
lodashCopy = _.cloneDeep(person);
}
console.timeEnd('lodash');

Results (sorted from faster to slower) or running in an old Macbook i7 with node 18.7.0

assign: 29.384ms

destructuring: 53.269ms

// This is especial as it converts Date to String and Map and Set to {}
stringify: 519.03ms

structuredClone: 707.867ms

lodash: 770.692ms

If we remove the Map and Set properties, the results are different.

assign: 23.853ms

destructuring: 56.728ms

lodash: 285.551ms

// This is especial as it converts Date to String and Map and Set to {}
stringify: 428.387ms

structuredClone: 482.132ms

Curiously the new native method is always at the bottom 🤔 (sometimes it’s better than parse + stringify.

Also, I created this comparison (without Setand Map) in jsperf and the results are very similar. Test it on your own.

My humble opinion

I would utilize destructuring/Object.assign if I don’t necessitate deep cloning and solely intend to signal that I’ve altered the object’s value, as we typically do in React.

If deep cloning is required and I’m operating within node.js without concerns regarding external module loading, I’d employ lodash(cloneDeep) as it’s well-tested and will clone all data types.

If a deep clone is necessary for the frontend, I’d probably select structuredClone as it supports all data types; however, we must exercise caution to prevent functions from being included in the object, as doing so will result in an error.

I’d advise against utilizing parse + stringify, as I’m losing data types in the process.

Would you like to introduce additional options? 🤔 Perhaps you have a different viewpoint on which approach to employ? Please share your thoughts in the comments!

Thanks for reading!

--

--

Ángel Cereijo

Software engineer with 15 year of experience. Lately focused in Node.js ecosystem and good practices.