JavaScript for impatient programmers (beta)
Please support this book: buy it or donate
(Ad, please don’t block.)

23. Prototype chains and classes

In this book, JavaScript’s style of object-oriented programming (OOP) is introduced in four steps. This chapter covers steps 2–4, the previous chapter covers step 1. The steps are (fig. 7):

  1. Single objects: How do objects, JavaScript’s basic OOP building blocks, work in isolation?
  2. Prototype chains: Each object has a chain of zero or more prototype objects. Prototypes are JavaScript’s core inheritance mechanism.
  3. Classes: JavaScript’s classes are factories for objects. The relationship between a class and its instances is based on prototypal inheritance.
  4. Subclassing: The relationship between a subclass and its superclass is also based on prototypal inheritance.
Figure 7: This book introduces object-oriented programming in JavaScript in four steps.
Figure 7: This book introduces object-oriented programming in JavaScript in four steps.

23.1. Prototype chains

Prototypes are JavaScript’s only inheritance mechanism: Each object has a prototype that is either null or an object. In the latter case, the object inherits all of the prototype’s properties.

In an object literal, you can set the prototype via the special property __proto__:

const proto = {
  protoProp: 'a',
};
const obj = {
  __proto__: proto,
  objProp: 'b',
};

// obj inherits .protoProp:
assert.equal(obj.protoProp, 'a');
assert.equal('protoProp' in obj, true);

Given that a prototype object can have a prototype itself, we get a chain of objects – the so-called prototype chain. That means that inheritance gives us the impression that we are dealing with single objects, but we are actually dealing with chains of objects.

Fig. 8 shows what the prototype chain of obj looks like.

Figure 8: obj starts a chain of objects that continues with proto and other objects.
Figure 8: obj starts a chain of objects that continues with proto and other objects.

Non-inherited properties are called own properties. obj has one own property, .objProp.

23.1.1. Pitfall: only first member of prototype chain is mutated

One aspect of prototype chains that may be counter-intuitive is that setting any property via an object – even an inherited one – only changes that object – never one of the prototypes.

Consider the following object obj:

const proto = {
  protoProp: 'a',
};
const obj = {
  __proto__: proto,
  objProp: 'b',
};

When we set the inherited property obj.protoProp in line A, we “change” it by creating an own property: When reading obj.protoProp, the own property is found first and its value overrides the value of the inherited property.

assert.deepEqual(Object.keys(obj), ['objProp']);

obj.protoProp = 'x'; // (A)

// We created a new own property:
assert.deepEqual(Object.keys(obj), ['objProp', 'protoProp']);

// The inherited property itself is unchanged:
assert.equal(proto.protoProp, 'a');

The prototype chain of obj is depicted in fig. 9.

Figure 9: The own property .protoProp of obj overrides the property inherited from proto.
Figure 9: The own property .protoProp of obj overrides the property inherited from proto.

23.1.2. Tips for working with prototypes (advanced)

These are a few things to keep in mind when working with prototypes:

The following code demonstrates some of the mentioned features:

const proto1 = {};
const proto2 = {};

const obj = Object.create(proto1);
assert.equal(Object.getPrototypeOf(obj), proto1);
assert.equal(proto1.isPrototypeOf(obj), true);

Object.setPrototypeOf(obj, proto2);
assert.equal(proto2.isPrototypeOf(obj), true);
assert.equal(proto1.isPrototypeOf(obj), false);

23.1.3. Sharing data via prototypes

Consider the following code:

const jane = {
  name: 'Jane',
  describe() {
    return 'Person named '+this.name;
  },
};
const tarzan = {
  name: 'Tarzan',
  describe() {
    return 'Person named '+this.name;
  },
};

assert.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan');

We have two objects that are very similar. Both have two properties whose names are .name and .describe. Additionally, method .describe() is the same. How can we avoid that method being duplicated?

We can move it to a shared prototype, PersonProto:

const PersonProto = {
  describe() {
    return 'Person named ' + this.name;
  },
};
const jane = {
  __proto__: PersonProto,
  name: 'Jane',
};
const tarzan = {
  __proto__: PersonProto,
  name: 'Tarzan',
};

The name of the prototype reflects that both jane and tarzan are persons.

Figure 10: Objects jane and tarzan share method .describe(), via their common prototype PersonProto.
Figure 10: Objects jane and tarzan share method .describe(), via their common prototype PersonProto.

