Inheritance is a fundamental concept in object-oriented programming that allows one object to acquire the properties and methods of another. In JavaScript, a prototype-based language, inheritance works differently from class-based languages, relying on prototype chains rather than classes to establish relationships between objects.
Prototype Chain Enheritance
This is the most basic form of inheritance in JavaScript. Every JavaScript object has a prototype, which is another object that the first object "inherits" properties and methods from. When you try to access a property or method on an object, if it's not found directly on the object, JavaScript looks up the prototype chain until it finds the property or reaches the end of the chain (null).
To implement prototype chain inheritance, you set the prototype of the child constructor to an instance of the parent constructor.
function SuperType(name) {
this.value = name;
this.commonArray = ['one', 'two']; // Reference type property
}
SuperType.prototype.showValue = function() {
return this.value;
};
function SubType(name, specificId) {
this.childValue = specificId;
}
// Inherit from SuperType by setting SubType's prototype to an instance of SuperType
SubType.prototype = new SuperType('DefaultName');
// Correct the constructor pointer, as SubType.prototype is now an instance of SuperType
SubType.prototype.constructor = SubType;
SubType.prototype.showChildValue = function() {
return this.childValue;
};
const instanceOne = new SubType('InstanceOneId');
console.log(instanceOne.showValue()); // 'DefaultName' - inherited from the SuperType instance in the prototype chain
console.log(instanceOne.value); // 'DefaultName'
instanceOne.commonArray.push('three');
console.log(instanceOne.commonArray); // ['one', 'two', 'three']
const instanceTwo = new SubType('InstanceTwoId');
console.log(instanceTwo.commonArray); // ['one', 'two', 'three'] - Demonstrates shared reference type issue
console.log(instanceTwo.showChildValue()); // 'InstanceTwoId'
Drawbacks:
- Reference type properties on the supertype's prototype are shared among all subtype instances.
- It's not possible to pass arguments to the supertype's constructor when setting up the prototype chain, as the instance is created only once.
- The supertype's constructor is called when setting the prototype, even if it's not strictly necessary for instance-specific properties.
Constructor Function Inheritance (or Call/Apply Inheritance)
This method addresses the issues of shared reference types and passing arguments by using call() or apply() within the subtype's constructor to invoke the supertype's constructor in the context of the new instance.
function Gadget(brand) {
this.brandName = brand;
this.features = ['bluetooth', 'wifi']; // Instance-specific array
}
Gadget.prototype.getBrand = function() {
return this.brandName;
};
function Smartphone(brand, model) {
// Call the Gadget constructor in the context of the new Smartphone instance
Gadget.call(this, brand);
this.modelName = model;
}
const myPhone = new Smartphone('TechCorp', 'X100');
console.log(myPhone.brandName); // 'TechCorp'
console.log(myPhone.modelName); // 'X100'
myPhone.features.push('5G');
console.log(myPhone.features); // ['bluetooth', 'wifi', '5G']
const yourPhone = new Smartphone('GlobalNet', 'P200');
console.log(yourPhone.features); // ['bluetooth', 'wifi'] - Not shared with myPhone's features
// console.log(myPhone.getBrand()); // This will throw an error
// because Gadget.prototype methods are not inherited
Drawbacks:
- Methods defined on the supertype's prototype are not inherited. They would need to be defined within the supertype's constructor, which leads to re-creation for every instance.
- No prototype chain is established between the supertype and subtype, meaning
instanceofchecks might not work as expected for the prototype chain.
Combination Inheritance
Combination inheritance (sometimes called pseudoclassical inheritance) combines constructor function inheritance (for properties) and prototype chain inheritance (for methods). It's a widely used pattern due to its robustness in handling both instance-specific properties and shared methods.
function Asset(name) {
this.assetName = name;
this.components = ['cpu', 'ram']; // Instance-specific array
}
Asset.prototype.getAssetName = function() {
return this.assetName;
};
function Computer(name, os) {
Asset.call(this, name); // Inherit properties (first call to Asset constructor)
this.operatingSystem = os;
}
// Inherit methods from Asset's prototype
Computer.prototype = new Asset(); // (Second call to Asset constructor)
Computer.prototype.constructor = Computer; // Correct the constructor pointer
Computer.prototype.getOperatingSystem = function() {
return this.operatingSystem;
};
const desktop = new Computer('Workstation', 'Windows');
desktop.components.push('gpu');
console.log(desktop.assetName); // 'Workstation'
console.log(desktop.getAssetName()); // 'Workstation'
console.log(desktop.getOperatingSystem()); // 'Windows'
console.log(desktop.components); // ['cpu', 'ram', 'gpu']
const laptop = new Computer('TravelBook', 'macOS');
console.log(laptop.components); // ['cpu', 'ram'] - Not shared
// console.log(desktop.__proto__.assetName); // undefined - showing the prototype instance has default values
Drawback:
- The supertype's constructor is called twice: once for property inheritance (
Asset.call(this, name)) and once to set up the prototype chain (Computer.prototype = new Asset()). This can be inefficient and might create redundant properties on the prototype object.
Prototypal Inheritance (Object.create)
Douglas Crockford popularized this pattern, which is now formalized in ES5 with Object.create(). It allows you to create a new object directly from an existing object, setting the existing object as the prototype of the new one. This is useful for object-to-object inheritance without involving constructor functions.
const userProfile = {
role: 'Guest',
permissions: ['read'],
displayRole: function() {
console.log(`User Role: ${this.role}`);
}
};
// A custom implementation similar to Object.create (for understanding)
function createPrototypeObject(o) {
function F() {}
F.prototype = o;
return new F();
}
const adminProfile = createPrototypeObject(userProfile); // or Object.create(userProfile)
adminProfile.role = 'Administrator'; // Own property, doesn't affect userProfile
adminProfile.permissions.push('write'); // Modifies shared reference
adminProfile.displayRole(); // User Role: Administrator
const editorProfile = Object.create(userProfile);
editorProfile.role = 'Editor';
editorProfile.permissions.push('edit');
editorProfile.displayRole(); // User Role: Editor
console.log(userProfile.permissions); // ['read', 'write', 'edit'] - Shows shared reference issue
Drawback:
- Reference type properties on the prototype object are shared among all instances created from it.
Parasitic Inheritance
Parasitic inheritance builds upon prototypal inheritance by creating a wrapper function. This function uses Object.create() (or a custom equivalent) to create an object, then "enhances" it by adding new properties and methods before returning it.
const baseConfig = {
version: '1.0',
environment: 'development',
getSettings: function() {
return `Version: ${this.version}, Env: ${this.environment}`;
}
};
function createAdvancedConfig(base) {
const config = Object.create(base); // Create a new object from the base
config.featureFlags = ['alpha', 'beta']; // Add new instance-specific properties
config.applyUpdates = function() { // Add new instance-specific method
console.log(`Applying updates for version ${this.version}`);
};
return config;
}
const appConfig = createAdvancedConfig(baseConfig);
console.log(appConfig.getSettings()); // Version: 1.0, Env: development
appConfig.applyUpdates(); // Applying updates for version 1.0
appConfig.featureFlags.push('gamma');
const serviceConfig = createAdvancedConfig(baseConfig);
console.log(serviceConfig.featureFlags); // ['alpha', 'beta'] - featureFlags is not shared
Drawback:
- Methods added to the instance (like
applyUpdates) are not shared via the prototype, meaning each instance gets its own copy. This can lead to inefficient memory usage if many instance are created with the same new methods.
Parasitic Combination Inheritance
This pattern is considered the most efficient inheritance strategy in ES5 and earlier versions, as it avoids the double constructor call of combination inheritance while preserving the prototype chain and allowing distinct instance properties. It combines the best aspects of constructor stealing (for properties) and prototypal inheritance (for methods).
/**
* Helper function to facilitate parasitic combination inheritance.
* Sets the subType's prototype to a new object that inherits from superType.prototype,
* then corrects the constructor pointer.
* @param {Function} subType The constructor function of the child.
* @param {Function} superType The constructor function of the parent.
*/
function enhancePrototype(subType, superType) {
// Create an object that inherits from superType.prototype
const prototype = Object.create(superType.prototype);
// Assign the constructor to the subType
prototype.constructor = subType;
// Set the subType's prototype to the new object
subType.prototype = prototype;
}
function Employee(id) {
this.employeeId = id;
this.projects = ['projectA']; // Instance-specific array
}
Employee.prototype.getEmployeeId = function() {
return this.employeeId;
};
function Manager(id, department) {
Employee.call(this, id); // Inherit properties using constructor stealing
this.departmentName = department;
}
// Establish the prototype chain without calling the Employee constructor
enhancePrototype(Manager, Employee);
Manager.prototype.getDepartment = function() {
return this.departmentName;
};
const managerOne = new Manager('E101', 'Sales');
managerOne.projects.push('projectB');
console.log(managerOne.getEmployeeId()); // 'E101'
console.log(managerOne.getDepartment()); // 'Sales'
console.log(managerOne.projects); // ['projectA', 'projectB']
const managerTwo = new Manager('E102', 'Marketing');
console.log(managerTwo.projects); // ['projectA'] - Not shared
// Verify prototype chain and constructor
console.log(managerOne instanceof Manager); // true
console.log(managerOne instanceof Employee); // true
console.log(managerOne.constructor); // Manager
Parasitic combination inheritance provides an efficient way to achieve property inheritance, method inheritance, and proper prototype chaining, making it the most robust pre-ES6 pattern.