Prototypes and Inheritance
Notes on prototypes and inheritance in JS
Updated
Prototypes
Introduction
- An instance's
constructoris the constructor function, and a constructor'sconstructorisFunction— includingFunctionitself (Function.__proto__ === Function.prototype) prototype: points to the shared instance of itself given to subclasses. Only things inheriting fromFunctionhave aprototype; it points to the shared object created bynewwhen the current function is used as a constructor. For example,Person.prototypeis the shared prototype ofnew Person()instances, so for any class,.prototype.constructoris the class itself__proto__: points to the shared instance the current object uses, i.e. the parent's shared instance. Every object (includingFunction) has__proto__, pointing to its constructor'sprototype. For example,new Person()'s__proto__points toPerson.prototype- Almost all objects inherit from
Object.prototype, but you can also useObject.create(null)to point the prototype chain atnull(which loses built-in methods liketoString)
Relationships
Constructors and Instances
If object obj is created by constructor A, then obj.__proto__ === A.prototype
function Foo() {}
const f = new Foo()
f.__proto__ === Foo.prototype // true
Plain Object and Function Instances
// Plain object chain
let obj = {};
obj.__proto__ → Object.prototype → null
// Function object chain
function Foo() {}
Foo.__proto__ → Function.prototype → Object.prototype → null
The Closed Loop Between the Object and Function Objects
All of the following evaluate to true
// The Function object is constructed by a function, so we have
Function.__proto__ === Function.prototype
// The Object object is also constructed by a function, so we have
Object.__proto__ === Function.prototype
// From the above we can derive
Function.__proto__ === Object.__proto__
// The parent of the Function function object is the Object object, so we have
Function.prototype.__proto__ === Object.prototype
Special Cases
- Although
Function.prototypeis a function, it has noprototype, in order to avoid an infinite loop - Arrow functions cannot be used as classes and have no
prototype, e.g.(() => {}).prototype === undefined, but they do have__proto__, e.g.(() => {}).__proto__ === Function.prototype
typeof Function.prototype // "function"
Function.prototype.prototype // undefined
Summary
---
title: Prototype Chain Reference Diagram
---
flowchart TD
subgraph Custom class
Person[Person function]
PersonProtoType[Person.prototype]
PersonProto[Person.\_\_proto\_\_]
person[new Person]
end
subgraph Top-level Object
Object[Object function]
ObjectProtoType[Object.prototype]
ObjectProto[Object.\_\_proto\_\_]
end
subgraph Top-level Function
Function[Function function]
FunctionProtoType[Function.prototype]
FunctionProto[Function.\_\_proto\_\_]
end
Person --> |.prototype| PersonProtoType
person --> |.\_\_proto\_\_| PersonProtoType
person --> |.constructor| Person
PersonProtoType --> |.constructor| Person
Person --> |.\_\_proto\_\_| PersonProto
PersonProto --> |equivalent to| FunctionProtoType
PersonProtoType --> |.\_\_proto\_\_| ObjectProtoType
Object --> |.prototype| ObjectProtoType
ObjectProtoType --> |.constructor| Object
ObjectProto --> |equivalent to|FunctionProtoType
Object --> |.\_\_proto\_\_|ObjectProto
Function --> |.prototype| FunctionProtoType
Function --> |.\_\_proto\_\_| FunctionProto
FunctionProto --> |equivalent to| FunctionProtoType
FunctionProtoType --> |\.\_\_proto\_\_| ObjectProtoType
ObjectProtoType --> |.\_\_proto\_\_| null
classDef construct fill:#e1f5fe,stroke:#039be5;
classDef proto fill:#f0f4c3,stroke:#9e9d24;
classDef instance fill:#f8bbd0,stroke:#e91e63;
class Person,Object construct;
class PersonProtoType,ObjectProtoType proto;
class person instance;
console.log(Function.prototype.__proto__ === Object.prototype) // true ✅
// Function.prototype is an object; its __proto__ points to Object.prototype
console.log(Function.__proto__ === Object.__proto__) // true ✅
// The __proto__ of all constructors (including Function and Object) points to Function.prototype
console.log(Function.__proto__.__proto__ === Object.prototype) // true ✅
// Function.__proto__ === Function.prototype
// Function.prototype.__proto__ === Object.prototype
console.log(Object.constructor.prototype === Object.prototype) // false ❌
// Object.constructor is actually Function, whose prototype is Function.prototype
console.log(Function.constructor === Function) // true ✅
// The constructor of every class is Function
console.log(Object.constructor === Object) // false ❌
// The constructor of every class is Function
console.log(Array.__proto__ === Function.__proto__) // true ✅
// The __proto__ of all built-in classes (such as Array) points to Function.prototype
console.log(Array.constructor === Function) // true ✅
// The constructor of every class is Function
console.log(Object.__proto__ === Function) // false ❌
// Object.__proto__ actually points to Function.prototype (not Function itself)
console.log(Function.__proto__ === Function.prototype) // true ✅
// Function's __proto__ points to its own prototype (a special design)
console.log(Object instanceof Object) // true ✅
// Object's prototype chain eventually points to Object.prototype; the lookup chain is:
// Object.__proto__ === Function.prototype
// Function.prototype.__proto__ === Object.prototype
console.log(typeof Function === 'function') // true ✅
// Function's prototype chain contains Function.prototype; the lookup chain is:
// Function.__proto__ === Function.prototype
console.log(Map instanceof Map) // false ❌
// Map's prototype is not on its own prototype chain (it cannot prove itself)
The Prototype Chain
Understanding Prototype Objects and the Prototype Chain
JS was originally designed without object-oriented features, so it could only simulate an inheritance mechanism with the prototype chain — essentially to achieve sharing.
The following concepts need to be distinguished:
- Constructor/class: a function used to create shared instances
- Instance: an instance created via
new - Shared instance: a shared instance used to reuse functions, code, and other content. What
__proto__andprototypepoint to are shared instances, not the constructor/class itself; only the shared instance'sconstructorproperty points to the constructor/class
---
title: Relationship Diagram of Instance, Class, and Shared Instance
---
flowchart LR
ins[Instance]
ctor[Constructor/class]
sins[Shared instance]
ins --> |.\_\_proto\_\_| sins
sins --> |.constructor| ctor
classDef ctor fill:#e1f5fe,stroke:#039be5;
class ctor ctor
classDef sins fill:#f0f4c3,stroke:#9e9d24;
class sins sins
classDef ins fill:#f8bbd0,stroke:#e91e63;
class ins ins
Why Manually Modifying __proto__ Stops Working
If an object's __proto__ property is assigned null, its prototype has indeed been changed to null. But if you then try to set the prototype again by assigning to __proto__, it has no effect — at this point __proto__ is no different from an ordinary property. The prototype can only be modified via Reflect.setPrototypeOf or Object.setPrototypeOf. The prototype is an internal property of the object, [[prototype]], and the reason Reflect.setPrototypeOf can modify it is that it directly assigns to the object's internal [[prototype]] property, instead of going through the __proto__ getter.
Sample code
const obj = { name: 'xiaoming' }
obj.__proto__ = null
console.log(obj.__proto__) // => undefined
console.log(Reflect.getPrototypeOf(obj)) // => null
obj.__proto__ = null
console.log(obj.__proto__) // => null
console.log(Reflect.getPrototypeOf(obj)) // => null
obj.__proto__ = { a: 1 }
console.log(obj.__proto__) // => { a: 1 }
console.log(Reflect.getPrototypeOf(obj)) // => null
Reference: TC39 __proto__ standard
A simulated proto looks like this:
const weakMap = new WeakMap()
Object.prototype = {
get __proto__() {
return this['[[prototype]]'] === null ? weakMap.get(this) : this['[[prototype]]']
},
set __proto__(newPrototype) {
if (!Object.isExtensible(newPrototype)) {
throw new TypeError(`${newPrototype} is not extensible`)
}
const isObject = typeof newPrototype === 'object' || typeof newPrototype === 'function'
if (newPrototype === null || isObject) {
// If __proto__ was previously set to null,
// assigning to __proto__ again to modify the prototype is futile
/// it behaves as if obj.__proto__ = { a: 1 } were an ordinary property like obj.xxx = { a: 1 }
if (this['[[prototype]]'] === null) {
weakMap.set(this, newPrototype)
} else {
this['[[prototype]]'] = newPrototype
}
}
},
// ... other properties such as toString, hasOwnProperty, etc.
}
instanceof
Implementation Principle
Basic form: leftValue instanceof rightValue
It traces the prototype chain to determine whether the right value exists on it. An alternative implementation:
function myInstanceof(left, right) {
// First use typeof to check for primitive types; if so, return false directly
if (typeof left !== 'object' || left === null) {
return false
}
// getPrototypeOf is a built-in API of the Object object that gets the prototype of the argument
let proto = Object.getPrototypeOf(left)
while (true) {
if (proto === null) {
return false
}
if (proto === right.prototype) {
// Found the same prototype object, return true
return true
}
proto = Object.getPrototypeOf(proto)
}
}
Overriding the Built-in instanceof Behavior
class MyArray {
static [Symbol.hasInstance](instance) {
return Array.isArray(instance)
}
}
console.log([] instanceof MyArray) // true
Differences from isPrototypeOf
isPrototypeOf is also used to determine whether an object exists on another object's prototype chain.
// Check whether f is an instance of the Foo class,
// and whether it is an instance of its parent type
function Aoo() {}
function Foo() {}
// JavaScript prototypal inheritance
Foo.prototype = new Aoo()
const foo = new Foo()
console.log(Foo.prototype.isPrototypeOf(foo)) // true
console.log(foo instanceof Foo) // true
console.log(Aoo.prototype.isPrototypeOf(foo)) // true
console.log(foo instanceof Aoo) // true
The differences between the two are:
instanceof: determines whether an object is an instance of a constructor, i.e. checks whether the constructor'sprototypeproperty appears on the object's prototype chainisPrototypeOf: can only be called on a prototype object; it determines whether an object (a prototype object) exists on another object's prototype chain — directly verifying the prototype relationship rather than the constructor
In terms of form:
childObject instanceof ClassClass.prototype.isPrototypeOf(childObject)
From the scope perspective:
instanceofdoes not work acrosswindow,iframe, orglobal; objectstransferred from elsewhere are treated as different objectsisPrototypeOfis not limited by origin; it works acrossiframes
From the literal perspective:
instanceofdoes not work on literals and primitive types, e.g.123 instanceof Number // falseisPrototypeOfdoes work on literals and primitive types
Differences from typeof
typeofcannot distinguish between different class objects — it can only distinguish primitive types;instanceofcan clearly distinguish instances of different classes- When
typeofchecks an undefined variable, it does not throw as long as there is no temporal dead zone;instanceofrequires the variable to be defined, otherwise it throws
Inheritance
Implementation Principle of new
function myNew(Func, ...args) {
// 1. Create a new object
const obj = {}
// 2. Point the new object's prototype at the constructor's prototype object
obj.__proto__ = Func.prototype
// 3. Point the constructor's this at the new object
const result = Func.apply(obj, args)
// 4. Decide based on the return value
return result instanceof Object ? result : obj
}
Differences Between new class and new function
- Although
classis syntactic sugar overfunction,functiondeclarations are hoisted, whileclassdeclarations are not
const a = new A() // Uncaught ReferenceError: A is not defined
class A {
b() {}
}
super
In JS, only methods inside an object can access super; even properties inside an object cannot — doing so throws an error
// case 1
const obj1 = {
foo() {
console.log(super.foo()) // Defined as a method, so super can be accessed normally
}
}
Object.setPrototypeOf(obj1, {
foo() {
return 'bar'
}
})
obj1.foo()
// case 2
const obj2 = {
foo: function() {
console.log(super.foo()) // Here foo is defined as a property whose value is an ordinary function, not a method, so `super` cannot be accessed and an exception is thrown
}
}
Object.setPrototypeOf(obj2, {
foo() {
return 'bar'
}
})
obj2.foo()
The accessibility check for super happens at compile time, so even though obj1.foo() above would print normally, the code already reports an error at compile time — the whole snippet actually prints nothing and throws directly.
override
getter / setter
When inheriting, getters and setters must be overridden together; overriding only one of them makes the property unreadable/unwritable
let val = 0
class A {
set foo(_val) {
val = _val
}
get foo() {
return val
}
}
class B extends A { }
class C extends A {
get foo() {
return val
}
}
const b = new B()
console.log(b.foo) // 0
b.foo = 1
console.log(b.foo) // 1
const c = new C()
console.log(c.foo) // 1
c.foo = 2 // No setter, so the global val cannot be updated
console.log(c.foo) // 1 — the earlier set failed, so it is still 1
console.log(b.foo) // 1
class A {
val = 1
get foo() {
return this.val
}
}
class B {
val = 2
set foo(val) {
this.val = val
}
}
const a = new A()
const b = new B()
console.log(a.foo) // 1
console.log(b.foo) // undefined — no getter, so only undefined can be read
b.foo = 3 // The setter is still there, so b.val gets updated
console.log(b.val) // 3
console.log(b.foo) // undefined
The Many Ways of JavaScript Inheritance
Prototype Chain Inheritance
function Parent() {
this.name = 'Naparte'
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child() {}
// Attached to the prototype; shared by multiple instances
Child.prototype = new Parent()
// Usage
const child1 = new Child()
console.log(child1.getName()) // Naparte
Borrowing Constructors (Classical Inheritance)
function Parent() {
this.names = ['Naparte', 'SXF']
}
function Child() {
// Call the parent constructor here
Parent.call(this)
}
// Usage
const child1 = new Child()
child1.names.push('VFrank')
console.log(child1.names) // ["Naparte", "SXF", "VFrank"]
const child2 = new Child()
console.log(child2.names) // ["Naparte", "SXF"]
Combination Inheritance
function Parent(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child(name, age) {
// Parent runs for the second time; this step eliminates the problems caused by a shared prototype
Parent.call(this, name)
this.age = age
}
// Parent runs for the first time
Child.prototype = new Parent()
Child.prototype.constructor = Child
// Usage
const child1 = new Child('Naparte', '18')
child1.colors.push('black')
console.log(child1.name) // Naparte
console.log(child1.age) // 18
console.log(child1.colors) // ["red", "blue", "green", "black"]
const child2 = new Child('SXF', '20')
console.log(child2.name) // SXF
console.log(child2.age) // 20
console.log(child2.colors) // ["red", "blue", "green"]
Prototypal Inheritance
function createObj(o) {
function F() {}
F.prototype = o
return new F()
}
// Usage
// Properties with reference-type values are always shared
const person = {
name: 'kevin',
friends: ['daisy', 'kelly'],
}
const person1 = createObj(person)
const person2 = createObj(person)
person1.name = 'person1'
console.log(person2.name) // kevin
person1.friends.push('taylor')
console.log(person2.friends) // ["daisy", "kelly", "taylor"]
Parasitic Inheritance
function createObj(o) {
const clone = Object.create(o)
clone.sayName = function () {
console.log('hi')
}
return clone
}
Parasitic Combination Inheritance
function object(o) {
// The key three steps
function F() {}
F.prototype = o
return new F()
}
function prototype(child, parent) {
const prototype = object(parent.prototype)
prototype.constructor = child
child.prototype = prototype
}
// When we use it:
prototype(Child, Parent)
Summary
| Inheritance method | Advantages | Disadvantages |
|---|---|---|
| Prototype chain inheritance | - | Reference-type properties are shared by all instances |
| Borrowing constructors | 1. Avoids reference-type properties being shared by all instances 2. Can pass arguments to Parent from Child |
Methods are defined in the constructor, so they are recreated for every instance |
| Combination inheritance | Combines the advantages of prototype chain inheritance and constructor borrowing | - |
| Prototypal inheritance | - | Properties with reference-type values are always shared |
| Parasitic inheritance | - | Methods are recreated every time an object is created |
| Parasitic combination inheritance | 1. Efficient — it only calls the Parent constructor once 2. The prototype chain stays intact 3. instanceof and isPrototypeOf still work normally |
- |