- Published on
JavaScript Prototypal Inheritance: The Prototype Chain, `new`, and Class Syntax
- Authors

- Name
- Duncan Leung
- @leungd
JavaScript's class keyword looks like classical inheritance from Java or C#, but underneath it's the same prototype-based machinery that has always existed in the language. ES6 classes are syntactic sugar - useful sugar, but if you don't understand what's underneath, you'll be confused the first time you read someone else's pre-ES6 code, or run Object.getPrototypeOf in a debugger.
This post walks through how method sharing actually works in JavaScript: the prototype chain, the new operator, and how the modern class syntax desugars to the same function-constructor pattern that came before it.
The Mental Model: Property Lookup Walks Up a Chain
Prototypal inheritance is the mechanism JavaScript's OOP construct uses to share methods among instances.
When you access a property on an object, JavaScript walks a chain looking for it:
- Look on the object itself
- If not found, look on the object's
[[Prototype]] - If not found, look on that object's
[[Prototype]] - Continue until you hit the end of the chain -
Object.prototype, thennull
That chain is built when the object is created. The most direct way to construct it is Object.create(prototypeObject), which returns a new object whose [[Prototype]] points to prototypeObject.
Everything else - function constructors, new, class - is a more convenient way of wiring up this same chain.
Two Ways to Write the Same Thing
Let's start with two equivalent implementations of an Animal type. The first uses the pre-ES6 pattern - a function constructor with methods attached to its prototype object.
Pseudoclassical OOP (Function Constructors)
function Animal(name) {
this.speed = 0
this.name = name
}
Animal.prototype = {
// Recreate the constructor property manually
constructor: Animal,
run(speed) {
this.speed = speed
console.log(`${this.name} runs with speed ${this.speed}`)
},
stop() {
this.speed = 0
console.log(`${this.name} is standing still`)
},
}
const dog = new Animal('Ned')
const cat = new Animal('Fifi')
dog.run(10) // Ned runs with speed 10
dog.stop() // Ned is standing still
Classical OOP (Class Syntax)
The ES6 class syntax is sugar over the function-constructor pattern above:
class Animal {
constructor(name) {
this.speed = 0
this.name = name
}
run(speed) {
this.speed = speed
console.log(`${this.name} runs with speed ${this.speed}`)
}
stop() {
this.speed = 0
console.log(`${this.name} is standing still`)
}
}
const dog = new Animal('Ned')
const cat = new Animal('Fifi')
dog.run(10) // Ned runs with speed 10
dog.stop() // Ned is standing still
Both versions produce the same runtime structure. Each dog and cat instance holds its own name and speed, but run and stop live on Animal.prototype - shared across all instances and looked up via the prototype chain.
prototype vs __proto__ vs [[Prototype]]
These three names get conflated constantly. They refer to related but distinct things:
prototypeis a property of a function (specifically, a constructor). It is the object that will become the[[Prototype]]of any instances created withnew.[[Prototype]]is an internal property on every object instance. It is what the engine actually walks during property lookup.__proto__is the legacy, non-standard way to read or write[[Prototype]]directly. It still works in browsers but is considered deprecated.
In modern code, prefer the standard APIs:
Object.create(proto[, descriptors])- creates an empty object withprotoas its[[Prototype]]Object.getPrototypeOf(obj)- returns the[[Prototype]]ofobjObject.setPrototypeOf(obj, proto)- sets the[[Prototype]]ofobjtoproto
The short version: prototype is on the constructor function. [[Prototype]] is on instances. new is what connects the two.
What the new Operator Actually Does
When you write new Animal("Ned"), four things happen behind the scenes:
- A new empty object is created
- That object's
[[Prototype]]is set toAnimal.prototype - The constructor function runs with
thisbound to the new object - The new object is returned (unless the constructor explicitly returns a different object)
It's roughly equivalent to writing this by hand:
// When an instance `dog` is created from `new Animal()`:
const dog = new Animal('Ned')
function Animal(name) {
// const this = Object.create(Animal.prototype)
// 1. Create a new object `this`
// 2. Set `this.__proto__` to Animal.prototype
this.speed = 0
this.name = name
// return this
}
// "Assign `dog`'s [[Prototype]] to this object":
Animal.prototype = {
constructor: Animal,
run(speed) {
// ...
},
stop() {
// ...
},
}
This is why all instances of Animal share the same run and stop functions - they all have the same [[Prototype]] pointer, and that one object holds the methods.
The Prototype Chain: Visualizing Inheritance
It's worth seeing the chain laid out. Here's what the structure looks like for our Animal constructor and two instances:
null
▲
|
|
Object.prototype
(Object)
┌─────────────────────────┐
| constructor: Object |
| toString: function |
| ... |
└─────────────────────────┘
|
┌─────────────────┴──────────────────┐
| |
Function.prototype |
(Object) |
┌──────────────────┐ |
| call: function | |
| apply: function | |
| ... | |
└──────────────────┘ |
▲ |
| [[Prototype]] | [[Prototype]]
| |
Animal(name) Animal.prototype
(Function Constructor) (Object)
┌──────────────────┐ ┌───────────────────────┐
│ this.name = name;│ prototype │ constructor: Animal │
│ | ------------► | run: function |
└──────────────────┘ | stop: function |
└───────────────────────┘
▲
|
| [[Prototype]]
|
┌────────────────────────────────────┴──┐
const dog = new Animal("Ned") const cat = new Animal("Fifi")
dog cat
(Object) (Object)
┌───────────────┐ ┌───────────────┐
| name: "Ned" | | name: "Fifi" |
| | | |
└───────────────┘ └───────────────┘
When you call dog.run(10):
- JavaScript looks for
runondog- not there - Walks up to
dog.[[Prototype]](which isAnimal.prototype) - finds it - Calls it with
thisbound todog
That's the entire mechanism.
Subclass Inheritance: Extending the Chain
To extend a type, you insert another link in the chain. A Dog subtype's instances should look up methods on Dog.prototype first, then fall back to Animal.prototype:
Animal(name) Animal.prototype
(Constructor) (Object)
┌──────────────────┐ ┌───────────────────────┐
│ this.name = name;│ prototype │ constructor: Animal │
│ | ------------► | run: function |
└──────────────────┘ | stop: function |
└───────────────────────┘
▲ ▲
| [[Prototype]] | [[Prototype]]
| (extends Animal) | (extends Animal)
| |
Dog(name) Dog.prototype
(Constructor) (Object)
┌───────────────────┐ ┌───────────────────────┐
│ callDogs: function│ prototype │ constructor: Dog │
│ | -----------► | bark: function |
└───────────────────┘ | slowDown: function |
└───────────────────────┘
▲
| [[Prototype]]
|
const dog = new Dog("Ned")
dog
(Object)
┌───────────────┐
| name: "Ned" |
| |
└───────────────┘
Notice there are two chains being set up:
- The instance chain:
dog→Dog.prototype→Animal.prototype→Object.prototype→null. This is what method lookup walks. - The constructor chain:
Dog→Animal→Function.prototype→Object.prototype→null. This is what makes static method inheritance work.
Extending Functionality with super
When you subclass with class ... extends, super does two jobs:
- Inside the subclass
constructor,super(...)calls the parent constructor. A derived constructor must callsuperbefore usingthis- otherwise the object forthiswon't be created. - Inside any subclass method,
super.method(...)calls the parent's version of that method.
class Animal {
constructor(name) {
this.speed = 0
this.name = name
}
static type = 'Mammal'
run(speed) {
this.speed = speed
console.log(`${this.name} runs with speed ${this.speed}`)
}
stop() {
this.speed = 0
console.log(`${this.name} is standing still`)
}
}
class Dog extends Animal {
constructor(name) {
super(name)
}
static callDogs(...args) {
if (args.length > 0) {
args.forEach((dog) => {
console.log(`Come home, ${dog.name}... `)
})
}
}
bark() {
super.stop()
console.log(`${this.name} says "woof woof"`)
}
slowDown() {
setTimeout(() => super.stop(), 1000)
}
}
let dog = new Dog('Ned')
dog.bark() // Ned is standing still. Ned says "woof woof"
// The inherited methods are still available
dog.run(10) // Ned runs with speed 10
dog.stop() // Ned is standing still
The Same Thing, Written by Hand
Here's what class ... extends actually compiles down to under the prototype-based model. This is what you'd have to write to set up the same chains manually:
function Animal(name) {
this.speed = 0
this.name = name
}
Animal.prototype = {
// Recreate the constructor property manually
constructor: Animal,
run(speed) {
this.speed = speed
console.log(`${this.name} runs with speed ${this.speed}`)
},
stop() {
this.speed = 0
console.log(`${this.name} is standing still`)
},
}
function Dog(name) {
// The equivalent of calling `super(name)` -
// run the parent constructor with `this` bound to the new instance
Animal.call(this, name)
}
// Inherit static properties (constructor chain)
Dog.__proto__ = Animal
// Inherit instance methods (prototype chain)
Dog.prototype = Object.create(Animal.prototype)
Dog.prototype.constructor = Dog
Dog.prototype.bark = function () {
// The equivalent of `super.stop()`
Animal.prototype.stop.call(this)
console.log(`${this.name} says "woof woof"`)
}
Dog.prototype.slowDown = function () {
setTimeout(() => Animal.prototype.stop.call(this), 1000)
}
Dog.callDogs = function (...args) {
if (args.length > 0) {
args.forEach((dog) => {
console.log(`Come home, ${dog.name}... `)
})
}
}
let dog = new Dog('Ned')
dog.bark() // Ned is standing still. Ned says "woof woof"
// The inherited methods are still available
dog.run(10) // Ned runs with speed 10
dog.stop() // Ned is standing still
Dog.callDogs(dog)
A few things worth pointing out in the desugared version:
Animal.call(this, name)runs the parent constructor against the new instance - this is whatsuper(name)does in a class.Dog.__proto__ = Animalsets up static method inheritance. This is whyDog.callDogsworks as a static method, and whyDogcan see static properties defined onAnimal.Dog.prototype = Object.create(Animal.prototype)sets up instance method inheritance - the chain that property lookup walks for anyDoginstance.Animal.prototype.stop.call(this)issuper.stop()- reach up to the parent's method and call it with the current instance'sthis.
Takeaways
- JavaScript's "inheritance" is property lookup walking a chain. Every object has a
[[Prototype]]; lookup follows it until the value is found or the chain ends atnull. prototype(on constructors) becomes the[[Prototype]](on instances) of any object created withnew.newdoes four things: creates an object, links its[[Prototype]]to the constructor'sprototype, runs the constructor withthisbound, and returns the new object.classis sugar. Subclassing wires up two chains - the instance chain (Dog.prototype→Animal.prototype) for methods, and the constructor chain (Dog.__proto__→Animal) for static methods.superis the explicit reach-up to the parent's constructor or method. In desugared form, it'sAnimal.call(this, ...)for constructors andAnimal.prototype.method.call(this, ...)for methods.