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:

  1. const original = {name: 'Jane', work: {employer: 'Acme'}};
  2. const copy = {name: original.name, work: {...original.work}};
  3. // We copied successfully:
  4. assert.deepEqual(original, copy);
  5. // The copy is deep:
  6. 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:

  1. function jsonDeepCopy(original) {
  2. return JSON.parse(JSON.stringify(original));
  3. }
  4. const original = {name: 'Jane', work: {employer: 'Acme'}};
  5. const copy = jsonDeepCopy(original);
  6. 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:

  1. assert.deepEqual(
  2. jsonDeepCopy({
  3. // Symbols are not supported as keys
  4. [Symbol('a')]: 'abc',
  5. // Unsupported value
  6. b: function () {},
  7. // Unsupported value
  8. c: undefined,
  9. }),
  10. {} // empty object
  11. );

Others cause exceptions:

  1. assert.throws(
  2. () => jsonDeepCopy({a: 123n}),
  3. /^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:

  1. function deepCopy(original) {
  2. if (Array.isArray(original)) {
  3. const copy = [];
  4. for (const [index, value] of original.entries()) {
  5. copy[index] = deepCopy(value);
  6. }
  7. return copy;
  8. } else if (typeof original === 'object' && original !== null) {
  9. const copy = {};
  10. for (const [key, value] of Object.entries(original)) {
  11. copy[key] = deepCopy(value);
  12. }
  13. return copy;
  14. } else {
  15. // Primitive value: atomic, no need to copy
  16. return original;
  17. }
  18. }

The function handles three cases:

  • If original is an Array we create a new Array and deep-copy the elements of original 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():

  1. const original = {a: 1, b: {c: 2, d: {e: 3}}};
  2. const copy = deepCopy(original);
  3. // Are copy and original deeply equal?
  4. assert.deepEqual(copy, original);
  5. // Did we really copy all levels
  6. // (equal content, but different objects)?
  7. assert.ok(copy !== original);
  8. assert.ok(copy.b !== original.b);
  9. 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():

  1. function deepCopy(original) {
  2. if (Array.isArray(original)) {
  3. return original.map(elem => deepCopy(elem));
  4. } else if (typeof original === 'object' && original !== null) {
  5. return Object.fromEntries(
  6. Object.entries(original)
  7. .map(([k, v]) => [k, deepCopy(v)]));
  8. } else {
  9. // Primitive value: atomic, no need to copy
  10. return original;
  11. }
  12. }