structuredClone(): deeply copying
objects in JavaScript
[2022-01-16] dev, javascript, jslang
(Ad, please don’t block)
Join us for AWS Innovate and learn
to scale your applications and
innovate with AWS Global
Infrastructure ADS VIA CARBON
Spreading is a common technique for copying objects in JavaScript:
Spreading into an Array literal to copy an Array
Spreading into an Object literal to copy a plain object
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.
1 On which JavaScript platforms is structuredClone() available?
2 Copying objects via spreading is shallow
3 Copying objects deeply via structuredClone()
4 Which values can structuredClone() copy?
4.1 Most built-in values can be copied
4.2 Some built-in values can’t be copied
4.3 Instances of user-defined classes become plain objects
4.4 The property attributes of copied objects
5 Sources of this blog post
6 Further reading
1 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):
Chrome 98
Safari 137 (Technology Preview Release)
Firefox 94
Node.js 17.0
Deno 1.14
Tips:
structuredClone() isn’t always available in WebWorkers – check the
MDN browser compatibility table for more information.
On platforms that don’t support structuredClone, we can use a
polyfill.
2 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']}
);
3 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']}
);
4 Which values can structuredClone() copy?
4.1 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.
4.2 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
);
}
4.3 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).
4.4 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.
4.4.1 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,
},
}
);
4.4.2 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,
}
}
);
5 Sources of this blog post
Section “Safe passing of structured data” in the WHATWG HTML
standard
“The structured clone algorithm” on MDN
“structuredClone()” on MDN
6 Further reading
Chapter “Copying objects and Arrays” of “Deep JavaScript”
Chapter “Copying instances of classes: .clone() vs. copy constructors”
of “Deep JavaScript”
Chapter “Property attributes: an introduction” of “Deep JavaScript”
2ality – JavaScript and more Comment Policy
HTML tags work! Use <pre><code> or <pre> for code blocks, <code> for
inline code.
3 Comments 2ality – JavaScript and more 🔒 Disqus' Privacy Policy
Ashok Patil
Favorite 2 t Tweet f Share Sort by Best
Join the discussion…
Samuel Reed −
7 months ago
It's great news that this is finally going to be available to developers. With
proper first-party engine support, should we expect `structuredClone()` to have
any performance wins over `_.deepClone()` and similar workarounds?
2△ ▽ Reply
john frazer −
7 months ago
Hi, thanks for your insights and thorough introduction. I really think much of
h i h ld i h MDN d i i h h f
what you write should go into the MDN documentation in one or the other form.
One nitpick though, you're using `assert.deepEqual()`, presumably as specified
in [CommonJS](http://wiki.commonjs.org/wi... und used in [NodeJS]
(https://nodejs.org/api/asse.... The problem with that standard is, IMHO, (1) the
mismatch between API terms like `equal` and the commonly used sense of
'equal' in mathematics and programming, and (2) the unwarranted split of
'equality testing' into two subfields, 'testing of equality of primitive values' and
'testing of equality of structured (compound) values'. To do away with point (2):
there shouldn't be any kind of 'deep' equality; two values are either equal, or
they're not equal, period (I could elaborate on that but let's not make this one
too long).
As for point (1), *'equal' in `assert` method names does not imply a method will
test for equality unless when preceded by the `strict` qualifier.*; in other
words: to test for equality, always use `assert.deepStrictEqual()`. Observe that
this method does also works for primitive values in a sane way, so *you never
need to use `assert.strictEqual()`. By contrast, `assert.equal()` and
`assert.deepEqual()` perform tests using the `==` operator, something that was
among the very first things Brandon Eich wanted to fix in the first TC39
meetings back in 1996 (but failed to). The result of this is that
`assert.deepEqual()` will pass for `1, '1'` and `[ 1 ], [ '1' ]`, which is most of the
time not what one wants I guess. If anything, `assert.equal()` etc. should have
been called `assert.equivalent()` or something along those lines. Also observe
that while `d = { '1': true }` and `d = { 1: true }` are indistinguishable, `'1' == 1`
is `true`, and `'a' + '1'` has the same result as `'a' + 1`, still, `1 + '1'` and `1 + 1`
are two totally different things (which is why many people shun implicit
coercions—there are just too many surprises).
I'm getting a bit lengthy here, but maybe it suffices to point out a discussion on
that subject: https://github.com/nodejs/n...
Happy to continue this if there's interest.
2△ ▽ Reply
Clem −
a month ago edited
While structuredClone() is not related to any API (like DOM or filesystem or
other) why it is not part of ECMAScript?
Is there anything that prevents such a function from being defined as an
ECMAScript standard?
Are there any plans to add this or a similar function as an ECMAScript standard?
△ ▽ Reply
✉ Subscribe d Add Disqus to your siteAdd DisqusAdd ⚠ Do Not Sell My Data