Black lives matter
Portrait Dr. Axel Rauschmayer
Dr. Axel Rauschmayer
Homepage | Twitter
Cover of book “JavaScript for impatient programmers”
Book, exercises, quizzes
(free to read online)
Cover of book “Deep JavaScript”
Book (50% free online)
Cover of book “Tackling TypeScript”
Book (first part free online)
Logo of newsletter “ES.next news”
Newsletter (free)

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.


Table of contents:


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