Diving into Object Cloning: Exploring Alternatives and Limitations
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 toObject.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 Set
and 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!