JavaScript Prototypes
The Foundation of JavaScript Objects
Explanation
What are Prototypes?
JavaScript uses prototypal inheritance. Every object has a hidden [[Prototype]] property that links to another object. When you access a property, JavaScript looks up the prototype chain.
Key Concepts
- Prototype Chain: Object → Prototype → Prototype → null
__proto__: Access an object's prototype (legacy)Object.getPrototypeOf(): Modern way to access prototype.prototype: Property on constructor functions
Classes vs Prototypes
// Classes are syntactic sugar
class Dog {
bark() { return 'Woof!'; }
}
// Equivalent prototype code
function Dog() {}
Dog.prototype.bark = function() { return 'Woof!'; };
Demonstration
Example 1: Prototype Basics
// Every object has a prototype
const obj = {};
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
// Prototype chain
const arr = [];
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null
// Property lookup
const parent = { greeting: 'Hello' };
const child = Object.create(parent);
child.name = 'Arthur';
console.log(child.name); // 'Arthur' (own property)
console.log(child.greeting); // 'Hello' (inherited)
console.log(child.hasOwnProperty('name')); // true
console.log(child.hasOwnProperty('greeting')); // false
// Shadowing
child.greeting = 'Hi';
console.log(child.greeting); // 'Hi'
console.log(parent.greeting); // 'Hello' (unchanged)
// Check prototype chain
console.log('greeting' in child); // true (any level)
console.log(parent.isPrototypeOf(child)); // true
Example 2: Constructor Functions
// Constructor function (pre-ES6)
function User(name, email) {
this.name = name;
this.email = email;
}
// Methods on prototype (shared across instances)
User.prototype.greet = function() {
return `Hello, I'm ${this.name}!`;
};
User.prototype.updateEmail = function(newEmail) {
this.email = newEmail;
return this;
};
// Static method (on constructor itself)
User.createGuest = function() {
return new User('Guest', 'guest@example.com');
};
// Usage
const user1 = new User('Arthur', 'art@bpc.com');
const user2 = new User('Sarah', 'sarah@example.com');
// Both share the same methods
console.log(user1.greet === user2.greet); // true
// But have different properties
console.log(user1.name); // 'Arthur'
console.log(user2.name); // 'Sarah'
// What `new` does:
// 1. Creates empty object: {}
// 2. Sets prototype: Object.setPrototypeOf({}, User.prototype)
// 3. Binds `this` and calls constructor
// 4. Returns the object (unless constructor returns object)
Example 3: Prototypal Inheritance
// Parent constructor
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return `${this.name} makes a sound`;
};
// Child constructor
function Dog(name, breed) {
Animal.call(this, name); // Call parent constructor
this.breed = breed;
}
// Set up inheritance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// Add/override methods
Dog.prototype.speak = function() {
return `${this.name} says Woof!`;
};
Dog.prototype.fetch = function() {
return `${this.name} fetches the ball`;
};
const dog = new Dog('Buddy', 'Golden Retriever');
console.log(dog.speak()); // Buddy says Woof!
console.log(dog.fetch()); // Buddy fetches the ball
// Check inheritance
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true
// Prototype chain:
// dog → Dog.prototype → Animal.prototype → Object.prototype → null
Example 4: Object.create() Patterns
// Pure prototypal inheritance (no constructors)
const personProto = {
greet() {
return `Hello, I'm ${this.name}!`;
},
introduce() {
return `I'm ${this.name}, ${this.age} years old`;
}
};
// Create object with prototype
const arthur = Object.create(personProto, {
name: { value: 'Arthur', writable: true },
age: { value: 30, writable: true }
});
console.log(arthur.greet()); // Hello, I'm Arthur!
// Factory with Object.create
function createPerson(name, age) {
const person = Object.create(personProto);
person.name = name;
person.age = age;
return person;
}
const sarah = createPerson('Sarah', 25);
console.log(sarah.introduce()); // I'm Sarah, 25 years old
// Object with no prototype
const dict = Object.create(null);
dict.key = 'value';
console.log(dict.hasOwnProperty); // undefined (no inherited methods!)
// Useful for dictionary/map objects to avoid prototype pollution
Example 5: Modifying Built-in Prototypes
// Adding methods to built-ins (use with caution!)
Array.prototype.first = function() {
return this[0];
};
Array.prototype.last = function() {
return this[this.length - 1];
};
console.log([1, 2, 3].first()); // 1
console.log([1, 2, 3].last()); // 3
// Better: Use symbols to avoid conflicts
const first = Symbol('first');
const last = Symbol('last');
Array.prototype[first] = function() {
return this[0];
};
// Even better: Don't modify built-ins
// Use utility functions or subclass
class ExtendedArray extends Array {
first() {
return this[0];
}
last() {
return this[this.length - 1];
}
}
// Checking if method exists before adding
if (!Array.prototype.includes) {
Array.prototype.includes = function(item) {
return this.indexOf(item) !== -1;
};
}
Example 6: Performance Considerations
// Methods on prototype (efficient - shared)
function User(name) {
this.name = name;
}
User.prototype.greet = function() {
return `Hello, ${this.name}`;
};
// Methods in constructor (inefficient - new function per instance)
function UserBad(name) {
this.name = name;
this.greet = function() { // New function each time!
return `Hello, ${this.name}`;
};
}
// Memory comparison
const users = Array.from({ length: 1000 }, (_, i) =>
new User(`User ${i}`)
);
const usersBad = Array.from({ length: 1000 }, (_, i) =>
new UserBad(`User ${i}`)
);
// All 1000 users share ONE greet function
console.log(users[0].greet === users[999].greet); // true
// Each user has their OWN greet function
console.log(usersBad[0].greet === usersBad[999].greet); // false
Key Takeaways:
- Objects inherit from their prototype
- Methods on prototype are shared (memory efficient)
Object.create()creates objects with specified prototype- Classes are syntactic sugar over prototypes
- Avoid modifying built-in prototypes
Imitation
Challenge 1: Implement Basic Inheritance
Task: Create a Shape hierarchy using only prototypes (no classes).
Solution
// Base constructor
function Shape(x, y) {
this.x = x;
this.y = y;
}
Shape.prototype.move = function(dx, dy) {
this.x += dx;
this.y += dy;
return this;
};
Shape.prototype.area = function() {
throw new Error('Not implemented');
};
// Circle constructor
function Circle(x, y, radius) {
Shape.call(this, x, y);
this.radius = radius;
}
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;
Circle.prototype.area = function() {
return Math.PI * this.radius ** 2;
};
// Rectangle constructor
function Rectangle(x, y, width, height) {
Shape.call(this, x, y);
this.width = width;
this.height = height;
}
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
Rectangle.prototype.area = function() {
return this.width * this.height;
};
// Usage
const circle = new Circle(0, 0, 5);
const rect = new Rectangle(0, 0, 10, 5);
console.log(circle.area()); // ~78.54
console.log(rect.area()); // 50
circle.move(10, 10);
console.log(circle.x, circle.y); // 10, 10
Practice
Exercise 1: Polyfill Array.flat()
Difficulty: Intermediate
Implement Array.prototype.flat using prototypes:
[1, [2, [3]]].flat(2) // [1, 2, 3]
Exercise 2: Object Pool
Difficulty: Advanced
Create an object pool using prototypes:
- Reuse objects instead of creating new ones
- acquire() and release() methods
- Efficient for high-frequency object creation
Summary
What you learned:
- How prototypal inheritance works
- Constructor functions and
.prototype - Setting up inheritance chains
- Object.create() patterns
- Performance implications
Next Steps:
- Read: JavaScript this
- Practice: Convert prototype code to classes
- Deep dive: Reflect and Proxy
