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:

  1. const copyOfObject = {...originalObject};
  2. 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

For example:

  1. class MyClass {}
  2. const original = new MyClass();
  3. assert.equal(original instanceof MyClass, true);
  4. const copy = {...original};
  5. assert.equal(copy instanceof MyClass, false);

Note that the following two expressions are equivalent:

  1. obj instanceof SomeClass
  2. SomeClass.prototype.isPrototypeOf(obj)

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

  1. class MyClass {}
  2. const original = new MyClass();
  3. const copy = {
  4. __proto__: Object.getPrototypeOf(original),
  5. ...original,
  6. };
  7. 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

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

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.

  1. const proto = { inheritedProp: 'a' };
  2. const original = {__proto__: proto, ownProp: 'b' };
  3. assert.equal(original.inheritedProp, 'a');
  4. assert.equal(original.ownProp, 'b');
  5. const copy = {...original};
  6. assert.equal(copy.inheritedProp, undefined);
  7. assert.equal(copy.ownProp, 'b');
6.2.1.4 Only enumerable properties are copied

For example, the own property .length of Array instances is not enumerable and not copied:

  1. const arr = ['a', 'b'];
  2. assert.equal(arr.length, 2);
  3. assert.equal({}.hasOwnProperty.call(arr, 'length'), true);
  4. const copy = {...arr};
  5. 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):

  • They consider all attributes (not just 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 [content not included].

6.2.1.5 Property attributes aren’t always copied faithfully

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:

  1. const original = Object.defineProperties(
  2. {}, {
  3. prop: {
  4. value: 1,
  5. writable: false,
  6. configurable: false,
  7. enumerable: true,
  8. },
  9. });
  10. assert.deepEqual(original, {prop: 1});

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

  1. const copy = {...original};
  2. // Attributes `writable` and `configurable` of copy are different:
  3. assert.deepEqual(
  4. Object.getOwnPropertyDescriptors(copy),
  5. {
  6. prop: {
  7. value: 1,
  8. writable: true,
  9. configurable: true,
  10. enumerable: true,
  11. },
  12. });

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

  1. const original = {
  2. get myGetter() { return 123 },
  3. set mySetter(x) {},
  4. };
  5. assert.deepEqual({...original}, {
  6. myGetter: 123, // not a getter anymore!
  7. mySetter: undefined,
  8. });

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:

  1. const original = {name: 'Jane', work: {employer: 'Acme'}};
  2. const copy = {...original};
  3. // Property .name is a copy: changing the copy
  4. // does not affect the original
  5. copy.name = 'John';
  6. assert.deepEqual(original,
  7. {name: 'Jane', work: {employer: 'Acme'}});
  8. assert.deepEqual(copy,
  9. {name: 'John', work: {employer: 'Acme'}});
  10. // The value of .work is shared: changing the copy
  11. // affects the original
  12. copy.work.employer = 'Spectre';
  13. assert.deepEqual(
  14. original, {name: 'Jane', work: {employer: 'Spectre'}});
  15. assert.deepEqual(
  16. 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:

  1. const copy1 = {...original};
  2. 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.

  • Object.assign() uses assignment to create the properties of the copy.
  • Spreading defines new properties in 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:

  1. const original = {['__proto__']: null}; // (A)
  2. const copy1 = {...original};
  3. // copy1 has the own property '__proto__'
  4. assert.deepEqual(
  5. Object.keys(copy1), ['__proto__']);
  6. const copy2 = Object.assign({}, original);
  7. // copy2 has the prototype null
  8. 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:

  1. function copyAllOwnProperties(original) {
  2. return Object.defineProperties(
  3. {}, Object.getOwnPropertyDescriptors(original));
  4. }

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:

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

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

  1. const arr = ['a', 'b'];
  2. assert.equal(arr.length, 2);
  3. assert.equal({}.hasOwnProperty.call(arr, 'length'), true);
  4. const copy = copyAllOwnProperties(arr);
  5. assert.equal({}.hasOwnProperty.call(copy, 'length'), true);