6.3 Deep copying in JavaScript
Now it is time to tackle deep copying. First, we will deep-copy manually, then we’ll examine generic approaches.
6.3.1 Manual deep copying via nested spreading
If we nest spreading, we get deep copies:
const original = {name: 'Jane', work: {employer: 'Acme'}};
const copy = {name: original.name, work: {...original.work}};
// We copied successfully:
assert.deepEqual(original, copy);
// The copy is deep:
assert.ok(original.work !== copy.work);
6.3.2 Hack: generic deep copying via JSON
This is a hack, but, in a pinch, it provides a quick solution: In order to deep-copy an object original
, we first convert it to a JSON string and parse that JSON string:
function jsonDeepCopy(original) {
return JSON.parse(JSON.stringify(original));
}
const original = {name: 'Jane', work: {employer: 'Acme'}};
const copy = jsonDeepCopy(original);
assert.deepEqual(original, copy);
The significant downside of this approach is that we can only copy properties with keys and values that are supported by JSON.
Some unsupported keys and values are simply ignored:
assert.deepEqual(
jsonDeepCopy({
// Symbols are not supported as keys
[Symbol('a')]: 'abc',
// Unsupported value
b: function () {},
// Unsupported value
c: undefined,
}),
{} // empty object
);
Others cause exceptions:
assert.throws(
() => jsonDeepCopy({a: 123n}),
/^TypeError: Do not know how to serialize a BigInt$/);
6.3.3 Implementing generic deep copying
The following function generically deep-copies a value original
:
function deepCopy(original) {
if (Array.isArray(original)) {
const copy = [];
for (const [index, value] of original.entries()) {
copy[index] = deepCopy(value);
}
return copy;
} else if (typeof original === 'object' && original !== null) {
const copy = {};
for (const [key, value] of Object.entries(original)) {
copy[key] = deepCopy(value);
}
return copy;
} else {
// Primitive value: atomic, no need to copy
return original;
}
}
The function handles three cases:
- If
original
is an Array we create a new Array and deep-copy the elements oforiginal
into it. - If
original
is an object, we use a similar approach. - If
original
is a primitive value, we don’t have to do anything.
Let’s try out deepCopy()
:
const original = {a: 1, b: {c: 2, d: {e: 3}}};
const copy = deepCopy(original);
// Are copy and original deeply equal?
assert.deepEqual(copy, original);
// Did we really copy all levels
// (equal content, but different objects)?
assert.ok(copy !== original);
assert.ok(copy.b !== original.b);
assert.ok(copy.b.d !== original.b.d);
Note that deepCopy()
only fixes one issue of spreading: shallow copying. All others remain: prototypes are not copied, special objects are only partially copied, non-enumerable properties are ignored, most property attributes are ignored.
Implementing copying completely generically is generally impossible: Not all data is a tree, sometimes we don’t want to copy all properties, etc.
6.3.3.1 A more concise version of deepCopy()
We can make our previous implementation of deepCopy()
more concise if we use .map()
and Object.fromEntries()
:
function deepCopy(original) {
if (Array.isArray(original)) {
return original.map(elem => deepCopy(elem));
} else if (typeof original === 'object' && original !== null) {
return Object.fromEntries(
Object.entries(original)
.map(([k, v]) => [k, deepCopy(v)]));
} else {
// Primitive value: atomic, no need to copy
return original;
}
}