What is a Prototype?
In JavaScript, every object has a hidden link to another object called its prototype. When you try to access a property that doesn't exist on an object, JavaScript automatically looks for it in the object's prototype, and if not found there, in the prototype's prototype, continuing up the chain until it reaches the end (typically Object.prototype).
Think of prototypes like genetic inheritance in living organisms. Just as you inherit traits from your ancestors, JavaScript objects inherit properties and methods from their prototype ancestors.
Visualizing the Prototype Chain
How Prototypes Work
Every JavaScript object has an internal property called [[Prototype]] (accessible via __proto__ in browsers, though this is deprecated). This property refers to the object's prototype.
// Creating an object
const person = {
firstName: "John",
lastName: "Doe",
getFullName: function() {
return this.firstName + " " + this.lastName;
}
};
// Creating another object that inherits from person
const employee = {
jobTitle: "Developer",
employeeId: "EMP123"
};
// Set person as the prototype of employee
Object.setPrototypeOf(employee, person);
// Now employee inherits properties and methods from person
console.log(employee.firstName); // "John"
console.log(employee.getFullName()); // "John Doe"
console.log(employee.jobTitle); // "Developer"
// Check if a property exists directly on the object
console.log(employee.hasOwnProperty('jobTitle')); // true
console.log(employee.hasOwnProperty('firstName')); // false (inherited)
Property Lookup Process
When you access a property on an object, JavaScript follows these steps:
- Check if the property exists on the object itself
- If not, check the object's prototype
- If still not found, check the prototype's prototype
- Continue up the prototype chain until the property is found or until Object.prototype is reached
- If the property is not found anywhere in the chain, return
undefined
// Property lookup visualization
console.log(employee.toString()); // "[object Object]"
// Here's what happens:
// 1. JavaScript looks for toString on the employee object - not found
// 2. Looks for toString on employee's prototype (person) - not found
// 3. Looks for toString on person's prototype (Object.prototype) - FOUND!
// 4. The method is called with 'employee' as 'this'
Constructor Functions and Prototypes
When you use constructor functions with the new keyword, JavaScript automatically creates a new object and links it to the constructor's prototype property.
// Constructor function
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// Adding a method to the prototype
Person.prototype.getFullName = function() {
return this.firstName + " " + this.lastName;
};
// Creating instances
const person1 = new Person("John", "Doe");
const person2 = new Person("Jane", "Smith");
console.log(person1.getFullName()); // "John Doe"
console.log(person2.getFullName()); // "Jane Smith"
// Both instances share the same method
console.log(person1.getFullName === person2.getFullName); // true
The power of prototypes is memory efficiency - the method is defined only once in memory (on the prototype) but can be used by all instances.
The Benefits of Prototype Methods
// Methods defined in the constructor (less efficient)
function Car1(make, model) {
this.make = make;
this.model = model;
// Each instance gets its own copy of this method
this.getInfo = function() {
return `${this.make} ${this.model}`;
};
}
// Methods defined on the prototype (more efficient)
function Car2(make, model) {
this.make = make;
this.model = model;
}
// All instances share one copy of this method
Car2.prototype.getInfo = function() {
return `${this.make} ${this.model}`;
};
// Memory usage comparison
const car1a = new Car1("Toyota", "Corolla");
const car1b = new Car1("Honda", "Civic");
const car2a = new Car2("Toyota", "Corolla");
const car2b = new Car2("Honda", "Civic");
console.log(car1a.getInfo === car1b.getInfo); // false (different function objects)
console.log(car2a.getInfo === car2b.getInfo); // true (same function object)
Prototype Inheritance
JavaScript's inheritance is prototype-based, not class-based. Objects inherit directly from other objects.
Creating Inheritance Chains
// Parent constructor
function Animal(name) {
this.name = name;
this.isAlive = true;
}
// Parent methods
Animal.prototype.eat = function(food) {
console.log(`${this.name} is eating ${food}`);
};
Animal.prototype.sleep = function() {
console.log(`${this.name} is sleeping`);
};
// Child constructor
function Dog(name, breed) {
// Call the parent constructor
Animal.call(this, name);
this.breed = breed;
}
// Set up inheritance
// This creates the prototype chain: Dog -> Animal -> Object
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // Fix the constructor property
// Add methods specific to Dog
Dog.prototype.bark = function() {
console.log(`${this.name} says woof!`);
};
Dog.prototype.fetch = function(item) {
console.log(`${this.name} fetches the ${item}`);
};
// Create a dog instance
const rex = new Dog("Rex", "German Shepherd");
rex.eat("dog food"); // "Rex is eating dog food" (inherited method)
rex.sleep(); // "Rex is sleeping" (inherited method)
rex.bark(); // "Rex says woof!" (own method)
// Verify the prototype chain
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
console.log(rex instanceof Object); // true
This pattern is how JavaScript implemented inheritance before ES6 classes.
ES6 Classes and Inheritance
ES6 introduced the class syntax, which is syntactic sugar over the prototype-based inheritance system. Under the hood, it still uses prototypes.
// Parent class
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`);
}
}
// Child class extending the parent
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call parent constructor
this.breed = breed;
}
bark() {
console.log(`${this.name} says woof!`);
}
fetch(item) {
console.log(`${this.name} fetches the ${item}`);
}
}
// Create a dog instance
const buddy = new Dog("Buddy", "Golden Retriever");
buddy.eat("treats"); // "Buddy is eating treats" (inherited method)
buddy.bark(); // "Buddy says woof!" (own method)
// This is still prototype-based inheritance behind the scenes
console.log(Object.getPrototypeOf(buddy) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
The ES6 class syntax makes inheritance patterns more readable and familiar to developers coming from class-based languages, while still leveraging JavaScript's prototype system.
Object.create() Pattern
Another way to create objects with specific prototypes is using Object.create(), which allows direct prototype assignment without constructors.
// Create a base object to serve as a prototype
const vehiclePrototype = {
init: function(make, model) {
this.make = make;
this.model = model;
return this;
},
getInfo: function() {
return `${this.make} ${this.model}`;
},
start: function() {
console.log("Engine started");
},
stop: function() {
console.log("Engine stopped");
}
};
// Create a car object that inherits from vehiclePrototype
const car = Object.create(vehiclePrototype);
car.init("Toyota", "Camry");
car.drive = function() {
console.log("Car is driving");
};
console.log(car.getInfo()); // "Toyota Camry"
car.start(); // "Engine started"
car.drive(); // "Car is driving"
// Create a motorcycle that also inherits from vehiclePrototype
const motorcycle = Object.create(vehiclePrototype);
motorcycle.init("Harley", "Davidson");
motorcycle.wheelie = function() {
console.log("Doing a wheelie!");
};
motorcycle.start(); // "Engine started"
motorcycle.wheelie(); // "Doing a wheelie!"
This approach emphasizes behavior delegation rather than type inheritance, which many consider more aligned with JavaScript's design philosophy.
Property Shadowing
When an object has a property with the same name as a property in its prototype chain, the object's own property "shadows" the prototype property.
function Vehicle() {
this.speed = 0;
}
Vehicle.prototype.accelerate = function() {
this.speed += 10;
console.log(`Speed: ${this.speed}`);
};
function SportsCar() {
Vehicle.call(this);
}
SportsCar.prototype = Object.create(Vehicle.prototype);
SportsCar.prototype.constructor = SportsCar;
// Override the accelerate method
SportsCar.prototype.accelerate = function() {
// This shadows the Vehicle.prototype.accelerate method
this.speed += 20; // Sports cars accelerate faster
console.log(`ZOOM! Speed: ${this.speed}`);
};
const car = new Vehicle();
const ferrari = new SportsCar();
car.accelerate(); // "Speed: 10"
ferrari.accelerate(); // "ZOOM! Speed: 20"
// We can still access the parent method using call or apply
Vehicle.prototype.accelerate.call(ferrari); // "Speed: 30"
Property shadowing allows for method overriding, a key concept in object-oriented programming.
Working with Prototypes Correctly
Best Practices
- Don't modify built-in prototypes - Extending Object.prototype, Array.prototype, etc., can cause conflicts and unexpected behavior in other code
- Use Object.getPrototypeOf() instead of __proto__ - The __proto__ property is deprecated
- Fix the constructor property when setting up inheritance - Otherwise instanceof might not work correctly
- Consider using ES6 classes for cleaner inheritance - They provide a more familiar syntax while still using prototypes
Common Pitfalls
// BAD PRACTICE: Modifying built-in prototypes
Array.prototype.first = function() {
return this[0];
};
// This could break code that loops through arrays
for (let prop in [1, 2, 3]) {
console.log(prop); // "0", "1", "2", "first" - Oops!
}
// BETTER PRACTICE: Create utility functions or extend only your objects
function getFirst(array) {
return array[0];
}
// Or create subclasses if using ES6
class MyArray extends Array {
first() {
return this[0];
}
}
Real-World Applications
Building a UI Component System
// Base Component
class UIComponent {
constructor(id) {
this.id = id;
this.element = null;
}
render() {
throw new Error("Render method must be implemented by subclasses");
}
mount(container) {
if (!this.element) {
this.render();
}
container.appendChild(this.element);
this.onMount();
}
onMount() {
// Hook for subclasses
}
unmount() {
if (this.element && this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
this.onUnmount();
}
}
onUnmount() {
// Hook for subclasses
}
}
// Button Component
class Button extends UIComponent {
constructor(id, text, onClick) {
super(id);
this.text = text;
this.onClick = onClick;
}
render() {
this.element = document.createElement('button');
this.element.id = this.id;
this.element.textContent = this.text;
this.element.addEventListener('click', this.onClick);
return this.element;
}
setText(newText) {
this.text = newText;
if (this.element) {
this.element.textContent = newText;
}
}
disable() {
if (this.element) {
this.element.disabled = true;
}
}
enable() {
if (this.element) {
this.element.disabled = false;
}
}
}
// Form Input Component
class Input extends UIComponent {
constructor(id, type, placeholder, value = '') {
super(id);
this.type = type;
this.placeholder = placeholder;
this.value = value;
}
render() {
this.element = document.createElement('input');
this.element.id = this.id;
this.element.type = this.type;
this.element.placeholder = this.placeholder;
this.element.value = this.value;
this.element.addEventListener('input', (e) => {
this.value = e.target.value;
});
return this.element;
}
getValue() {
return this.value;
}
setValue(newValue) {
this.value = newValue;
if (this.element) {
this.element.value = newValue;
}
}
}
// Usage example
const loginButton = new Button('login-btn', 'Log In', () => {
console.log('Login clicked');
if (usernameInput.getValue() && passwordInput.getValue()) {
// Perform login
loginButton.setText('Logging in...');
loginButton.disable();
}
});
const usernameInput = new Input('username', 'text', 'Username');
const passwordInput = new Input('password', 'password', 'Password');
// Mount components to DOM
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('app');
usernameInput.mount(container);
passwordInput.mount(container);
loginButton.mount(container);
});
Game Entity System
// Base Game Entity
class Entity {
constructor(x, y) {
this.x = x;
this.y = y;
this.width = 0;
this.height = 0;
this.speed = 0;
this.active = true;
}
update(deltaTime) {
// Base update logic
}
render(context) {
// Base rendering logic
}
getBounds() {
return {
left: this.x,
top: this.y,
right: this.x + this.width,
bottom: this.y + this.height
};
}
collidesWith(entity) {
const a = this.getBounds();
const b = entity.getBounds();
return a.left < b.right &&
a.right > b.left &&
a.top < b.bottom &&
a.bottom > b.top;
}
}
// Player Character
class Player extends Entity {
constructor(x, y) {
super(x, y);
this.width = 32;
this.height = 48;
this.speed = 200;
this.health = 100;
this.isJumping = false;
this.jumpVelocity = 0;
}
update(deltaTime) {
super.update(deltaTime);
// Player-specific update logic
if (this.isJumping) {
this.y += this.jumpVelocity * deltaTime;
this.jumpVelocity += 980 * deltaTime; // Gravity
if (this.y > 400) { // Ground level
this.y = 400;
this.isJumping = false;
}
}
}
render(context) {
context.fillStyle = 'blue';
context.fillRect(this.x, this.y, this.width, this.height);
}
jump() {
if (!this.isJumping) {
this.isJumping = true;
this.jumpVelocity = -500;
}
}
takeDamage(amount) {
this.health -= amount;
if (this.health <= 0) {
this.health = 0;
this.active = false;
}
}
}
// Enemy Character
class Enemy extends Entity {
constructor(x, y, type) {
super(x, y);
this.type = type;
this.width = 32;
this.height = 32;
if (type === 'basic') {
this.speed = 100;
this.damage = 10;
} else if (type === 'advanced') {
this.speed = 150;
this.damage = 20;
}
}
update(deltaTime) {
super.update(deltaTime);
// Move toward the player
this.x -= this.speed * deltaTime;
// Remove if off screen
if (this.x < -this.width) {
this.active = false;
}
}
render(context) {
context.fillStyle = this.type === 'basic' ? 'red' : 'darkred';
context.fillRect(this.x, this.y, this.width, this.height);
}
attack(player) {
if (this.collidesWith(player)) {
player.takeDamage(this.damage);
this.active = false; // Enemy disappears after attacking
}
}
}
Practice Activities
Activity 1: Create a Shape Hierarchy
Implement a Shape base class and create subclasses for Circle, Rectangle, and Triangle. Add methods to calculate area and perimeter for each shape.
Activity 2: Build a Vehicle Inheritance System
Create a hierarchical structure for vehicles, starting with a base Vehicle class, then Car and Motorcycle subclasses, and finally specific models like Sedan, SUV, SportBike, etc.
Activity 3: Implement Prototype Methods for Arrays
Create your own MyArray constructor that inherits from Array but has additional methods like sum(), average(), and max(). Do this without modifying the built-in Array prototype.
Summary
In this lecture, we've explored:
- The prototype chain - how JavaScript objects inherit properties and methods
- Constructor functions and their relationship with prototypes
- Various inheritance patterns: constructor functions, Object.create(), and ES6 classes
- Property shadowing and method overriding
- Best practices when working with prototypes
- Real-world applications of prototype-based inheritance
Understanding JavaScript's prototype system is essential for mastering object-oriented programming in the language. Even with the introduction of ES6 classes, the prototype chain is still the underlying mechanism for inheritance.