Skip to content
Back to blog

Prototypes and Inheritance

Notes on prototypes and inheritance in JS

Updated

Prototypes

Introduction

  • An instance's constructor is the constructor function, and a constructor's constructor is Function — including Function itself (Function.__proto__ === Function.prototype)
  • prototype: points to the shared instance of itself given to subclasses. Only things inheriting from Function have a prototype; it points to the shared object created by new when the current function is used as a constructor. For example, Person.prototype is the shared prototype of new Person() instances, so for any class, .prototype.constructor is the class itself
  • __proto__: points to the shared instance the current object uses, i.e. the parent's shared instance. Every object (including Function) has __proto__, pointing to its constructor's prototype. For example, new Person()'s __proto__ points to Person.prototype
  • Almost all objects inherit from Object.prototype, but you can also use Object.create(null) to point the prototype chain at null (which loses built-in methods like toString)

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.prototype is a function, it has no prototype, 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__ and prototype point to are shared instances, not the constructor/class itself; only the shared instance's constructor property 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's prototype property appears on the object's prototype chain
  • isPrototypeOf: 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 Class
  • Class.prototype.isPrototypeOf(childObject)

From the scope perspective:

  • instanceof does not work across window, iframe, or global; objects transferred from elsewhere are treated as different objects
  • isPrototypeOf is not limited by origin; it works across iframes

From the literal perspective:

  • instanceof does not work on literals and primitive types, e.g. 123 instanceof Number // false
  • isPrototypeOf does work on literals and primitive types

Differences from typeof

  • typeof cannot distinguish between different class objects — it can only distinguish primitive types; instanceof can clearly distinguish instances of different classes
  • When typeof checks an undefined variable, it does not throw as long as there is no temporal dead zone; instanceof requires 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 class is syntactic sugar over function, function declarations are hoisted, while class declarations 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
-