instanceof
operatorIn 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. 9):
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. 10 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
.
Some operations consider all properties (own and inherited). For example, getting properties:
> const obj = { foo: 1 };
> typeof obj.foo // own
'number'
> typeof obj.toString // inherited
'function'
Other operations only consider own properties. For example, Object.keys()
:
Read on for another operation that also only considers own properties: setting properties.
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 very object – never one of the prototypes.
Consider the following object obj
:
In the next code snippet, we set the inherited property obj.protoProp
(line A). That “changes” 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.
// In the beginning, obj has one own 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 own property overrides the inherited property:
assert.equal(obj.protoProp, 'x');
The prototype chain of obj
is depicted in fig. 11.
.protoProp
of obj
overrides the property inherited from proto
.__proto__
, except in object literalsI recommend to avoid the pseudo-property __proto__
: As we will see later, not all objects have it.
However, __proto__
in object literals is different. There, it is a built-in feature and always available.
The recommended ways of getting and setting prototypes are:
The best way to get a prototype is via the following method:
The best way to set a prototype is when creating an object – via __proto__
in an object literal or via:
If you have to, you can use Object.setPrototypeOf()
to change the prototype of an existing object. But that may affect performance negatively.
This is how these features are used:
const proto1 = {};
const proto2 = {};
const obj = Object.create(proto1);
assert.equal(Object.getPrototypeOf(obj), proto1);
Object.setPrototypeOf(obj, proto2);
assert.equal(Object.getPrototypeOf(obj), proto2);
So far, “p
is a prototype of o
” always meant “p
is a direct prototype of o
”. But it can also be used more loosely and mean that p
is in the prototype chain of o
. That looser relationship can be checked via:
For example:
const a = {};
const b = {__proto__: a};
const c = {__proto__: b};
assert.equal(a.isPrototypeOf(b), true);
assert.equal(a.isPrototypeOf(c), true);
assert.equal(a.isPrototypeOf(a), false);
assert.equal(c.isPrototypeOf(a), false);
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 an object PersonProto
and make that object a prototype of both jane
and tarzan
:
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.
jane
and tarzan
share method .describe()
, via their common prototype PersonProto
.The diagram in fig. 12 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 are 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. tarzan.describe()
works similarly.
assert.equal(jane.describe(), 'Person named Jane');
assert.equal(tarzan.describe(), 'Person named Tarzan');
We are now ready to take on classes, which are basically a compact syntax for setting up prototype chains. Under the hood, JavaScript’s classes are unconventional. But that is something you rarely see when working with them. They should normally feel familiar to people who have used other object-oriented programming languages.
We have previously worked with jane
and tarzan
, single objects representing persons. Let’s use a class declaration to implement a factory for person objects:
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.name, 'Jane');
assert.equal(jane.describe(), 'Person named Jane');
const tarzan = new Person('Tarzan');
assert.equal(tarzan.name, 'Tarzan');
assert.equal(tarzan.describe(), 'Person named Tarzan');
There are two kinds of class definitions (ways of defining classes):
Class expressions can be anonymous and named:
// Anonymous class expression
const Person = class { ··· };
// Named class expression
const Person = class MyClass { ··· };
The name of a named class expression works similarly to the name of a named function expression.
This was a first look at classes. We’ll explore more features soon, but first we need to learn the internals of classes.
There is a lot going on under the hood of classes. Let’s look at the diagram for jane
(fig. 13).
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.
It is easy to confuse .__proto__
and .prototype
. Hopefully, the diagram in fig. 13 makes it clear, how they differ:
.__proto__
is a pseudo-property for accessing the prototype of an object..prototype
is a normal property that is only special due to how the new
operator uses it. The name is not ideal: Person.prototype
does not point to the prototype of Person
, it points to the prototype of all instances of Person
.Person.prototype.constructor
(advanced)There is one detail in fig. 13 that we haven’t looked at, yet: Person.prototype.constructor
points back to Person
:
This setup also exists due to backward compatibility. But it has two additional 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');
// cheeta is also an instance of Person
// (the instanceof operator is explained later)
assert.equal(cheeta instanceof Person, true);
Second, you can get the name of the class that created a given instance:
All constructs in the body of the following class declaration, 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:
.constructor()
is called after creating a new instance of Foo
, to set up that instance..protoMethod()
is a normal method. It is stored in Foo.prototype
..protoGetter
is a getter that is stored in Foo.prototype
.The following interaction uses class Foo
:
> const foo = new Foo(123);
> foo.prop
123
> foo.protoMethod()
'protoMethod'
> foo.protoGetter
'protoGetter'
All constructs in the body of the following class declaration, create so-called static properties – properties of Bar
itself.
class Bar {
static staticMethod() {
return 'staticMethod';
}
static get staticGetter() {
return 'staticGetter';
}
}
The static method and the static getter are used as follows.
instanceof
operatorThe 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
We’ll explore the instanceof
operator in more detail later, after we have looked at subclassing.
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.). This is an improvement to how things were before, when almost every framework had its own inheritance library.
They help tools such as IDEs and type checkers with their work and enable new features there.
If you come from another language to JavaScript and are used to classes, then you can get started more quickly.
JavaScript engines optimize them. That is, code that uses classes is almost always faster than code that uses a custom inheritance library.
You can subclass built-in constructor functions such as Error
.
That doesn’t mean that classes are perfect:
There is a risk of overdoing inheritance.
There is a risk of putting too much functionality in classes (when some of it is often better put in functions).
How they work superficially and under the hood is quite different. In other words, there is a disconnect between syntax and semantics. Two examples are:
C
creates a method in the object C.prototype
.The motivation for the disconnect is backward compatibility. Thankfully, the disconnect causes few problems in practice; you are usually OK if you go along what classes pretend to be.
Exercise: Writing a class
exercises/proto-chains-classes/point_class_test.mjs
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, e.g. via object literals.
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() {
this._counter--;
if (this._counter === 0) {
this._action();
}
}
}
// The two properties aren’t really private:
assert.deepEqual(
Object.keys(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.
Another technique is to use WeakMaps. How exactly that works is explained in the chapter on WeakMaps. This is a preview:
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
counter--;
_counter.set(this, counter);
if (counter === 0) {
_action.get(this)();
}
}
}
// The two pseudo-properties are truly private:
assert.deepEqual(
Object.keys(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.
This book explains the most important techniques for private data in classes. There will also probably soon be built-in support for it. Consult the ECMAScript proposal “Class Public Instance Fields & Private Instance Fields” for details.
A few additional techniques are explained in “Exploring ES6”.
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 via super()
, before you can access this
. That’s because this
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()
:
Exercise: Subclassing
exercises/proto-chains-classes/color_point_class_test.mjs
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. 14). One key insight for understanding how these objects are related, is that there are two prototype chains:
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:
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
.
instanceof
in more detail (advanced)We have not yet seen how instanceof
really works. Given the expression:
How does instanceof
determine if x
is an instance of C
(or of a subclass of C
)? It does so by checking if C.prototype
is in the prototype chain of x
. That is, the following expression is equivalent:
If we go back to fig. 14, we can confirm that the prototype chain does lead us to the following correct answers:
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
.
{}
Let’s start by examining plain objects:
Object.prototype
and ends with null
.Fig. 15 shows a diagram for this prototype chain. We can see that {}
really is an instance of Object
– Object.prototype
is in its prototype chain.
[]
What does the prototype chain of an Array look like?
Array.prototype
, Object.prototype
, null
.This prototype chain (visualized in fig. 16) tells us that an Array object is an instance of Array
, which is a subclass of Object
.
function () {}
Lastly, the prototype chain of an ordinary function tells us that all functions are objects:
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:
.__proto__
work?The pseudo-property .__proto__
is implemented by class Object
, via a getter and a setter. It could be implemented like this:
class Object {
get __proto__() {
return Object.getPrototypeOf(this);
}
set __proto__(other) {
Object.setPrototypeOf(this, other);
}
// ···
}
That means that you can switch .__proto__
off, by creating an object that doesn’t have Object.prototype
in its prototype chain (see previous section):
Let’s examine how method calls work with classes. We are revisiting jane
from earlier:
class Person {
constructor(name) {
this.name = name;
}
describe() {
return 'Person named '+this.name;
}
}
const jane = new Person('Jane');
Fig. 17 has a diagram with jane
’s prototype chain.
jane
starts with jane
and continues with Person.prototype
.Normal method calls are dispatched – the method call jane.describe()
happens in two steps:
Dispatch: In the prototype chain of jane
, find the first property whose key is 'describe'
and retrieve its value.
Call: Call the value, while setting this
to jane
.
This way of dynamically looking for a method and invoking it, is called dynamic dispatch.
You can make the same method call directly, without dispatching:
This time, we directly point to the method, via Person.prototype.describe
and don’t search for it in the prototype chain. We also specify this
differently, via .call()
.
Note that this
always points to the beginning of a prototype chain. That enables .describe()
to access .name
.
Direct method calls become useful when you are working with methods of Object.prototype
. For example, Object.prototype.hasOwnProperty(k)
checks if this
has a non-inherited property whose key is k
:
However, in the prototype chain of an object, there may be another property with the key 'hasOwnProperty'
, that overrides the method in Object.prototype
. Then a dispatched method call doesn’t work:
> const obj = { hasOwnProperty: true };
> obj.hasOwnProperty('bar')
TypeError: obj.hasOwnProperty is not a function
The work-around is to use a direct method call:
> Object.prototype.hasOwnProperty.call(obj, 'bar')
false
> Object.prototype.hasOwnProperty.call(obj, 'hasOwnProperty')
true
This kind of direct method call is often abbreviated as follows:
This pattern may seem inefficient, but most engines optimize this pattern, so that performance should not be an issue.
JavaScript’s class system only supports single inheritance. That is, each class can have at most one superclass. One way around this limitation is via a technique called mixin classes (short: mixins).
The idea is as follows: Let’s say we want a class C
to inherit from two superclasses S1
and S2
. That would be multiple inheritance, which JavaScript doesn’t support.
Our work-around is to turn S1
and S2
into mixins, factories for subclasses:
const S1 = (Sup) => class extends Sup { /*···*/ };
const S2 = (Sup) => class extends Sup { /*···*/ };
Each of these two functions returns a class that extends a given superclass Sup
. We create class C
as follows:
We now have a class C
that extends a class S2
that extends a class S1
that extends Object
(which most classes do, implicitly).
We implement a mixin Branded
that has helper methods for setting and getting the brand of an object:
const Branded = (Sup) => class extends Sup {
setBrand(brand) {
this._brand = brand;
return this;
}
getBrand() {
return this._brand;
}
};
We use this mixin to implement brand management for a class Car
:
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 free us from the constraints of single inheritance:
In principle, objects are unordered. The main reason for ordering properties is so that operations that list entries, keys or values, are deterministic. That helps, e.g., with testing.
Quiz
See quiz app.