The diagram in fig. 10 illustrates how the three objects are connected: The objects at the bottom now contain the properties that are specific to jane and tarzan. The object at the top contains the properties that is shared between them.

When you make the method call jane.describe(), this points to the receiver of that method call, jane (in the bottom left corner of the diagram). That’s why the method still works. The analogous thing happens when you call tarzan.describe().

assert.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan');

23.1.4. Dispatched vs. direct method calls (advanced)

Let’s examine how method calls work with prototype chains. We revisit jane from the previous example:

const PersonProto = {
  describe() {
    return 'Person named ' + this.name;
  },
};
const jane = {
  __proto__: PersonProto,
  name: 'Jane',
};

Fig. 11 has a diagram with jane’s prototype chain.

Figure 11: The prototype chain of jane starts with jane and continues with PersonProto.
Figure 11: The prototype chain of jane starts with jane and continues with PersonProto.

Normal method calls are dispatched. To make the method call jane.describe():

This way of dynamically looking for methods, is called dynamic dispatch.

You can make the same method call while (mostly) bypassing dispatch:

PersonProto.describe.call(jane)

This time, PersonProto.describe is an own property and there is no need to search the prototypes. We also specify this ourselves, via .call().

Note how this always points to the beginning of a prototype chain. That enables .describe() to access .name. And it is where the mutations happen (should a method want to set the .name).

23.2. Classes

We are now ready to take on classes, which are basically a compact syntax for setting up prototype chains. While their foundations may be unconventional, working with JavaScript’s classes should still feel familiar – if you have used an object-oriented language before.

23.2.1. A class for persons

We have previously worked with jane and tarzan, single objects representing persons. Let’s use a class to implement a factory for persons:

class Person {
  constructor(name) {
    this.name = name;
  }
  describe() {
    return 'Person named '+this.name;
  }
}

jane and tarzan can now be created via new Person():

const jane = new Person('Jane');
assert.equal(jane.describe(), 'Person named Jane');

const tarzan = new Person('Tarzan');
assert.equal(tarzan.describe(), 'Person named Tarzan');

23.2.2. Class expressions

The previous class definition was a class declaration. There are also anonymous class expressions:

const Person = class { ··· };

And named class expressions:

const Person = class MyClass { ··· };

23.2.3. Classes under the hood (advanced)

There is a lot going on under the hood of classes. Let’s look at the diagram for jane (fig. 12).

Figure 12: The class Person has the property .prototype that points to an object that is the prototype of all instances of Person. jane is one such instance.
Figure 12: The class Person has the property .prototype that points to an object that is the prototype of all instances of Person. jane is one such instance.

The main purpose of class Person is to set up the prototype chain on the right (jane, followed by Person.prototype). It is interesting to note that both constructs inside class Person (.constructor and .describe()) created properties for Person.prototype, not for Person.

The reason for this slightly odd approach is backward compatibility: Prior to classes, constructor functions were often used as factories for objects. Classes are mostly better syntax for constructor functions and therefore remain compatible with old code. That explains why classes are functions:

> typeof Person
'function'

In this book, I use the terms constructor (function) and class interchangeably.

The name of property .prototype is not ideal: Person.prototype does not point the the prototype of Person, it points to the prototype of all instances of Person. Many people confuse .__proto__ and .prototype. Hopefully, the diagram in fig. 12 makes it clear how they differ.

23.2.3.1. Person.prototype.constructor

There is one detail in fig. 12 that we haven’t look at, yet: Person.prototype.constructor points back to Person:

> Person.prototype.constructor === Person
true

This setup is also there for historical reasons. But it also has two benefits.

First, each instance of a class inherits property .constructor. Therefore, given an instance, you can make “similar” objects via it:

const jane = new Person('Jane');

const cheeta = new jane.constructor('Cheeta');
assert.ok(cheeta instanceof Person);

Second, you can get the name of the class that created a given instance:

const tarzan = new Person('Tarzan');

assert.equal(tarzan.constructor.name, 'Person');

23.2.4. Class definitions: prototype properties

The following code demonstrates all parts of a class definition Foo that create properties of Foo.prototype:

class Foo {
  constructor(prop) {
    this.prop = prop;
  }
  protoMethod() {
    return 'protoMethod';
  }
  get protoGetter() {
    return 'protoGetter';
  }
}

