Prototypes

To understand what it means to be a prototype-based language and how JavaScript actually works, we will go back to the time when there were no classes.

Prototype Syntax

Constructor Function

In JavaScript, the template (class) is facilitated by a regular function. When a function is supposed to be used as such a template, it is called a constructor function and the convention is that the function name should start with a capital letter. Instances (objects) are derived from the template using the new keyword when invoking the constructor function.

function Car() {
  // ...
}

const myCar = new Car();
const yourCar = new Car();

It is important to note that in JavaScript, the instances and the constructor function keep a relationship to each other even after the instances were created. Every instance object includes a hidden, internal property referred to as [[prototype]] in the language specification. It holds a reference to the value of the prototype key of the constructor function. Yes, you read that correctly, a JavaScript function can have key/value pairs because it is also an object behind the scenes.

To summarize:

  • Constructors in JavaScript are regular functions.
  • Constructing a new instance creates an object with a relation to its constructor called its prototype.
  • Functions are objects (callable objects) and therefore they can have properties.
  • The constructor's (function) prototype property will become the instance's prototype.

Instance Fields

Often, you want all the derived objects (instances) to include some fields and pass some initial values for those when the object is constructed. This can be facilitated via the this keyword. Inside the constructor function, this represents the new object that will be created via new. this is automatically returned from the constructor function when it is called with new.

That means we can add fields to the new instance by adding them to this in the constructor function.

function Car(color, weight) {
  this.color = color;
  this.weight = weight;
  this.engineRunning = false;
}

const myCar = new Car('red', '2mt');
myCar.color;
// => 'red'
myCar.engineRunning;
// => false

Instance Methods

Methods are added via the prototype property of the constructor function. Inside a method, you can access the fields of the instance via this. This works because of the following general rule.

When a function is called as a method of an object, its this is set to the object the method is called on. 1

function Car() {
  this.engineRunning = false;
  // ...
}

Car.prototype.startEngine = function () {
  this.engineRunning = true;
};

Car.prototype.addGas = function (litre) {
  // ...
};

const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true

The Prototype Chain

myCar in the example above is a regular JavaScript object and if we would inspect it (e.g. in the browser console), we would not find a property startEngine with a function as a value directly inside the myCar object. So how does the code above even work then?

The secret here is called the prototype chain. When you try to access any property (field or method) of an object, JavaScript first checks whether the respective key can be found directly in the object itself. If not, it continues to look for the key in the object referenced by the [[prototype]] property of the original object. As mentioned before, in our example [[prototype]] points to the prototype property of the constructor function. That is where JavaScript would find the startEngine function because we added it there.

function Car() {
  // ...
}

Car.prototype.startEngine = function () {
  // ...
};

And the chain does not end there. The [[prototype]] property of Car.prototype (myCar.[[prototype]].[[prototype]]) references Object.prototype (the prototype property of the Object constructor function). It contains general methods that are available for all JavaScript objects, e.g. toString(). In conclusion, you can call myCar.toString() and that method will exist because JavaScript searches for that method throughout the whole prototype chain.

Note that the prototype chain is only travelled when retrieving a value. Setting a property directly or deleting a property of an instance object only targets that specific instance. This might not be what you would expect when you are used to a language with class-based inheritance.


References


Backlinks