The Prototype Chain â How `new` Creates Objects and Enables Method Sharing
Last time when discussing this, I mentioned that ordinary methods live on the prototype and are shared by all instances. Arrow functions are instance properties, each object gets its own copy. I didnât expand on it thenâthis article dives deeper.
Without Prototypes: What Happens When You new a Thousand Objects?
Letâs start with the most basic problem: creating objects with constructors.
function Dog(name: string) {
this.name = name;
this.bark = function () {
console.log(`${this.name}: æ±ȘïŒ`);
};
}
const d1 = new Dog("性é»");
const d2 = new Dog("ć°é»");
Does this work? Yes. Is it good? No!
console.log(d1.bark === d2.bark); // false
Every time you call new, a brand-new copy of the bark function is created. d1 and d2 each hold an identical function definition, but they are two independent objects in memory.
It makes sense for name to differ per instanceâeach gets its own copy. But bark behaves exactly the same every time. Why store a thousand copies?
The solution: extract shared methods into a common place where all instances can look them up.
The Perfect Storage Location
Since all instances are born from a single class, and a class is essentially a function, why not store all shared methods directly on a property called prototype of that function?
function Dog(name: string) {
this.name = name; // per-instance data
}
// Shared methods go here:
Dog.prototype.bark = function () {
console.log(`${this.name}: æ±ȘïŒ`);
};
const d1 = new Dog("性é»");
const d2 = new Dog("ć°é»");
d1.bark(); // "性é»: æ±ȘïŒ"
d2.bark(); // "ć°é»: æ±ȘïŒ"
console.log(d1.bark === d2.bark); // true â same function!
From now on, each instance only needs to remember which class created it, using a property called __proto__ to point to its shared repository.
d1 itself doesnât have bark, yet d1.bark() works. This is the JS engine doing a lookup behind the scenes:
1. Does d1 have its own bark? â No
2. Look on d1.__proto__ (i.e., Dog.prototype) â Found!
3. Execute itâsince d1 called it, d1 naturally becomes this
So the core value of prototypes: sharing methods to save memory.
What Does new Actually Do?
// new Dog("性é»") is equivalent to:
function myNew<T>(constructor: new (...args: any[]) => T, ...args: any[]): T {
// 1. Create an empty object with __proto__ pointing to constructor.prototype
const obj = Object.create(constructor.prototype);
// 2. Bind constructor's this to the empty object, execute it with args
const result = constructor.apply(obj, args);
// 3. Return the object
return obj;
}
const d = myNew(Dog, "性é»");
d.bark(); // "性é»: æ±ȘïŒ"
Note that Object.create(Dog.prototype) creates an empty object whose __proto__ is set to Dog.prototype. Thatâs why it can access methods on the prototype.
The Prototype Chain
Prototypes donât just share methodsâone prototype can point to another. When they chain together, thatâs the prototype chain.
First, a prerequisite: when a function is created, its prototype objectâbesides methods you manually addâcomes with a default constructor property that points back to the function itself:
function Animal(type: string) {
this.type = type;
}
Animal.prototype.breathe = function () {
console.log(`${this.type} ćšćŒćž`);
};
console.log(Animal.prototype.constructor === Animal); // true â present by default
You rarely use constructor in practice, but itâs the prototype objectâs âID cardââit declares which constructor this prototype repository belongs to.
Now letâs manually implement class inheritanceâmaking Dog inherit from Animalâin four steps.
Step 1: Inherit Properties
type is per-instance data and cannot be shared. You must manually call Animal inside Dogâs constructor to assign it:
function Dog(name: string) {
Animal.call(this, "ç"); // Manually execute Animal constructor, writing this.type = "ç" to the current instance
this.name = name; // Dog's own property
}
Without this line â d.type is undefined. The prototype chain only handles method lookupâit doesnât handle properties.
Step 2: Bridge with Object.create
To let Dog instances call breathe(), we need to connect Dogâs prototype repository to Animalâs prototype repository.
Object.create(x) does one thing: creates an empty object {} and sets its __proto__ to x.
Dog.prototype = Object.create(Animal.prototype);
At this point, Dog.prototype looks like this:
Dog.prototype = {
// empty object, nothing inside
// __proto__ â Animal.prototype = { breathe, constructor: Animal }
}
Why not
Dog.prototype = Animal.prototype? That would make two constructors share the same prototype objectâadding a method to Dog.prototype would add it to Animal.prototype too. A dogâs bark would become every animalâs bark. You must useObject.createto create an intermediary object.
Step 3: Fix constructor
Dog.prototype is now an empty object with no constructor property of its own. Following __proto__ upward, we find Animal.prototype.constructor, which is Animal:
console.log(Dog.prototype.constructor); // Animal â wrong, should be Dog
Dogâs prototype repository has Animal on its ID card. Letâs fix it directly on the empty object:
Dog.prototype.constructor = Dog;
Nobody uses this property day-to-day, but it ensures the fact of âwhich constructor this prototype belongs toâ is correct.
Step 4: Add Dogâs Own Methods
The bridge is built and constructor is fixed. Now add Dogâs unique methods to its prototype:
Dog.prototype.bark = function () {
console.log(`${this.name}: æ±ȘïŒ`);
};
Full code + verification:
function Animal(type: string) {
this.type = type;
}
Animal.prototype.breathe = function () {
console.log(`${this.type} ćšćŒćž`);
};
function Dog(name: string) {
Animal.call(this, "ç"); // â inherit properties
this.name = name;
}
Dog.prototype = Object.create(Animal.prototype); // ⥠bridge
Dog.prototype.constructor = Dog; // âą fix constructor
Dog.prototype.bark = function () {
// ⣠add Dog's own methods
console.log(`${this.name}: æ±ȘïŒ`);
};
const d = new Dog("性é»");
d.bark(); // "性é»: æ±ȘïŒ" â found on Dog.prototype
d.breathe(); // "ç ćšćŒćž" â up via __proto__ to Animal.prototype
d.toString(); // "[object Object]" â further up to Object.prototype
The lookup chain:
d = { name: "性é»", type: "ç" }
â __proto__: Dog.prototype = { bark, constructor: Dog }
â __proto__: Animal.prototype = { breathe, constructor: Animal }
â __proto__: Object.prototype = { toString, hasOwnProperty, ... }
â __proto__: null
The rule is simple: when looking up a property, traverse __proto__ layer by layer until you hit null. If not found, return undefined.
class Is Just Syntactic Sugar
The previous section manually implemented Dog extends Animal in four steps. ES6 class essentially automates this entire process:
class Animal {
type: string;
constructor(type: string) {
this.type = type;
}
breathe() {
console.log(`${this.type} ćšćŒćž`);
}
}
class Dog extends Animal {
name: string;
constructor(name: string) {
super("ç"); // â Inherit properties â equivalent to Animal.call(this, "ç")
this.name = name;
}
bark() {
// ⥠⹠⣠extends handles this automatically
console.log(`${this.name}: æ±ȘïŒ`);
}
}
A single line of extends does three things for you:
- Bridge:
Dog.prototype = Object.create(Animal.prototype)â Step 2 - Fix the ID:
Dog.prototype.constructor = Dogâ Step 3 - Add methods:
bark()in the class body is automatically attached toDog.prototypeâ Step 4
Letâs verifyâthe class version is identical to the manual prototype version:
const d = new Dog("性é»");
// super("ç") is equivalent to Animal.call(this, "ç")
console.log(d.type); // "ç"
// bark is on Dog.prototype, same as the manual approach
console.log(d.hasOwnProperty("bark")); // false
console.log(d.__proto__.hasOwnProperty("bark")); // true
// The prototype chain structure is unchanged â identical to the previous section's lookup chain
console.log(d.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(Dog.prototype.constructor === Dog); // true â constructor is also auto-fixed
d.bark(); // "性é»: æ±ȘïŒ"
d.breathe(); // "ç ćšćŒćž"
class didnât invent a new mechanismâit just wrapped the âfour-step inheritance methodâ into extends + super. The underlying prototype chain lookup logic hasnât changed one bit.
With this mapping in mind, letâs examine a common pitfallâwhy arrow functions as class fields are not shared:
class Dog {
name: string = "";
// Ordinary method â Dog.prototype.bark
bark() {
console.log(this.name);
}
// Arrow property â each instance gets its own copy, equivalent to writing this.run = () => {} in constructor
run = () => {
console.log(this.name);
};
}
const d1 = new Dog();
const d2 = new Dog();
console.log(d1.bark === d2.bark); // true â shared on prototype
console.log(d1.run === d2.run); // false â instance property, each has its own
run = () => {} as a class field is essentially equivalent to:
class Dog {
constructor() {
this.run = () => {
console.log(this.name);
};
}
}
Anything written in constructor is attached to the instanceâit will not be shared on the prototype.
Three Common Interview Questions, Answered Instantly with the Prototype Chain
How Does instanceof Work?
d instanceof Dog; // true
d instanceof Animal; // true
d instanceof Object; // true
What it does: traverse up d.__proto__ and check if it can find Dog.prototype (or Animal.prototype, Object.prototype). If found, return true.
What Is hasOwnProperty For?
const d = new Dog("性é»");
console.log(d.hasOwnProperty("name")); // true â name is its own
console.log(d.hasOwnProperty("bark")); // false â bark is on the prototype
Itâs commonly used to filter out prototype methods when iterating over an object:
for (const key in d) {
if (d.hasOwnProperty(key)) {
console.log(`own property: ${key}`);
}
}
// Output: "own property: name"
What Does Object.create(null) Mean?
const pureObj = Object.create(null);
pureObj.name = "pure";
console.log(pureObj.toString); // undefined
console.log(pureObj.__proto__); // undefined
Creates a pure dictionary object without a prototypeânot even toString or hasOwnProperty. Ideal as a plain key-value map without worrying about prototype property pollution.
Prototype Chain Diagram
In summary:
- Prototypes exist to share methods and save memory. Methods live on
prototype; instances find them via__proto__. prototypeis the âtemplate repositoryâ created by a function. Each repository also has its own__proto__pointing to the upper repositoryâforming the âroadmapâ for property lookup.- class syntax didnât invent anything newâitâs just syntactic sugar over the prototype mechanism. Once you understand the prototype chain, everything class does is traceable and predictable.