Let’s examine them in order:

The following interaction uses class Foo:

> const foo = new Foo(123);
> foo.prop
123

> foo.protoMethod()
'protoMethod'
> foo.protoGetter
'protoGetter'

23.2.5. Class definitions: static properties

The following code demonstrates all parts of a class definition that create so-called static properties – properties of the class itself.

class Bar {
  static staticMethod() {
    return 'staticMethod';
  }
  static get staticGetter() {
    return 'staticGetter';
  }
}

The static method and the static getter are used as follows.

> Bar.staticMethod()
'staticMethod'
> Bar.staticGetter
'staticGetter'

23.2.6. The instanceof operator

The instanceof operator tells you if a value is an instance of a given class:

> new Person('Jane') instanceof Person
true
> ({}) instanceof Person
false
> ({}) instanceof Object
true
> [] instanceof Array
true

23.2.7. Why I recommend classes

I recommend using classes for the following reasons:

That doesn’t mean that classes are perfect. One issue I have with them, is:

It would be nice if classes were (syntax for) constructor objects (new-able prototype objects) and not to constructor functions. But backward compatibility is a legitimate reason for them being the latter.

  Exercise: Implementing a class

exercises/proto-chains-classes/point_class_test.js

23.3. Private data for classes

This section describes techniques for hiding some of the data of an object from the outside. We discuss them in the context of classes, but they also work for objects created directly, via object literals etc.

23.3.1. Private data: naming convention

The first technique makes a property private by prefixing its name with an underscore. This doesn’t protect the property in any way; it merely signals to the outside: “You don’t need to know about this property.”

In the following code, the properties ._counter and ._action are private.

class Countdown {
  constructor(counter, action) {
    this._counter = counter;
    this._action = action;
  }
  dec() {
    if (this._counter < 1) return;
    this._counter--;
    if (this._counter === 0) {
      this._action();
    }
  }
}

// The two properties aren’t really private:
assert.deepEqual(
  Reflect.ownKeys(new Countdown()),
  ['_counter', '_action']);

With this technique, you don’t get any protection and private names can clash. On the plus side, it is easy to use.

23.3.2. Private data: WeakMaps

Another technique is to use WeakMaps. How exactly that works is explained in the chapter on Maps (to be written). This is a preview:

let _counter = new WeakMap();
let _action = new WeakMap();

class Countdown {
  constructor(counter, action) {
    _counter.set(this, counter);
    _action.set(this, action);
  }
  dec() {
    let counter = _counter.get(this);
    if (counter < 1) return;
    counter--;
    _counter.set(this, counter);
    if (counter === 0) {
      _action.get(this)();
    }
  }
}

// The two pseudo-properties are truly private:
assert.deepEqual(
  Reflect.ownKeys(new Countdown()),
  []);

This technique offers you considerable protection from outside access and there can’t be any name clashes. But it is also more complicated to use.

23.3.3. More techniques for private data

There are more techniques for private data for classes. These are explained in “Exploring ES6”.

The reason why this section does not go into much depth, is that JavaScript will probably soon have built-in support for private data. Consult the ECMAScript proposal “Class Public Instance Fields & Private Instance Fields” for details.

23.4. Subclassing

Classes can also subclass (“extend”) existing classes. As an example, the following class Employee subclasses Person:

class Person {
  constructor(name) {
    this.name = name;
  }
  describe() {
    return `Person named ${this.name}`;
  }
  static logNames(persons) {
    for (const person of persons) {
      console.log(person.name);
    }
  }
}

class Employee extends Person {
  constructor(name, title) {
    super(name);
    this.title = title;
  }
  describe() {
    return super.describe() +
      ` (${this.title})`;
  }
}

const jane = new Employee('Jane', 'CTO');
assert.equal(
  jane.describe(),
  'Person named Jane (CTO)');

Two comments:

  Exercise: Subclassing

exercises/proto-chains-classes/color_point_class_test.js

23.4.1. Subclasses under the hood (advanced)

Figure 13: These are the objects that make up class Person and its subclass, Employee. The left column is about classes. The right column is about the Employee instance jane and its prototype chain.
Figure 13: These are the objects that make up class Person and its subclass, Employee. The left column is about classes. The right column is about the Employee instance jane and its prototype chain.

