Deep JavaScript
Please support this book: buy it or donate
(Ad, please don’t block.)

14 Copying instances of classes: .clone() vs. copy constructors



In this chapter, we look at two techniques for implementing copying for class instances:

14.1 .clone() methods

This technique introduces one method .clone() per class whose instances are to be copied. It returns a deep copy of this. The following example shows three classes that can be cloned.

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  clone() {
    return new Point(this.x, this.y);
  }
}
class Color {
  constructor(name) {
    this.name = name;
  }
  clone() {
    return new Color(this.name);
  }
}
class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y);
    this.color = color;
  }
  clone() {
    return new ColorPoint(
      this.x, this.y, this.color.clone()); // (A)
  }
}

Line A demonstrates an important aspect of this technique: compound instance property values must also be cloned, recursively.

14.2 Static factory methods

A copy constructor is a constructor that uses another instance of the current class to set up the current instance. Copy constructors are popular in static languages such as C++ and Java, where you can provide multiple versions of a constructor via static overloading. Here, static means that the choice which version to use, is made at compile time.

In JavaScript, we must make that decision at runtime and that leads to inelegant code:

class Point {
  constructor(...args) {
    if (args[0] instanceof Point) {
      // Copy constructor
      const [other] = args;
      this.x = other.x;
      this.y = other.y;
    } else {
      const [x, y] = args;
      this.x = x;
      this.y = y;
    }
  }
}

This is how you’d use this class:

const original = new Point(-1, 4);
const copy = new Point(original);
assert.deepEqual(copy, original);

Static factory methods are an alternative to constructors and work better in this case because we can directly invoke the desired functionality. (Here, static means that these factory methods are class methods.)

In the following example, the three classes Point, Color and ColorPoint each have a static factory method .from():

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  static from(other) {
    return new Point(other.x, other.y);
  }
}
class Color {
  constructor(name) {
    this.name = name;
  }
  static from(other) {
    return new Color(other.name);
  }
}
class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y);
    this.color = color;
  }
  static from(other) {
    return new ColorPoint(
      other.x, other.y, Color.from(other.color)); // (A)
  }
}

In line A, we once again copy recursively.

This is how ColorPoint.from() works:

const original = new ColorPoint(-1, 4, new Color('red'));
const copy = ColorPoint.from(original);
assert.deepEqual(copy, original);

14.3 Acknowledgements