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:
- First, we want an instance of
BoardSquare
to initialize – in this case, with'white'
. It will then be passed as theprototype
property during theBoardSquarePrototype
constructor call:const whiteSquare = new BoardSquare('white'); const whiteSquarePrototype = new BoardSquarePrototype (whiteSquare);
- We can then use
whiteSquarePrototype
with.clone()
to create our copies ofwhiteSquare
. Note thatcolor
is copied over but each call toclone()
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.