The classes Person and Employee from the previous section are made up of several objects (fig. 13). One key insight for understanding how these objects are related, is that there are two prototype chains:

23.4.1.1. The instance prototype chain (right column)

The instance prototype chain starts with jane and continues with Employee.prototype and Person.prototype. In principle, the prototype chain ends at this point, but we get one more object: Object.prototype. This prototype provides services to virtually all objects, which is why it is included here, too:

> Object.getPrototypeOf(Person.prototype) === Object.prototype
true
23.4.1.2. The class prototype chain (left column)

In the class prototype chain, Employee comes first, Person next. Afterwards, the chain continues with Function.prototype, which is only there, because Person is a function and functions need the services of Function.prototype.

> Object.getPrototypeOf(Person) === Function.prototype
true

23.4.2. instanceof in more detail (advanced)

We have not yet seen how instanceof really works. Given the expression x instanceof C, how does instanceof determine if x is an instance of C? It does so by checking if C.prototype is in the prototype chain of x. That is, the following two expressions are equivalent:

x instanceof C
C.prototype.isPrototypeOf(x)

If we go back to fig. 13, we can confirm that the prototype chain does lead us to the following answers:

> jane instanceof Employee
true
> jane instanceof Person
true
> jane instanceof Object
true

23.4.3. Prototype chains of built-in objects (advanced)

Next, we’ll use our knowledge of subclassing to understand the prototype chains of a few built-in objects. The following tool function p() helps us with our explorations.

const p = Object.getPrototypeOf.bind(Object);

We extracted method .getPrototypeOf() of Object and assigned it to p.

23.4.3.1. The prototype chain of {}

Let’s start by examining plain objects:

> p({}) === Object.prototype
true
> p(p({})) === null
true
Figure 14: The prototype chain of an object created via an object literal starts with that object, continues with Object.prototype and ends with null.
Figure 14: The prototype chain of an object created via an object literal starts with that object, continues with Object.prototype and ends with null.

Fig. 14 shows a diagram for this prototype chain. We can see that {} really is an instance of ObjectObject.prototype is in its prototype chain.

Object.prototype is a curious value: It is an object, but it is not an instance of Object:

> typeof Object.prototype
'object'
> Object.prototype instanceof Object
false

That can’t be avoided, because Object.prototype can’t be in its own prototype chain.

23.4.3.2. The prototype chain of []

What does the prototype chain of an Array look like?

> p([]) === Array.prototype
true
> p(p([])) === Object.prototype
true
> p(p(p([]))) === null
true
Figure 15: The prototype chain of an Array has these members: the Array instance, Array.prototype, Object.prototype, null.
Figure 15: The prototype chain of an Array has these members: the Array instance, Array.prototype, Object.prototype, null.

This prototype chain (visualized in fig. 15) tells us that an Array object is an instance of Array, which is a subclass of Object.

23.4.3.3. The prototype chain of function () {}

Lastly, the prototype chain of an ordinary function tells us that all functions are objects:

> p(function () {}) === Function.prototype
true
> p(p(function () {})) === Object.prototype
true

23.4.4. Mixin classes (advanced)

JavaScript’s class system only supports single inheritance. That is, each class can have at most one superclass. A way around this limitation is via a technique called mixin classes (short: mixins).

The idea is as follows: Let’s assume there is a class C that extends a class S – its superclass. Mixins are class fragments that are inserted between C and S.

In JavaScript, you can implement a mixin Mix via function whose input is a class and whose output is the mixin class fragment – a new class that extends the input. To use Mix(), you create C as follows.

class C extends Mix(S) {
  ···
}

Let’s look at an example:

const Branded = S => class extends S {
  setBrand(brand) {
    this._brand = brand;
    return this;
  }
  getBrand() {
    return this._brand;
  }
};

We use this mixin to insert a class between Car and Object:

class Car extends Branded(Object) {
  constructor(model) {
    super();
    this._model = model;
  }
  toString() {
    return `${this.getBrand()} ${this._model}`;
  }
}

The following code confirms that the mixin worked: Car has method .setBrand() of Branded.

const modelT = new Car('Model T').setBrand('Ford');
assert.equal(modelT.toString(), 'Ford Model T');

Mixins are more flexible than normal classes:

  Quiz

See quiz app.