- 26. Prototype chains and classes
- 26.1. Prototype chains
- 26.2. Classes
- 26.3. Private data for classes
- 26.4. Subclassing
26. 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. 8):
- Single objects: How do objects, JavaScript’s basic OOP building blocks, work in isolation?
- Prototype chains: Each object has a chain of zero or more prototype objects. Prototypes are JavaScript’s core inheritance mechanism.
- Classes: JavaScript’s classes are factories for objects. The relationship between a class and its instances is based on prototypal inheritance.
- Subclassing: The relationship between a subclass and its superclass is also based on prototypal inheritance.
Figure 8: This book introduces object-oriented programming in JavaScript in four steps.
26.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
:
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. 9 shows what the prototype chain of obj
looks like.
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
.
26.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
:
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.
The prototype chain of obj
is depicted in fig. 10.
.protoProp
of obj
overrides the property inherited from proto
.26.1.2. Tips for working with prototypes (advanced)
26.1.2.1. Avoid proto (except in object literals)
I recommend to avoid the special property proto
: It is implemented via a getter and a setter in Object.prototype
and therefore only available if Object.prototype
is in the prototype chain of an object. That is usually the case, but to be safe, you can use these alternatives:
- The best way to set a prototype is when creating an object. E.g. via:
If you have to, you can use Object.setPrototypeOf()
to change the prototype of an existing object.
- The best way to get a prototype is via the following method:
This is how these features are used:
Note that proto
in object literals is different. There, it is a built-in feature and always safe to use.
26.1.2.2. Check: is an object a prototype of another one?
A looser definition of “o
is a prototype of p
” is “o
is in the prototype chain of p
”. This relationship can be checked via:
For example:
26.1.3. Sharing data via prototypes
Consider the following code:
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
:
The name of the prototype reflects that both jane
and tarzan
are persons.
jane
and tarzan
share method .describe()
, via their common prototype PersonProto
.The diagram in fig. 11 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()
.
26.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.
26.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:
jane
and tarzan
can now be created via new Person()
:
26.2.2. Class expressions
The previous class definition was a class declaration. There are also anonymous class expressions:
And named class expressions:
26.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).
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 (ordinary functions, invoked via the new
operator) 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:
In this book, I use the terms constructor (function) and class interchangeably.
Many people confuse .proto
and .prototype
. Hopefully, the diagram in fig. 12 makes it clear, how they differ:
.proto
is a special property for accessing the prototype of an object..prototype
is a normal property that is only special due to how thenew
operator uses it. The name is not ideal:Person.prototype
does not point to the prototype ofPerson
, it points to the prototype of all instances ofPerson
.
26.2.3.1. Person.prototype.constructor
There is one detail in fig. 12 that we haven’t looked at, yet: Person.prototype.constructor
points back to Person
:
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:
Second, you can get the name of the class that created a given instance:
26.2.4. Class definitions: prototype properties
The following code demonstrates all parts of a class definition Foo
that create properties of Foo.prototype
:
Let’s examine them in order:
.constructor()
is called after creating a new instance ofFoo
, to set up that instance..protoMethod()
is a normal method. It is stored inFoo.prototype
..protoGetter
is a getter that is stored inFoo.prototype
.
The following interaction uses classFoo
:
26.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.
The static method and the static getter are used as follows.
26.2.6. The instanceof operator
The instanceof
operator tells you if a value is an instance of a given class:
We’ll explore the instanceof
operator in more detail later, after we have looked at subclassing.
26.2.7. Why I recommend classes
I recommend using classes for the following reasons:
- Classes are a common standard for object creation and inheritance that is now widely supported across frameworks (React, Angular, Ember, etc.).
- They help tools such as IDEs and type checkers with their work and enable new features.
- They are a foundation for future features such as value objects, immutable objects, decorators, etc.
- They make it easier for newcomers to get started with JavaScript.
JavaScript engines optimize them. That is, code that uses classes is usually faster than code that uses a custom inheritance library.
That doesn’t mean that classes are perfect. One issue I have with them, is:Classes look different from what they are under the hood. In other words, there is a disconnect between syntax and semantics.
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.
26.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.
26.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.
26.3.2. Private data: WeakMaps
Another technique is to use WeakMaps. How exactly that works is explained in the chapter on WeakMaps. 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.
26.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.
26.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:
Inside a
.constructor()
method, you must call the super-constructor viasuper()
, before you can accessthis
. That’s becausethis
doesn’t exist before the super-constructor was called (this phenomenon is specific to classes).Static methods are also inherited. For example,
Employee
inherits the static method.logNames()
:
26.4.1. Subclasses under the hood (advanced)
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:
- The instance prototype chain, on the right.
- The class prototype chain, on the left.
26.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:
26.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
.
26.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:
If we go back to fig. 13, we can confirm that the prototype chain does lead us to the following answers:
26.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.
We extracted method .getPrototypeOf()
of Object
and assigned it to p
.
26.4.3.1. The prototype chain of {}
Let’s start by examining plain objects:
Object.prototype
and ends with null
.Fig. 14 shows a diagram for this prototype chain. We can see that {}
really is an instance of Object
– Object.prototype
is in its prototype chain.
Object.prototype
is a curious value: It is an object, but it is not an instance of Object
:
That can’t be avoided, because Object.prototype
can’t be in its own prototype chain.
26.4.3.2. The prototype chain of []
What does the prototype chain of an Array look like?
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
.
26.4.3.3. The prototype chain of function () {}
Lastly, the prototype chain of an ordinary function tells us that all functions are objects:
26.4.3.4. Objects that aren’t instances of Object
An object is only an instance of Object
if Object.prototype
is in its prototype chain. Most objects created via various literals are instances of Object
:
Objects that don’t have prototypes are not instances of Object
:
Object.prototype
ends most prototype chains. Its prototype is null
, which means it isn’t an instance of Object
, either:
26.4.4. Dispatched vs. direct method calls (advanced)
Let’s examine how method calls work with classes. We revisit jane
from earlier:
Fig. 16 has a diagram with jane
’s prototype chain.
jane
starts with jane
and continues with Person.prototype
.Normal method calls are dispatched. To make the method call jane.describe()
:
- JavaScript first looks for the value of
jane.describe
, by traversing the prototype chain. - Then it calls the function it found, while setting
this
tojane
.this
is the receiver of the method call (where the search for property.describe
started).
This way of dynamically looking for methods, is called dynamic dispatch.
You can make the same method call while bypassing dispatch:
This time, Person.prototype.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
).
26.4.4.1. Borrowing methods
Direct method calls become useful when you are working with methods of Object.prototype
. For example, Object.prototype.hasOwnProperty()
checks if an object has a non-inherited property whose key is as given:
However, this method may be overridden. Then a dispatched method call doesn’t work:
The work-around is to use a direct method call:
This kind of direct method call is often abbreviated as follows:
JavaScript engines optimize this pattern, so that performance should not be an issue.
26.4.5. 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 a 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.
Let’s look at an example:
We use this mixin to insert a class between Car
and Object
:
The following code confirms that the mixin worked: Car
has method .setBrand()
of Branded
.
Mixins are more flexible than normal classes:
First, you can use the same mixin multiple times, in multiple classes.
Second, you can use multiple mixins at the same time. As an example, consider an additional mixin called
Stringifiable
that helps with implementing.toString()
. We could use bothBranded
andStringifiable
as follows: