JavaScript Core
Prototypes
JavaScript's inheritance model
Advanced
javascript oop inheritance objects
Definition
JavaScript uses prototype-based inheritance where objects inherit directly from other objects through a prototype chain. Every object has an internal [[Prototype]] link that references another object, forming a chain that the JavaScript engine traverses when looking up properties.
The Prototype Chain
How It Works
const animal = {
eats: true,
walk() {
console.log('Animal walks');
}
};
const rabbit = {
jumps: true,
__proto__: animal // rabbit inherits from animal
};
// Property lookup goes up the chain
console.log(rabbit.jumps); // true (own property)
console.log(rabbit.eats); // true (inherited from animal)
rabbit.walk(); // "Animal walks" (inherited method)
The Chain Visualization
rabbit
├── jumps: true
└── [[Prototype]] ──> animal
├── eats: true
├── walk: function
└── [[Prototype]] ──> Object.prototype
├── toString
├── valueOf
└── [[Prototype]] ──> null
Creating Objects with Prototypes
Object.create()
const person = {
greet() {
console.log(`Hello, I'm ${this.name}`);
}
};
const student = Object.create(person, {
name: { value: 'Alice' },
study: { value: function() { console.log('Studying...'); } }
});
student.greet(); // "Hello, I'm Alice"
student.study(); // "Studying..."
Constructor Functions
function Animal(name) {
this.name = name;
}
// Add methods to prototype
Animal.prototype.speak = function() {
console.log(`${this.name} makes a sound`);
};
const dog = new Animal('Rex');
dog.speak(); // "Rex makes a sound"
// Verify prototype chain
console.log(dog.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
Prototype Property vs proto
Understanding the Difference
function Person(name) {
this.name = name;
}
// Person.prototype - the object that will be the prototype of instances
Person.prototype.greet = function() {
console.log(`Hi, I'm ${this.name}`);
};
const john = new Person('John');
// john.__proto__ - the actual prototype of the instance
console.log(john.__proto__ === Person.prototype); // true
// Modern alternative: Object.getPrototypeOf()
console.log(Object.getPrototypeOf(john) === Person.prototype); // true
Setting Prototypes
const parent = { parentProp: true };
const child = { childProp: true };
// Old way (deprecated but still works)
child.__proto__ = parent;
// Modern way
Object.setPrototypeOf(child, parent);
// Better: create with prototype
const child2 = Object.create(parent, {
childProp: { value: true }
});
Class Syntax (Syntactic Sugar)
ES6 Classes
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
// Static method on constructor, not prototype
static isAnimal(obj) {
return obj instanceof Animal;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call parent constructor
this.breed = breed;
}
speak() {
console.log(`${this.name} barks!`);
}
fetch() {
console.log(`${this.name} is fetching`);
}
}
const rex = new Dog('Rex', 'German Shepherd');
rex.speak(); // "Rex barks!"
rex.fetch(); // "Rex is fetching"
Under the Hood
// Class syntax is equivalent to:
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a sound`);
};
function Dog(name, breed) {
Animal.call(this, name); // super()
this.breed = breed;
}
// Set up inheritance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function() {
console.log(`${this.name} barks!`);
};
Property Lookup and Shadowing
Shadowing
const parent = {
name: 'Parent',
greet() {
console.log('Hello from parent');
}
};
const child = {
__proto__: parent,
name: 'Child', // Shadows parent's name
greet() {
console.log('Hello from child');
}
};
console.log(child.name); // "Child" (own property)
child.greet(); // "Hello from child" (own method)
Checking Property Origin
const obj = {
ownProp: true,
__proto__: { inheritedProp: true }
};
// Check if property is own property
console.log(obj.hasOwnProperty('ownProp')); // true
console.log(obj.hasOwnProperty('inheritedProp')); // false
// Check if property exists anywhere in chain
console.log('ownProp' in obj); // true
console.log('inheritedProp' in obj); // true
console.log('toString' in obj); // true
// Get all own property names
console.log(Object.keys(obj)); // ['ownProp']
// Get all enumerable properties
for (const key in obj) {
console.log(key); // ownProp, inheritedProp
}
Advanced Patterns
Mixin Pattern
const flyMixin = {
fly() {
console.log(`${this.name} is flying`);
}
};
const swimMixin = {
swim() {
console.log(`${this.name} is swimming`);
}
};
function Duck(name) {
this.name = name;
}
// Copy methods to prototype
Object.assign(Duck.prototype, flyMixin, swimMixin);
const donald = new Duck('Donald');
donald.fly(); // "Donald is flying"
donald.swim(); // "Donald is swimming"
Parasitic Inheritance
function createSpecialArray() {
// Create instance using array constructor
const array = [];
// Augment with custom methods
array.specialMethod = function() {
console.log('Special method called');
};
return array;
}
const special = createSpecialArray();
special.push(1, 2, 3);
special.specialMethod();
Functional Inheritance
function person(name) {
const self = {};
// Private variable (closure)
let secret = 'my secret';
// Public methods
self.getName = () => name;
self.greet = () => console.log(`Hello, I'm ${name}`);
self.revealSecret = () => secret;
return self;
}
function employee(name, title) {
const self = person(name); // Inherit from person
// Extend with new methods
self.getTitle = () => title;
self.introduce = () => {
console.log(`I'm ${self.getName()}, ${title}`);
};
return self;
}
const emp = employee('John', 'Developer');
emp.greet(); // Inherited
emp.introduce(); // Own method
Common Issues
Prototype Pollution
// Dangerous - modifying built-in prototypes
Array.prototype.bad = function() {
// This affects ALL arrays!
};
// Safe - extend with symbol
const customMethod = Symbol('customMethod');
Array.prototype[customMethod] = function() {
// Won't conflict with future JS features
};
Performance Considerations
// Creating methods in constructor
function BadPerson(name) {
this.name = name;
this.greet = function() { // New function every instance!
console.log(`Hello, ${this.name}`);
};
}
// Using prototype
function GoodPerson(name) {
this.name = name;
}
GoodPerson.prototype.greet = function() {
console.log(`Hello, ${this.name}`);
};
// Single function shared across all instances
instanceof Operator
class Animal {}
class Dog extends Animal {}
const dog = new Dog();
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true
// Works with constructor functions too
function Cat() {}
const cat = new Cat();
console.log(cat instanceof Cat); // true
Modern Alternatives
Composition over Inheritance
// Favor composition for complex hierarchies
const canWalk = (state) => ({
walk: () => console.log(`${state.name} is walking`)
});
const canSwim = (state) => ({
swim: () => console.log(`${state.name} is swimming`)
});
const canFly = (state) => ({
fly: () => console.log(`${state.name} is flying`)
});
const createDuck = (name) => {
const state = { name };
return Object.assign(
{},
state,
canWalk(state),
canSwim(state),
canFly(state)
);
};
const duck = createDuck('Donald');
duck.walk();
duck.swim();
duck.fly();
Private Fields (ES2022)
class Counter {
#count = 0; // Truly private
increment() {
this.#count++;
return this.#count;
}
get #formatted() { // Private getter
return `Count: ${this.#count}`;
}
}
const counter = new Counter();
counter.increment();
console.log(counter.#count); // SyntaxError - private!
Key Takeaway
JavaScript’s prototype system provides flexible inheritance through object linking. While ES6 classes offer familiar syntax, understanding prototypes is crucial for debugging, optimizing performance, and avoiding common pitfalls like prototype pollution. Consider composition for complex hierarchies and use modern private fields for true encapsulation.