A subtle error I made with constructors

martinandersongraham

Martin Graham

Posted on December 30, 2021

A subtle error I made with constructors

In which I almost didn't notice a mistake

I was able to track down a subtle but important error I was making when using constructors with sub-classes and manually setting prototypes.

Consider the following code (from an implementation of rock-paper-scissors):

function Player(){
  this.move = null;
}
Player.prototype.setMove = function(mv) {
  this.move = mv;
};

function HumanPlayer() {

}
HumanPlayer.prototype = new Player();

let player1 = new HumanPlayer();
let player2 = new HumanPlayer();

player1.setMove('paper');
console.log(player1.move, player2.move);
//paper null
Enter fullscreen mode Exit fullscreen mode

While the error ended up not being a major issue here, notice where the move property of player1 and player2 is initially stored - the object referenced by HumanPlayer.prototype - and this object is shared by all HumanPlayer objects!

The prototypal relationships

Both players access their move property from Human.prototype - meaning they share this.move! Let's confirm this by checking the value of move using getPrototypeOf()

console.log(player1.hasOwnProperty('move'));
//false
console.log(Object.getPrototypeOf(player1).move);
//null
Object.getPrototypeOf(player2).move = 'paper';
console.log(player1.move);
//paper
Enter fullscreen mode Exit fullscreen mode

Not only does player1 not have an own property move, setting player2.[[Prototype]].move to paper is accessed by player1.move! We don't actually have move defined as individual state for each instance of HumanPlayer

Oddly enough, the program worked fine - consider the setMove() function:

Player.prototype.setMove = function(mv) {
  this.move = mv;
};
Enter fullscreen mode Exit fullscreen mode

When invoking this function using player1.setMove('paper'), this refers to player1. Since player1 doesn't have an own property move, one is created! Each player calls setMove(), each now has their own move property, and the move on HumanPlayer.prototype is never used again.

player1.setMove('rock');
player2.setMove('paper');
console.log(player1.move, player2.move);
//rock paper
console.log(Object.getPrototypeOf(player1).move);
//null
Enter fullscreen mode Exit fullscreen mode

We got lucky - this time. How to properly fix this?

function Player(){
  this.move = null;
}

function HumanPlayer() {
  Player.call(this);
}
HumanPlayer.prototype = new Player();

let player1 = new HumanPlayer();
let player2 = new HumanPlayer();

console.log(player1.hasOwnProperty('move'));
//true
Enter fullscreen mode Exit fullscreen mode

Focus on the HumanPlayer constructor - we've added a call to the Player constructor. Now creating a new HumanPlayer invokes the Player constructor, (using the context of the object first created due to new), and sets the move property on this object. Now each player has their own properties. All is well with the world.

💖 💪 🙅 🚩
martinandersongraham
Martin Graham

Posted on December 30, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

A subtle error I made with constructors
javascript A subtle error I made with constructors

December 30, 2021