In this chapter, we will learn how to copy objects and Arrays in JavaScript.
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.
Let’s look at several ways of shallowly copying data.
We can spread into object literals and into Array literals to make copies:
Alas, spreading has several issues. Those will be covered in the next subsections. Among those, some are real limitations, others mere pecularities.
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:
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()
.
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.
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');
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):
value
) and therefore correctly copy getters, setters, read-only properties, etc.Object.getOwnPropertyDescriptors()
retrieves both enumerable and non-enumerable properties.For more information on enumerability, see §12 “Enumerability of properties”.
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).
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.
Object.assign()
(optional)Object.assign()
works mostly like spreading into objects. That is, the following two ways of copying are mostly equivalent:
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.
Object.assign()
uses assignment to create the properties of the copy.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”.)
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);
Now it is time to tackle deep copying. First, we will deep-copy manually, then we’ll examine generic approaches.
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);
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$/);
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:
original
is an Array we create a new Array and deep-copy the elements of original
into it.original
is an object, we use a similar approach.original
is a primitive value, we don’t have to do anything.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.
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;
}
}
.clone()
vs. copy constructors” explains class-based patterns for copying.