What is a Prototype?
Every JavaScript object has a hidden internal link ([[Prototype]]) pointing
to another object - its prototype. When you access a property that does
not exist on the object, JavaScript looks for it on the prototype, then the prototype's
prototype, and so on.
const arr = [1, 2, 3];
// arr has its own property: the elements
console.log(arr[0]); // 1
// But arr.push is NOT on arr directly - it is on Array.prototype
console.log(Object.getOwnPropertyNames(arr)); // ['0','1','2','length']
console.log(typeof arr.push); // "function" (found on prototype)
// The chain: arr -> Array.prototype -> Object.prototype -> null
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null (end of chain)
// Same for plain objects
const obj = { name: "Alice" };
// obj -> Object.prototype -> null
console.log(typeof obj.hasOwnProperty); // "function" (from Object.prototype)
console.log(typeof obj.toString); // "function" (from Object.prototype)
Prototype Chain Lookup
Property access walks the prototype chain until the property is found or the chain ends
at null. This enables shared methods across all instances.
// The chain in action
const animal = {
breathe() { return `${this.name} breathes`; }
};
const dog = {
name: "Rex",
bark() { return "Woof!"; }
};
// Set animal as dog's prototype
Object.setPrototypeOf(dog, animal);
console.log(dog.bark()); // "Woof!" (own method)
console.log(dog.breathe()); // "Rex breathes" (from prototype, this = dog)
console.log(dog.toString()); // "[object Object]" (from Object.prototype)
console.log(dog.nonexistent); // undefined (not found anywhere in chain)
// Visualizing the lookup:
// dog.breathe
// -> not on dog
// -> check dog's prototype (animal) -> found! returns animal.breathe
// (called with this = dog because dog is the receiver)
// Override: own property shadows prototype property
dog.breathe = () => "dog breathes differently";
console.log(dog.breathe()); // "dog breathes differently" (own method wins)
Object.create()
Object.create(proto) creates a new object with the specified object as its
prototype. It is the explicit way to set up prototype-based inheritance.
const vehicle = {
type: "vehicle",
describe() {
return `${this.name} is a ${this.type}`;
}
};
// Create car with vehicle as prototype
const car = Object.create(vehicle);
car.name = "Tesla";
car.type = "car"; // overrides prototype type
console.log(car.describe()); // "Tesla is a car"
console.log(Object.getPrototypeOf(car) === vehicle); // true
// Create with null prototype (no inherited properties)
const pure = Object.create(null);
pure.name = "pure object";
console.log(pure.toString); // undefined - no Object.prototype
console.log(Object.keys(pure)); // ["name"]
// Create with property descriptors
const point = Object.create(Object.prototype, {
x: { value: 10, writable: true, enumerable: true, configurable: true },
y: { value: 20, writable: true, enumerable: true, configurable: true }
});
console.log(point.x, point.y); // 10 20
Constructor Function Prototypes
Every function has a prototype property. When you use new,
the new instance's [[Prototype]] is set to that prototype object.
function Animal(name) {
this.name = name; // own property on each instance
}
// Add methods to the prototype (shared across all instances)
Animal.prototype.speak = function() {
return `${this.name} makes a sound.`;
};
Animal.prototype.toString = function() {
return `Animal(${this.name})`;
};
const cat = new Animal("Cat");
const dog = new Animal("Dog");
console.log(cat.speak()); // "Cat makes a sound."
console.log(dog.speak()); // "Dog makes a sound."
// Both instances share the same speak function (memory efficient)
console.log(cat.speak === dog.speak); // true (same reference!)
// Instance chain: cat -> Animal.prototype -> Object.prototype -> null
console.log(Object.getPrototypeOf(cat) === Animal.prototype); // true
console.log(cat instanceof Animal); // true
console.log(cat.constructor === Animal); // true
Prototype-based Inheritance
// Parent constructor
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
return `${this.name} is eating.`;
};
// Child constructor inheriting from Animal
function Dog(name, breed) {
Animal.call(this, name); // call parent constructor (sets this.name)
this.breed = breed;
}
// Set up prototype chain: Dog -> Animal
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // restore constructor reference
// Add Dog-specific methods
Dog.prototype.bark = function() {
return `${this.name} barks!`;
};
const rex = new Dog("Rex", "German Shepherd");
console.log(rex.eat()); // "Rex is eating." (from Animal.prototype)
console.log(rex.bark()); // "Rex barks!" (from Dog.prototype)
console.log(rex.name); // "Rex" (own property)
console.log(rex.breed); // "German Shepherd" (own property)
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
Own vs Inherited Properties
function Person(name) {
this.name = name; // own property
}
Person.prototype.greet = function() { return `Hi, ${this.name}`; }; // prototype
const alice = new Person("Alice");
// Check own property
console.log(Object.hasOwn(alice, "name")); // true (own)
console.log(Object.hasOwn(alice, "greet")); // false (on prototype)
// for...in iterates BOTH own and inherited enumerable properties
for (const key in alice) {
console.log(key); // "name", "greet"
}
// Filter to own only
for (const key in alice) {
if (Object.hasOwn(alice, key)) {
console.log(key); // "name" only
}
}
// Object.keys - own enumerable only
console.log(Object.keys(alice)); // ["name"]
// Object.getOwnPropertyNames - own only (including non-enumerable)
console.log(Object.getOwnPropertyNames(alice)); // ["name"]
Classes as Syntactic Sugar
ES6 class syntax is syntactic sugar over the same prototype-based system.
Under the hood, it creates the same constructor function and prototype chain.
// Class syntax (ES6+)
class Animal {
constructor(name) {
this.name = name; // same as this.name in constructor function
}
eat() { // goes on Animal.prototype
return `${this.name} is eating.`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // same as Animal.call(this, name)
this.breed = breed;
}
bark() { // goes on Dog.prototype
return `${this.name} barks!`;
}
}
const rex = new Dog("Rex", "Labrador");
console.log(rex.eat()); // "Rex is eating."
console.log(rex.bark()); // "Rex barks!"
// Same prototype chain as the function-based approach
console.log(Object.getPrototypeOf(rex) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(typeof Animal); // "function" - classes are just functions!