structuredClone(): deeply copying objects in JavaScript

[2022-01-16] dev, javascript, jslang
(Ad, please don’t block)

Spreading is a common technique for copying objects in JavaScript:

Spreading has one significant downside – it creates shallow copies: The top levels are copied, but property values are shared.

structuredClone() is a new function that will soon be supported by most browsers, Node.js and Deno. It creates deep copies of objects. This blog post explains how it works.

On which JavaScript platforms is structuredClone() available?  

Even though structuredClone() is not part of ECMAScript, it was added to the platform-specific parts of many platforms and is still widely available (either now or soon):

Tips:

Copying objects via spreading is shallow  

One common way of copying Arrays and plain objects in JavaScript is via spreading. This code demonstrates the latter:

const obj = {id: 'e1fd960b', values: ['a', 'b']};
const clone1 = {...obj};

Alas, this way of copying is shallow. On one hand, the key-value entry clone1.id is a copy, so changing it does not change obj:

clone1.id = 'yes';
assert.equal(obj.id, 'e1fd960b');

On the other hand, the Array in clone1.values is shared with obj. If we change it, we also change obj:

clone1.values.push('x');
assert.deepEqual(
  clone1, {id: 'yes', values: ['a', 'b', 'x']}
);
assert.deepEqual(
  obj, {id: 'e1fd960b', values: ['a', 'b', 'x']}
);

Copying objects deeply via structuredClone()  

Structured clone has the following typeSignature:

structuredClone(value: any): any

(This function has a second parameter which is rarely useful and beyond the scope of this blog post. I couldn’t even replicate the use case that MDN showed for it. For more information, see the MDN page for structuredClone().)

structuredClone() copies objects deeply:

const obj = {id: 'e1fd960b', values: ['a', 'b']};
const clone2 = structuredClone(obj);

clone2.values.push('x');
assert.deepEqual(
  clone2, {id: 'e1fd960b', values: ['a', 'b', 'x']}
);
assert.deepEqual(
  obj, {id: 'e1fd960b', values: ['a', 'b']}
);

Which values can structuredClone() copy?  

Most built-in values can be copied  

Primitive values can be copied:

> typeof structuredClone(true)
'boolean'
> typeof structuredClone(123)
'number'
> typeof structuredClone('abc')
'string'

Most built-in objects can be copied – even though they have internal slots:

> Array.isArray(structuredClone([]))
true
> structuredClone(/^a+$/) instanceof RegExp
true

However, when copying a regular expression, property .lastIndex is always reset to zero.

Some built-in values can’t be copied  

Some built-in objects cannot be copied – structuredClone() throws a DOMException if we try to do so:

  • Functions (ordinary functions, arrow functions, classes, methods)
  • DOM nodes

Demonstration of the former:

assert.throws(
  () => structuredClone(function () {}), // ordinary function
  DOMException
);
assert.throws(
  () => structuredClone(() => {}), // arrow function
  DOMException
);
assert.throws(
  () => structuredClone(class {}),
  DOMException
);

const objWithMethod = {
  myMethod() {},
};
assert.throws(
  () => structuredClone(objWithMethod.myMethod), // method
  DOMException
);
assert.throws(
  () => structuredClone(objWithMethod), // object with method
  DOMException
);

What does the exception look like that is thrown by structuredClone()?

try {
  structuredClone(() => {});
} catch (err) {
  assert.equal(
    err instanceof DOMException, true
  );
  assert.equal(
    err.name, 'DataCloneError'
  );
  assert.equal(
    err.code, DOMException.DATA_CLONE_ERR
  );
}

Instances of user-defined classes become plain objects  

In the following example, we copy an instance of the class C. The result, clone, is not an instance of C.

class C {}
const clone = structuredClone(new C());

assert.equal(clone instanceof C, false);
assert.equal(
  Object.getPrototypeOf(clone),
  Object.prototype
);

To summarize – structuredClone() never copies the prototype chain of an object:

  • Copies of built-in objects have the same prototypes as the originals.
  • Copies of instances of user-defined classes always have the prototype Object.prototype (like plain objects).

The property attributes of copied objects  

structuredClone() doesn’t always faithfully copy the property attributes of objects:

  • Accessors are turned into data properties.
  • In copies, the property attributes always have default values.

Read on for more information.

Accessors become data properties  

Accessors become data properties:

const obj = Object.defineProperties(
  {},
  {
    accessor: {
      get: function () {
        return 123;
      },
      set: undefined,
      enumerable: true,
      configurable: true,
    },
  }
);
const copy = structuredClone(obj);
assert.deepEqual(
  Object.getOwnPropertyDescriptors(copy),
  {
    accessor: {
      value: 123,
      writable: true,
      enumerable: true,
      configurable: true,
    },
  }
);

Copies of properties have default attribute values  

Data properties of copies always have the following attributes:

writable: true,
enumerable: true,
configurable: true,
const obj = Object.defineProperties(
  {},
  {
    accessor: {
      get: function () {
        return 123;
      },
      set: undefined,
      enumerable: true,
      configurable: true,
    },
    readOnlyProp: {
      value: 'abc',
      writable: false,
      enumerable: true,
      configurable: true,
    },
  }
);
const copy = structuredClone(obj);
assert.deepEqual(
  Object.getOwnPropertyDescriptors(copy),
  {
    accessor: {
      value: 123,
      writable: true,
      enumerable: true,
      configurable: true,
    },
    readOnlyProp: {
      value: 'abc',
      writable: true,
      enumerable: true,
      configurable: true,
    }
  }
);

Sources of this blog post  

Further reading