原型继承:JavaScript 中的多种继承模式详解
与其他基于类的编程语言不同,JavaScript 的继承机制是建立在对象之间的链接之上的。这种机制被称为原型继承。当我们访问一个对象的属性时,如果对象本身没有,引擎会自动沿着这个链接向上查找。理解这种委托机制,是掌握 JavaScript 独特面向对象特性的关键。
原型继承的基本概念
原型继承是 JavaScript 实现对象间继承关系的一种方式。通过原型继承,一个对象可以访问另一个对象(其原型)的属性和方法。这种继承方式不同于传统面向对象语言中的类继承,而是基于对象之间的委托关系。
基本原型继承
javascript
// 父对象
const animal = {
type: "animal",
speak() {
console.log(`${this.type} makes a sound`);
},
eat() {
console.log(`${this.type} is eating`);
},
};
// 通过Object.create()创建继承animal的对象
const dog = Object.create(animal);
dog.type = "dog";
dog.bark = function () {
console.log("Woof! Woof!");
};
// dog继承了animal的方法
dog.speak(); // dog makes a sound
dog.eat(); // dog is eating
dog.bark(); // Woof! Woof!
// 检查原型关系
console.log(dog.__proto__ === animal); // true原型链继承模式
1. 简单原型继承
这是最基本的继承方式,直接将子构造函数的原型指向父构造函数的实例。
javascript
function Parent(name) {
this.name = name;
this.colors = ["red", "green", "blue"];
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child(name, age) {
this.age = age;
}
// 关键:将Child的原型指向Parent的实例
Child.prototype = new Parent();
const child1 = new Child("John", 25);
const child2 = new Child("Sarah", 30);
child1.sayName(); // undefined (因为Parent构造函数执行时没有传入name)
child1.colors.push("yellow");
// 问题:引用类型的属性会被所有实例共享
console.log(child2.colors); // ['red', 'green', 'blue', 'yellow']问题分析:
- 引用类型的属性会被所有实例共享
- 创建子实例时无法向父构造函数传递参数
2. 构造函数继承(借用构造函数)
为了解决原型继承的问题,出现了构造函数继承模式。
javascript
function Parent(name, colors) {
this.name = name;
this.colors = colors;
this.sayName = function () {
console.log(this.name);
};
}
function Child(name, age, colors) {
// 关键:借用Parent构造函数,使用call或apply
Parent.call(this, name, colors);
this.age = age;
}
const child1 = new Child("John", 25, ["red", "green"]);
const child2 = new Child("Sarah", 30, ["blue", "yellow"]);
child1.sayName(); // John
child2.sayName(); // Sarah
// 各自拥有独立的colors数组
child1.colors.push("purple");
console.log(child1.colors); // ['red', 'green', 'purple']
console.log(child2.colors); // ['blue', 'yellow']
// 问题:方法都在构造函数中定义,无法实现方法复用
console.log(child1.sayName === child2.sayName); // false优点:
- 解决了引用类型共享的问题
- 可以向父构造函数传递参数
缺点:
- 方法在构造函数中定义,每次创建实例都会重新创建
- 无法实现函数复用
3. 组合继承(伪经典继承)
组合继承结合了原型链继承和构造函数继承的优点,是最常用的继承模式。
javascript
function Parent(name, colors) {
this.name = name;
this.colors = colors;
}
// 方法在原型上定义
Parent.prototype.sayName = function () {
console.log(this.name);
};
Parent.prototype.sayColors = function () {
console.log(this.colors.join(", "));
};
function Child(name, age, colors) {
// 继承属性(借用构造函数)
Parent.call(this, name, colors);
this.age = age;
}
// 继承方法(原型链)
Child.prototype = new Parent();
Child.prototype.constructor = Child; // 修复constructor指向
// 添加子类特有的方法
Child.prototype.sayAge = function () {
console.log(`I am ${this.age} years old`);
};
const child1 = new Child("John", 25, ["red", "green"]);
const child2 = new Child("Sarah", 30, ["blue", "yellow"]);
// 验证继承
child1.sayName(); // John
child1.sayColors(); // red, green
child1.sayAge(); // I am 25 years old
// 引用类型独立
child1.colors.push("purple");
console.log(child1.colors); // ['red', 'green', 'purple']
console.log(child2.colors); // ['blue', 'yellow']
// 方法复用
console.log(child1.sayName === child2.sayName); // true
console.log(child1.sayColors === child2.sayColors); // true组合继承的改进版本:
javascript
function Parent(name, colors) {
this.name = name;
this.colors = colors;
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child(name, age, colors) {
Parent.call(this, name, colors);
this.age = age;
}
// 不使用new Parent(),而是直接使用Object.create
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.sayAge = function () {
console.log(`I am ${this.age} years old`);
};这种改进版本避免了调用父构造函数两次的问题。
4. 寄生式继承
寄生式继承创建一个封装继承过程的函数,在内部以某种方式增强对象。
javascript
function createAnother(original) {
// 创建一个新对象,继承original
const clone = Object.create(original);
// 增强对象
clone.sayHi = function () {
console.log("Hi!");
};
return clone;
}
const person = {
name: "John",
friends: ["Tom", "Jerry"],
};
const anotherPerson = createAnother(person);
anotherPerson.sayHi(); // Hi!
console.log(anotherPerson.name); // John
console.log(anotherPerson.friends); // ['Tom', 'Jerry']
// 注意:引用类型仍然是共享的
anotherPerson.friends.push("Bob");
console.log(person.friends); // ['Tom', 'Jerry', 'Bob']5. 寄生组合式继承
这是最理想的继承方式,解决了组合继承调用两次父构造函数的问题。
javascript
function inheritPrototype(child, parent) {
// 创建父类原型的副本
const prototype = Object.create(parent.prototype);
// 修复constructor指向
prototype.constructor = child;
// 设置子类原型
child.prototype = prototype;
}
function Parent(name, colors) {
this.name = name;
this.colors = colors;
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child(name, age, colors) {
Parent.call(this, name, colors);
this.age = age;
}
// 使用寄生组合式继承
inheritPrototype(Child, Parent);
// 添加子类方法
Child.prototype.sayAge = function () {
console.log(`I am ${this.age} years old`);
};
const child = new Child("John", 25, ["red", "green"]);
child.sayName(); // John
child.sayAge(); // I am 25 years old寄生组合式继承的优势:
- 只调用一次父构造函数
- 避免在子类原型上创建不必要的属性
- 保持原型链不变
- 是最理想的继承范式
实际应用中的继承模式
1. 实现接口(Interface)
JavaScript 没有原生的接口概念,但可以通过原型继承模拟:
javascript
// 定义接口原型
const Drawable = {
draw() {
throw new Error("draw method must be implemented");
},
};
const Movable = {
move(x, y) {
throw new Error("move method must be implemented");
},
};
// 创建实现接口的对象
function Shape(x, y) {
this.x = x;
this.y = y;
}
// 混入接口
Object.assign(Shape.prototype, Drawable, Movable);
// 具体类
function Circle(x, y, radius) {
Shape.call(this, x, y);
this.radius = radius;
}
// 继承Shape并实现接口方法
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;
Circle.prototype.draw = function () {
console.log(
`Drawing circle at (${this.x}, ${this.y}) with radius ${this.radius}`
);
};
Circle.prototype.move = function (x, y) {
this.x += x;
this.y += y;
console.log(`Circle moved to (${this.x}, ${this.y})`);
};
const circle = new Circle(10, 10, 5);
circle.draw(); // Drawing circle at (10, 10) with radius 5
circle.move(5, 5); // Circle moved to (15, 15)2. 实现 Mixin 模式
Mixin 允许将多个对象的功能组合到一个对象中:
javascript
// 定义多个Mixin
const canEat = {
eat() {
console.log(`${this.name} is eating`);
},
};
const canWalk = {
walk() {
console.log(`${this.name} is walking`);
},
};
const canSwim = {
swim() {
console.log(`${this.name} is swimming`);
},
};
// 创建基础对象
function Animal(name) {
this.name = name;
}
// 应用Mixin
function applyMixins(constructor, mixins) {
mixins.forEach((mixin) => {
Object.assign(constructor.prototype, mixin);
});
}
// 不同的动物组合不同的能力
function Dog(name) {
Animal.call(this, name);
}
function Fish(name) {
Animal.call(this, name);
}
function Duck(name) {
Animal.call(this, name);
}
applyMixins(Dog, [canEat, canWalk]);
applyMixins(Fish, [canEat, canSwim]);
applyMixins(Duck, [canEat, canWalk, canSwim]);
const dog = new Dog("Buddy");
dog.eat(); // Buddy is eating
dog.walk(); // Buddy is walking
const fish = new Fish("Nemo");
fish.eat(); // Nemo is eating
fish.swim(); // Nemo is swimming
const duck = new Duck("Donald");
duck.eat(); // Donald is eating
duck.walk(); // Donald is walking
duck.swim(); // Donald is swimming3. 实现装饰器模式
使用原型继承来增强现有对象的功能:
javascript
// 基础组件
function Coffee() {
this.cost = 2;
this.description = "Black coffee";
}
Coffee.prototype.getDescription = function () {
return this.description;
};
Coffee.prototype.getCost = function () {
return this.cost;
};
// 装饰器基础
function CoffeeDecorator(coffee) {
this.coffee = coffee;
}
CoffeeDecorator.prototype.getDescription = function () {
return this.coffee.getDescription();
};
CoffeeDecorator.prototype.getCost = function () {
return this.coffee.getCost();
};
// 具体装饰器
function MilkDecorator(coffee) {
CoffeeDecorator.call(this, coffee);
this.cost = 0.5;
this.description = "milk";
}
MilkDecorator.prototype = Object.create(CoffeeDecorator.prototype);
MilkDecorator.prototype.constructor = MilkDecorator;
MilkDecorator.prototype.getDescription = function () {
return `${this.coffee.getDescription()}, ${this.description}`;
};
MilkDecorator.prototype.getCost = function () {
return this.coffee.getCost() + this.cost;
};
// 使用装饰器
const coffee = new Coffee();
const coffeeWithMilk = new MilkDecorator(coffee);
console.log(coffee.getDescription()); // Black coffee
console.log(coffee.getCost()); // 2
console.log(coffeeWithMilk.getDescription()); // Black coffee, milk
console.log(coffeeWithMilk.getCost()); // 2.5性能考虑和最佳实践
1. 避免过深的原型链
javascript
// ❌ 避免过深的原型链
function Level1() {}
function Level2() {}
function Level3() {}
function Level4() {}
function Level5() {}
Level2.prototype = Object.create(Level1.prototype);
Level3.prototype = Object.create(Level2.prototype);
Level4.prototype = Object.create(Level3.prototype);
Level5.prototype = Object.create(Level4.prototype);
// ✅ 使用组合而非深继承
class Level5 {
constructor(level1, level2, level3, level4) {
this.level1 = level1;
this.level2 = level2;
this.level3 = level3;
this.level4 = level4;
}
}2. 合理放置属性和方法
javascript
function Car(brand, model) {
// 实例独有的属性放在构造函数中
this.brand = brand;
this.model = model;
this.odometer = 0;
}
// 共享的方法放在原型上
Car.prototype.drive = function (distance) {
this.odometer += distance;
console.log(`${this.brand} ${this.model} drove ${distance} km`);
};
// 私有方法可以通过闭包实现
const CarFactory = (function () {
function validateBrand(brand) {
return typeof brand === "string" && brand.length > 0;
}
return function (brand, model) {
if (!validateBrand(brand)) {
throw new Error("Invalid brand");
}
this.brand = brand;
this.model = model;
};
})();3. 使用现代 Class 语法
虽然理解原型继承很重要,但在现代项目中,推荐使用 Class 语法:
javascript
// 现代的写法(底层仍然是原型继承)
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
console.log(`${this.name} barks: Woof!`);
}
fetch() {
console.log(`${this.name} is fetching the ball`);
}
}
const dog = new Dog("Buddy", "Golden Retriever");
dog.speak(); // Buddy barks: Woof!
dog.fetch(); // Buddy is fetching the ball总结
JavaScript 中的原型继承提供了灵活的对象间继承机制。了解不同的继承模式有助于我们在合适的场景选择合适的方案:
- 原型链继承:简单但有问题,适合简单场景
- 构造函数继承:解决了引用类型共享问题,但无法复用方法
- 组合继承:最常用,结合了前两者的优点
- 寄生组合式继承:最理想的继承范式
- Mixin 模式:适合多重继承场景
- 装饰器模式:适合动态扩展功能
虽然现代 JavaScript 提供了 Class 语法,但理解原型继承的底层机制对于深入掌握 JavaScript 仍然至关重要。在实际开发中,要根据具体需求选择最合适的继承模式。