Deep JavaScript
Please support this book: buy it or donate
(Ad, please don’t block.)

6 Copying objects and Arrays



In this chapter, we will learn how to copy objects and Arrays in JavaScript.

6.1 Shallow copying vs. deep copying

There are two “depths” with which data can be copied:

The next sections cover both kinds of copying. Unfortunately, JavaScript only has built-in support for shallow copying. If we need deep copying, we need to implement it ourselves.

6.2 Shallow copying in JavaScript

Let’s look at several ways of shallowly copying data.

6.2.1 Copying plain objects and Arrays via spreading

We can spread into object literals and into Array literals to make copies:

const copyOfObject = {...originalObject};
const copyOfArray = [...originalArray];

Alas, spreading has several issues. Those will be covered in the next subsections. Among those, some are real limitations, others mere pecularities.

6.2.1.1 The prototype is not copied by object spreading

For example:

class MyClass {}

const original = new MyClass();
assert.equal(original instanceof MyClass, true);

const copy = {...original};
assert.equal(copy instanceof MyClass, false);

Note that the following two expressions are equivalent:

obj instanceof SomeClass
SomeClass.prototype.isPrototypeOf(obj)

Therefore, we can fix this by giving the copy the same prototype as the original:

class MyClass {}

const original = new MyClass();

const copy = {
  __proto__: Object.getPrototypeOf(original),
  ...original,
};
assert.equal(copy instanceof MyClass, true);

Alternatively, we can set the prototype of the copy after its creation, via Object.setPrototypeOf().

6.2.1.2 Many built-in objects have special “internal slots” that aren’t copied by object spreading

Examples of such built-in objects include regular expressions and dates. If we make a copy of them, we lose most of the data stored in them.

6.2.1.3 Only own (non-inherited) properties are copied by object spreading

Given how prototype chains work, this is usually the right approach. But we still need to be aware of it. In the following example, the inherited property .inheritedProp of original is not available in copy because we only copy own properties and don’t keep the prototype.

const proto = { inheritedProp: 'a' };
const original = {__proto__: proto, ownProp: 'b' };
assert.equal(original.inheritedProp, 'a');
assert.equal(original.ownProp, 'b');

const copy = {...original};
assert.equal(copy.inheritedProp, undefined);
assert.equal(copy.ownProp, 'b');
6.2.1.4 Only enumerable properties are copied by object spreading

For example, the own property .length of Array instances is not enumerable and not copied. In the following example, we are copying the Array arr via object spreading (line A):

const arr = ['a', 'b'];
assert.equal(arr.length, 2);
assert.equal({}.hasOwnProperty.call(arr, 'length'), true);

const copy = {...arr}; // (A)
assert.equal({}.hasOwnProperty.call(copy, 'length'), false);

This is also rarely a limitation because most properties are enumerable. If we need to copy non-enumerable properties, we can use Object.getOwnPropertyDescriptors() and Object.defineProperties() to copy objects (how to do that is explained later):

For more information on enumerability, see §12 “Enumerability of properties”.

6.2.1.5 Property attributes aren’t always copied faithfully by object spreading

Independently of the attributes of a property, its copy will always be a data property that is writable and configurable.

For example, here we create the property original.prop whose attributes writable and configurable are false:

const original = Object.defineProperties(
  {}, {
    prop: {
      value: 1,
      writable: false,
      configurable: false,
      enumerable: true,
    },
  });
assert.deepEqual(original, {prop: 1});

If we copy .prop, then writable and configurable are both true:

const copy = {...original};
// Attributes `writable` and `configurable` of copy are different:
assert.deepEqual(
  Object.getOwnPropertyDescriptors(copy),
  {
    prop: {
      value: 1,
      writable: true,
      configurable: true,
      enumerable: true,
    },
  });

As a consequence, getters and setters are not copied faithfully, either:

const original = {
  get myGetter() { return 123 },
  set mySetter(x) {},
};
assert.deepEqual({...original}, {
  myGetter: 123, // not a getter anymore!
  mySetter: undefined,
});

The aforementioned Object.getOwnPropertyDescriptors() and Object.defineProperties() always transfer own properties with all attributes intact (as shown later).

6.2.1.6 Copying is shallow

The copy has fresh versions of each key-value entry in the original, but the values of the original are not copied themselves. For example:

const original = {name: 'Jane', work: {employer: 'Acme'}};
const copy = {...original};

// Property .name is a copy: changing the copy
// does not affect the original
copy.name = 'John';
assert.deepEqual(original,
  {name: 'Jane', work: {employer: 'Acme'}});
assert.deepEqual(copy,
  {name: 'John', work: {employer: 'Acme'}});

// The value of .work is shared: changing the copy
// affects the original
copy.work.employer = 'Spectre';
assert.deepEqual(
  original, {name: 'Jane', work: {employer: 'Spectre'}});
assert.deepEqual(
  copy, {name: 'John', work: {employer: 'Spectre'}});

We’ll look at deep copying later in this chapter.

6.2.2 Shallow copying via Object.assign() (optional)

Object.assign() works mostly like spreading into objects. That is, the following two ways of copying are mostly equivalent:

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

Using a method instead of syntax has the benefit that it can be polyfilled on older JavaScript engines via a library.

Object.assign() is not completely like spreading, though. It differs in one, relatively subtle point: it creates properties differently.

Among other things, assignment invokes own and inherited setters, while definition doesn’t (more information on assignment vs. definition). This difference is rarely noticeable. The following code is an example, but it’s contrived:

const original = {['__proto__']: null}; // (A)
const copy1 = {...original};
// copy1 has the own property '__proto__'
assert.deepEqual(
  Object.keys(copy1), ['__proto__']);

const copy2 = Object.assign({}, original);
// copy2 has the prototype null
assert.equal(Object.getPrototypeOf(copy2), null);

By using a computed property key in line A, we create .__proto__ as an own property and don’t invoke the inherited setter. However, when Object.assign() copies that property, it does invoke the setter. (For more information on .__proto__, see “JavaScript for impatient programmers”.)

6.2.3 Shallow copying via Object.getOwnPropertyDescriptors() and Object.defineProperties() (optional)

JavaScript lets us create properties via property descriptors, objects that specify property attributes. For example, via the Object.defineProperties(), which we have already seen in action. If we combine that method with Object.getOwnPropertyDescriptors(), we can copy more faithfully:

function copyAllOwnProperties(original) {
  return Object.defineProperties(
    {}, Object.getOwnPropertyDescriptors(original));
}

That eliminates two issues of copying objects via spreading.

First, all attributes of own properties are copied correctly. Therefore, we can now copy own getters and own setters:

const original = {
  get myGetter() { return 123 },
  set mySetter(x) {},
};
assert.deepEqual(copyAllOwnProperties(original), original);

Second, thanks to Object.getOwnPropertyDescriptors(), non-enumerable properties are copied, too:

const arr = ['a', 'b'];
assert.equal(arr.length, 2);
assert.equal({}.hasOwnProperty.call(arr, 'length'), true);

const copy = copyAllOwnProperties(arr);
assert.equal({}.hasOwnProperty.call(copy, 'length'), true);

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:

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;
  }
}

6.4 Further reading