The Prototype Chain
All JavaScript objects have an internal [[Prototype]]
property that points to their prototype.
- A prototype can either be an object or null.
- It is impossible to create cyclic prototype chains (attempting to do so will throw a
TypeError
).
It follows from the above that there is a prototype in-tree with null as the root (this may be the only way in which typeof null === "object"
makes some kind of sense).
Initial [[Prototype]]
The initial reference of an object’s internal [[Prototype]]
property depends on how the object was created. There are essentially 4 ways of creating a new object in JavaScript:
Object.create()
- Constructor invocations
-
Literals
- Object literals
- Array literals
- Regular expression literals
- Function expressions
-
Declarations
- Function declarations
- Class declarations
The internal [[Prototype]]
property is set as follows in the different cases:
NB: An exception to the above diagram arises for constructors that explicitly return an object. In those cases the object’s [[Prototype]]
property depends on how the returned object was created and does not automatically point to Constructor.prototype
(see Constructors).
Inspecting the Prototype Chain
Two methods and one operator are available to explore the prototype chain:
Object.getPrototypeOf()
to get the prototype of an object.Object.prototype.isPrototypeOf()
to check whether a given object lies in the prototype chain of another object.instanceof
to test the presence of a constructor’sprototype
property in an object’s prototype chain.
Object.getPrototypeOf()
Object.getPrototypeOf()
returns the prototype of the specified object. If the argument is not an object, it is first attempted to coerce it to an object.
const a = {};
const b = Object.create(a);
console.log(Object.getPrototypeOf(b) === a); // true
console.log(Object.getPrototypeOf(a) === Object.prototype); // true
console.log(Object.getPrototypeOf("hey") === String.prototype); // true | "hey" is coerced to an instance of String
Object.prototype.isPrototypeOf()
Object.prototype.isPrototypeOf()
returns a boolean indicating whether the calling object lies in the prototype chain of the specified argument (for primitive values false is returned).
const a = {};
const b = Object.create(a);
console.log(a.isPrototypeOf(b)); // true
console.log(Object.prototype.isPrototypeOf(b)); // true
console.log(String.isPrototypeOf("hey")); // false
instanceof
The instanceof
operator takes two parameters (one before and one after the operator). The expression evaluates to true if and only if:
- The first parameter is an object (otherwise the expression evaluates to false without checking the second parameter).
- The second parameter is a function with a
prototype
property that points to an object (else aTypeError
is thrown). - The reference of the second parameter’s
prototype
property lies in the first parameter’s prototype chain.
NB 1: When the second parameter is a bound function, it is treated as if it was the target function.
NB 2: In fact, bound functions apart, for a an object o
and a constructor C
, o instanceof C
is equivalent to C.prototype.isPrototypeOf( o )
.
// Primitive values:
console.log("hey" instanceof String); // false
// Non-constructor values:
console.log([] instanceof {}); // TypeError: Right-hand side of 'instanceof' is not callable
// Bound functions:
function Constructor() {}
const BoundConstructor = Constructor.bind();
const instance = new Constructor();
console.log(instance instanceof BoundConstructor); // true
Manipulating the Prototype Chain
Object.setPrototypeOf()
is the only method available to change the prototype chain (other than by creating new objects).
Object.setPrototypeOf()
Object.setPrototypeOf()
sets the internal [[Prototype]]
property of the first argument to the second argument. The first argument is returned.
-
If the first argument is not an object, it is attempted to coerce it to an object.
- In the case of null or undefined, a
TypeError
is thrown. - For primitive values other than null or undefined, no error is thrown but nothing will happen due to autoboxing.
- In the case of null or undefined, a
-
A
TypeError
is thrown in the following cases:- If the first argument is null, undefined or a non-extensible object.
- If the second argument is neither null nor an object.
- If a prototype cycle is about to be created.
// Non-extensible object:
const a = {};
const b = {};
Object.preventExtensions(a);
Object.setPrototypeOf(a, b); // TypeError: #<Object> is not extensible
// Cyclic prototype chain:
const a = {};
const b = {};
const c = {};
Object.setPrototypeOf(a, b);
Object.setPrototypeOf(b, c);
Object.setPrototypeOf(c, a); // TypeError: Cyclic __proto__ value
Performance
Although Object.setPrototypeOf()
is part of the language specifications, its use is discouraged as it can be a very slow operation due to how engines optimize property accesses.
Caveats
Unintuitive Behavior
Note that Object.getPrototypeOf()
deals differently with primitive values than Object.prototype.isPrototypeOf()
and instanceof
, so that for instance Object.getPrototypeOf(a).isPrototypeOf(a) === true
does not always hold.
const string = "hey";
// getPrototypeOf() coerces primitives to objects:
console.log(Object.getPrototypeOf(string) === String.prototype); // true
// isPrototypeOf() and instanceof always evaluate to false for primitive values:
console.log(String.prototype.isPrototypeOf(string)); // false
console.log(string instanceof String); // false
When passing objects between different execution environments the built-in constructors will not share the same reference, which can lead to unintuitive results such as:
console.log([] instanceof Array); // false
console.log(Object.prototype.isPrototypeOf({})); // false
Writable [[Prototype]]
and prototype
properties
Given that an instance’s internal [[Prototype]]
property and a constructor’s own prototype
property are writable, there is no guarantee that an instanceof
expression will evaluate to true for objects created from a given constructor.
function Constructor() {}
const instance = new Constructor();
// Replacing the instance's internal [[Prototype]] property
Object.setPrototypeOf(instance, {});
console.log(instance instanceof Constructor); // false
// Replacing the constructor's own prototype property
Constructor.prototype = {};
console.log(instance instanceof Constructor); // false
Syntax
Don’t forget parentheses when negating the instanceof
operator:
console.log(!({} instanceof Object)); // true
console.log(!{} instanceof Object); // false (!{} evaluates to false, which is a primitive value)
ESLint’s no-unsafe-negation rule prevents those kind of errors.
Legacy
Many browsers implemented an accessor property __proto__
that exposed the internal [[Prototype]]
property. This was included in ES6 as a legacy feature but is deprecated and should not be used.
Resources
- Chapter 4 - Constructors and Prototypes, The Principles of Object-Oriented JavaScript, Zakas (2014).
- Direct comparison of
instanceof
andisPrototypeOf
, StackOverflow.