𧬠Object Prototypes and Inheritance
Every JavaScript object has a hidden link to another object it borrows from. Follow that link far enough and you understand inheritance, why toString() works on things you never defined it on, and what ES6 classes are really doing under their friendly syntax.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Define a prototype and trace the prototype chain a property lookup follows
- Explain how constructor functions link instances to
Constructor.prototype - Build inheritance with constructor functions,
Object.create(), and ES6class β¦ extends - Describe property shadowing and use it to override methods
- Apply the rules for using prototypes safely (and avoid modifying built-ins)
Estimated Time: 45β55 minutes β’ Difficulty: IntermediateβAdvanced
Hands-on: Build a shape hierarchy that computes area through inheritance.
In This Lesson
What Is a Prototype?
Every JavaScript object carries a hidden link to another object called its prototype. When you read a property that the object does not have, JavaScript automatically looks for it on the prototype β and if it isn't there, on the prototype's prototype β walking upward until it either finds the property or hits the end of the chain.
π‘ A useful analogy: Prototypes are like genetic inheritance. You inherit traits from your parents, who inherited from theirs. When JavaScript can't find a trait on an object, it "asks its ancestors" one at a time until someone has it β or until the family tree runs out at null.
π Key Terms
Prototype: the object another object delegates to for missing properties.
[[Prototype]]: the internal link, read via Object.getPrototypeOf(obj) (the old __proto__ is deprecated).
Prototype chain: the series of prototype links ending at Object.prototype, then null.
The Prototype Chain
Picture three linked objects: your instance, the prototype that gives it shared methods, and Object.prototype at the top with universal methods like toString() and hasOwnProperty().
Object.prototype, then stops at null.const person = {
firstName: 'John',
lastName: 'Doe',
getFullName() {
return `${this.firstName} ${this.lastName}`;
},
};
// employee delegates to person for anything it lacks
const employee = { jobTitle: 'Developer', employeeId: 'EMP123' };
Object.setPrototypeOf(employee, person);
console.log(employee.getFullName()); // 'John Doe' β found on person
console.log(employee.jobTitle); // 'Developer' β its own
console.log(employee.hasOwnProperty('jobTitle')); // true (own)
console.log(employee.hasOwnProperty('firstName')); // false (inherited)
How Property Lookup Works
When you access obj.property, the engine follows a fixed procedure:
console.log(employee.toString()); // '[object Object]'
// 1. Not on employee
// 2. Not on person (its prototype)
// 3. Found on Object.prototype β called with `this` = employee
This is why toString(), hasOwnProperty(), and friends work on objects you never defined them on: they live on Object.prototype at the top of nearly every chain.
Constructors & Prototypes
When you call a constructor function with new, JavaScript links the new instance to that function's prototype property. Any method you put on Constructor.prototype is instantly shared by every instance β defined once, used by all.
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// One shared method on the prototype
Person.prototype.getFullName = function () {
return `${this.firstName} ${this.lastName}`;
};
const p1 = new Person('John', 'Doe');
const p2 = new Person('Jane', 'Smith');
console.log(p1.getFullName()); // 'John Doe'
console.log(p2.getFullName()); // 'Jane Smith'
// Both instances share the SAME function object
console.log(p1.getFullName === p2.getFullName); // true
β Why this matters: memory
If you defined getFullName inside the constructor with this.getFullName = function () {β¦}, every instance would carry its own copy of that function. Put it on the prototype and a thousand instances share one function object. Same behavior, a fraction of the memory.
// Method in the constructor β a copy per instance (wasteful)
function Car1(make) {
this.make = make;
this.getInfo = function () { return this.make; };
}
// Method on the prototype β one shared copy (efficient)
function Car2(make) { this.make = make; }
Car2.prototype.getInfo = function () { return this.make; };
console.log(new Car1('Toyota').getInfo === new Car1('Honda').getInfo); // false
console.log(new Car2('Toyota').getInfo === new Car2('Honda').getInfo); // true
Inheritance Patterns
Constructor functions (the classic pattern)
Before ES6 classes, you chained prototypes by hand: call the parent constructor with Parent.call(this, β¦), then point the child's prototype at a new object built from the parent's prototype.
function Animal(name) {
this.name = name;
this.isAlive = true;
}
Animal.prototype.eat = function (food) {
console.log(`${this.name} is eating ${food}`);
};
function Dog(name, breed) {
Animal.call(this, name); // run the parent constructor
this.breed = breed;
}
// Chain Dog β Animal β Object
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // repair the constructor pointer
Dog.prototype.bark = function () {
console.log(`${this.name} says woof!`);
};
const rex = new Dog('Rex', 'German Shepherd');
rex.eat('kibble'); // 'Rex is eating kibble' β inherited
rex.bark(); // 'Rex says woof!' β own
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
Object.create() (behavior delegation)
You can also skip constructors entirely and link objects directly. This style emphasizes delegating behavior rather than modeling types.
const vehicleActions = {
init(make, model) {
this.make = make;
this.model = model;
return this;
},
getInfo() { return `${this.make} ${this.model}`; },
start() { console.log('Engine started'); },
};
const car = Object.create(vehicleActions).init('Toyota', 'Camry');
console.log(car.getInfo()); // 'Toyota Camry'
car.start(); // 'Engine started'
ES6 Classes on Top
The class syntax is syntactic sugar over exactly the prototype machinery above. extends sets up the chain and super calls the parent constructor β but underneath, it is still prototypes all the way down.
class Animal {
constructor(name) {
this.name = name;
this.isAlive = true;
}
eat(food) { console.log(`${this.name} is eating ${food}`); }
sleep() { console.log(`${this.name} is sleeping`); }
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // call the parent constructor
this.breed = breed;
}
bark() { console.log(`${this.name} says woof!`); }
}
const buddy = new Dog('Buddy', 'Golden Retriever');
buddy.eat('treats'); // 'Buddy is eating treats' β inherited
buddy.bark(); // 'Buddy says woof!'
// Still prototype-based behind the scenes:
console.log(Object.getPrototypeOf(buddy) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
π‘ Same engine, friendlier dashboard
Compare this to the hand-rolled constructor version β no Object.create, no constructor repair, no Parent.call(this). Classes handle all of it. Use classes in new code; understand prototypes so the "magic" is never a mystery.
Property Shadowing
When an object has its own property with the same name as one up the chain, the object's own version wins β it shadows the inherited one. This is exactly how method overriding works.
class Vehicle {
constructor() { this.speed = 0; }
accelerate() {
this.speed += 10;
console.log(`Speed: ${this.speed}`);
}
}
class SportsCar extends Vehicle {
// Shadows Vehicle.prototype.accelerate
accelerate() {
this.speed += 20; // sports cars gain speed faster
console.log(`ZOOM! Speed: ${this.speed}`);
}
}
new Vehicle().accelerate(); // 'Speed: 10'
new SportsCar().accelerate(); // 'ZOOM! Speed: 20'
The subclass can still reach the parent's version when needed β with super.accelerate() inside a class, or Vehicle.prototype.accelerate.call(this) in the older style.
Using Prototypes Safely
β οΈ Never modify built-in prototypes
Adding methods to Array.prototype, Object.prototype, and the like is a classic mistake. Your addition becomes visible everywhere and can collide with other libraries or with future language features β and it leaks into forβ¦in loops.
// BAD β pollutes every array in the program
Array.prototype.first = function () { return this[0]; };
for (const key in [1, 2, 3]) {
console.log(key); // '0', '1', '2', 'first' β surprise!
}
// GOOD β a plain utility function
const first = (arr) => arr[0];
// GOOD β subclass only your own type
class MyArray extends Array {
first() { return this[0]; }
}
β Prototype rules of thumb
- Do use
Object.getPrototypeOf(obj), not the deprecated__proto__. - Do repair
Constructor.prototype.constructorif you hand-roll inheritance. - Do prefer ES6 classes for readable, correct inheritance.
- Don't extend built-in prototypes β write utilities or subclasses instead.
Hands-on Exercise
ποΈ Build a Shape Hierarchy
Objective: Use inheritance so every shape computes its own area through a shared interface.
Instructions:
- Create a base
Shapeclass with anameand anarea()method that throws "not implemented". - Create
Circle(radius) andRectangle(width, height) thatextend Shapeand overridearea(). - Give
Shapeadescribe()method that logs the name and area β inherited by both subclasses. - Put a circle and a rectangle in an array and loop, calling
describe()on each.
π‘ Hint
describe() lives only on Shape but calls this.area() β thanks to shadowing, each subclass's own area() runs. Circle area is Math.PI * r ** 2; rectangle area is width * height.
β Sample solution
class Shape {
constructor(name) { this.name = name; }
area() { throw new Error('area() must be implemented by a subclass'); }
describe() {
console.log(`${this.name} has an area of ${this.area().toFixed(2)}`);
}
}
class Circle extends Shape {
constructor(radius) { super('Circle'); this.radius = radius; }
area() { return Math.PI * this.radius ** 2; }
}
class Rectangle extends Shape {
constructor(width, height) { super('Rectangle'); this.width = width; this.height = height; }
area() { return this.width * this.height; }
}
const shapes = [new Circle(5), new Rectangle(4, 6)];
shapes.forEach((shape) => shape.describe());
// Circle has an area of 78.54
// Rectangle has an area of 24.00
π― Quick Quiz
Question 1: When you read a property that isn't on an object, what does JavaScript do?
Question 2: Why put a shared method on Constructor.prototype instead of inside the constructor?
Question 3: ES6 class syntax is best described asβ¦
Summary & Quiz
π Key Takeaways
- Every object links to a prototype; property lookup walks that chain up to
Object.prototype, thennull. - Constructor functions link instances to
Constructor.prototype, letting all instances share one copy of each method. - You can build inheritance with constructor functions,
Object.create(), or ES6class β¦ extendsβ all the same engine. - Shadowing lets an object's own property override an inherited one; that is method overriding.
- Never modify built-in prototypes β write utilities or subclass your own types.
π Further Reading
π What's Next?
You've mastered how objects relate to one another. Next we shift from single objects to collections, starting with how to create and access arrays β the workhorse data structure you'll use in almost every program.
π Excellent!
The prototype chain is one of JavaScript's deepest ideas β and now it's yours.