-
Book Overview & Buying
-
Table Of Contents
-
Feedback & Rating
JavaScript Design Patterns
By :
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.
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:
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);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.
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.
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'
); 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.
Change the font size
Change margin width
Change background colour