Reader small image

You're reading from  JavaScript Design Patterns

Product typeBook
Published inMar 2024
Reading LevelIntermediate
PublisherPackt
ISBN-139781804612279
Edition1st Edition
Languages
Right arrow
Author (1)
Hugo Di Francesco
Hugo Di Francesco
author image
Hugo Di Francesco

Hugo Di Francesco is a software engineer who has worked extensively with JavaScript. He holds a MEng degree in mathematical computation from University College London (UCL). He has used JavaScript across the stack to create scalable and performant platforms at companies such as Canon and Elsevier and in industries such as print on demand and mindfulness. He is currently tackling problems in the travel industry at Eurostar with Node.js, TypeScript, React, and Kubernetes while running the eponymous Code with Hugo website. Outside of work, he is an international fencer, in the pursuit of which he trains and competes across the globe.
Read more about Hugo Di Francesco

Right arrow

Implementing the prototype pattern in JavaScript

Let’s start with a definition of the prototype pattern first.

The prototype design pattern allows us to create an instance based on another existing instance (our prototype).

In more formal terms, a prototype class exposes a clone() method. Consuming code, instead of calling new SomeClass, will call new SomeClassPrototype(someClassInstance).clone(). This method call will return a new SomeClass instance with all the values copied from someClassInstance.

Implementation

Let’s imagine a scenario where we’re building a chessboard. There are two key types of squares – white and black. In addition to this information, each square contains information such as its row, file, and which piece sits atop it.

A BoardSquare class constructor might look like the following:

class BoardSquare {
  constructor(color, row, file, startingPiece) {
    this.color = color;
    this.row = row;
    this.file = file;
  }
}

A set of useful methods on BoardSquare might be occupySquare and clearSquare, as follows:

class BoardSquare {
  // no change to the rest of the class
  occupySquare(piece) {
    this.piece = piece;
  }
  clearSquare() {
    this.piece = null;
  }
}

Instantiating BoardSquare is quite cumbersome, due to all its properties:

const whiteSquare = new BoardSquare('white');
const whiteSquareTwo = new BoardSquare('white');
// ...
const whiteSquareLast = new BoardSquare('white');

Note the repetition of arguments being passed to new BoardSquare, which will cause issues if we want to change all board squares to black. We would need to change the parameter passed to each call of BoardSquare is one by one for each new BoardSquare call. This can be quite error-prone; all it takes is one hard-to-find mistake in the color value to cause a bug:

const blackSquare = new BoardSquare('black');
const blackSquareTwo = new BoardSquare('black');
// ...
const blackSquareLast = new BoardSquare('black');

Implementing our instantiation logic using a classical prototype looks as follows. We need a BoardSquarePrototype class; its constructor takes a prototype property, which it stores on the instance. BoardSquarePrototype exposes a clone() method that takes no arguments and returns a BoardSquare instance, with all the properties of prototype copied onto it:

class BoardSquarePrototype {
  constructor(prototype) {
    this.prototype = prototype;
  }
  clone() {
    const boardSquare = new BoardSquare();
    boardSquare.color = this.prototype.color;
    boardSquare.row = this.prototype.row;
    boardSquare.file = this.prototype.file;
    return boardSquare;
  }
}

Using BoardSquarePrototype requires the following steps:

  1. First, we want an instance of BoardSquare to initialize – in this case, with 'white'. It will then be passed as the prototype property during the BoardSquarePrototype constructor call:
    const whiteSquare = new BoardSquare('white');
    const whiteSquarePrototype = new BoardSquarePrototype
      (whiteSquare);
  2. We can then use whiteSquarePrototype with .clone() to create our copies of whiteSquare. Note that color is copied over but each call to clone() returns a new instance.
    const whiteSquareTwo = whiteSquarePrototype.clone();
    // ...
    const whiteSquareLast = whiteSquarePrototype.clone();
    console.assert(
      whiteSquare.color === whiteSquareTwo.color &&
        whiteSquareTwo.color === whiteSquareLast.color,
      'Prototype.clone()-ed instances have the same color
         as the prototype'
    );
    console.assert(
      whiteSquare !== whiteSquareTwo &&
        whiteSquare !== whiteSquareLast &&
        whiteSquareTwo !== whiteSquareLast,
      'each Prototype.clone() call outputs a different
         instances'
    );

Per the assertions in the code, the cloned instances contain the same value for color but are different instances of the Square object.

A use case

To illustrate what it would take to change from a white square to a black square, let’s look at some sample code where 'white' is not referenced in the variable names:

const boardSquare = new BoardSquare('white');
const boardSquarePrototype = new BoardSquarePrototype(boardSquare);
const boardSquareTwo = boardSquarePrototype.clone();
// ...
const boardSquareLast = boardSquarePrototype.clone();
console.assert(
  boardSquareTwo.color === 'white' &&
    boardSquare.color === boardSquareTwo.color &&
    boardSquareTwo.color === boardSquareLast.color,
  'Prototype.clone()-ed instances have the same color as
     the prototype'
);
console.assert(
  boardSquare !== boardSquareTwo &&
    boardSquare !== boardSquareLast &&
    boardSquareTwo !== boardSquareLast,
  'each Prototype.clone() call outputs a different
    instances'
);

In this scenario, we would only have to change the color value passed to BoardSquare to change the color of all the instances cloned from the prototype:

const boardSquare = new BoardSquare('black');
// rest of the code stays the same
console.assert(
  boardSquareTwo.color === 'black' &&
    boardSquare.color === boardSquareTwo.color &&
    boardSquareTwo.color === boardSquareLast.color,
  'Prototype.clone()-ed instances have the same color as
     the prototype'
);
console.assert(
  boardSquare !== boardSquareTwo &&
    boardSquare !== boardSquareLast &&
    boardSquareTwo !== boardSquareLast,
  'each Prototype.clone() call outputs a different
     instances'
);

The prototype pattern is useful in situations where a “template” for the object instances is useful. It’s a good pattern to create a “default object” but with custom values. It allows faster and easier changes, since they are implemented once on the template object but are applied to all clone()-ed instances.

Increasing robustness to change in the prototype’s instance variables with modern JavaScript

There are improvements we can make to our prototype implementation in JavaScript.

The first is in the clone() method. To make our prototype class robust to changes in the prototype’s constructor/instance variables, we should avoid copying the properties one by one.

For example, if we add a new startingPiece parameter that the BoardSquare constructor takes and sets the piece instance variable to, our current implementation of BoardSquarePrototype will fail to copy it, since it only copies color, row, and file:

class BoardSquare {
  constructor(color, row, file, startingPiece) {
    this.color = color;
    this.row = row;
    this.file = file;
    this.piece = startingPiece;
  }
  // same rest of the class
}
const boardSquare = new BoardSquare('white', 1, 'A',
  'king');
const boardSquarePrototype = new BoardSquarePrototype
  (boardSquare);
const otherBoardSquare = boardSquarePrototype.clone();
console.assert(
  otherBoardSquare.piece === undefined,
  'prototype.piece was not copied over'
);

Note

Reference for Object.assign: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign.

If we amend our BoardSquarePrototype class to use Object.assign(new BoardSquare(), this.prototype), it will copy all the enumerable properties of prototype:

class BoardSquarePrototype {
  constructor(prototype) {
    this.prototype = prototype;
  }
  clone() {
    return Object.assign(new BoardSquare(), this.prototype);
  }
}
const boardSquare = new BoardSquare('white', 1, 'A',
  'king');
const boardSquarePrototype = new BoardSquarePrototype
  (boardSquare);
const otherBoardSquare = boardSquarePrototype.clone();
console.assert(
  otherBoardSquare.piece === 'king' &&
    otherBoardSquare.piece === boardSquare.piece,
  'prototype.piece was copied over'
);

The prototype pattern without classes in JavaScript

For historical reasons, JavaScript has a prototype concept deeply embedded into the language. In fact, classes were introduced much later into the ECMAScript standard, with ECMAScript 6, which was released in 2015 (for reference, ECMAScript 1 was published in 1997).

This is why a lot of JavaScript completely forgoes the use of classes. The JavaScript “object prototype” can be used to make objects inherit methods and variables from each other.

One way to clone objects is by using the Object.create to clone objects with their methods. This relies on the JavaScript prototype system:

const square = {
  color: 'white',
  occupySquare(piece) {
    this.piece = piece;
  },
  clearSquare() {
    this.piece = null;
  },
};
const otherSquare = Object.create(square);

One subtlety here is that Object.create does not actually copy anything; it simply creates a new object and sets its prototype to square. This means that if properties are not found on otherSquare, they’re accessed on square:

console.assert(otherSquare.__proto__ === square, 'uses JS
  prototype');
console.assert(
  otherSquare.occupySquare === square.occupySquare &&
    otherSquare.clearSquare === square.clearSquare,
  "methods are not copied, they're 'inherited' using the
     prototype"
);
delete otherSquare.color;
console.assert(
  otherSquare.color === 'white' && otherSquare.color ===
    square.color,
  'data fields are also inherited'
);

A further note on the JavaScript prototype, and its existence before classes were part of JavaScript, is that subclassing in JavaScript is another syntax for setting an object’s prototype. Have a look at the following extends example. BlackSquare extends Square sets the prototype.__proto__ property of BlackSquare to Square.prototype:

class Square {
  constructor() {}
  occupySquare(piece) {
    this.piece = piece;
  }
  clearSquare() {
    this.piece = null;
  }
}
class BlackSquare extends Square {
  constructor() {
    super();
    this.color = 'black';
  }
}
console.assert(
  BlackSquare.prototype.__proto__ === Square.prototype,
  'subclass prototype has prototype of superclass'
);

In this section, we learned how to implement the prototype pattern with a prototype class that exposes a clone() method, which code situations the prototype patterns can help with, and how to further improve our prototype implementation with modern JavaScript features. We also covered the JavaScript “prototype,” why it exists, and its relationship with the prototype design pattern.

In the next part of the chapter, we’ll look at another creational design pattern, the singleton design pattern, with some implementation approaches in JavaScript and its use cases.

Previous PageNext Page
You have been reading a chapter from
JavaScript Design Patterns
Published in: Mar 2024Publisher: PacktISBN-13: 9781804612279
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $15.99/month. Cancel anytime

Author (1)

author image
Hugo Di Francesco

Hugo Di Francesco is a software engineer who has worked extensively with JavaScript. He holds a MEng degree in mathematical computation from University College London (UCL). He has used JavaScript across the stack to create scalable and performant platforms at companies such as Canon and Elsevier and in industries such as print on demand and mindfulness. He is currently tackling problems in the travel industry at Eurostar with Node.js, TypeScript, React, and Kubernetes while running the eponymous Code with Hugo website. Outside of work, he is an international fencer, in the pursuit of which he trains and competes across the globe.
Read more about Hugo Di Francesco