JavaScript 中如何实现继承
在 JavaScript 中,继承是面向对象编程的重要特性之一。JavaScript 采用基于原型(prototype-based)的继承机制,而不是像 Java 或 C++ 那样使用类继承。理解 JavaScript 的继承机制对于写出高效、可维护的代码至关重要。本文将介绍几种常见的实现继承的方式,并分析它们的优缺点。
一、原型链继承(Prototype Chaining)
原理:JavaScript 的对象通过其内部的 [[Prototype]] 属性指向另一个对象(即原型),从而形成一个链式结构,这就是原型链。通过将子类的原型指向父类的一个实例,可以实现继承。
示例:
function Parent() {
this.parentName = "Parent";
}
Parent.prototype.sayHello = function() {
console.log("Hello from Parent");
};
function Child() {
this.childName = "Child";
}
// 继承 Parent
Child.prototype = new Parent();
// 修复 constructor 指向
Child.prototype.constructor = Child;
const child = new Child();
child.sayHello(); // Hello from Parent
console.log(child.parentName); // Parent
优点:简单直观、方法定义在原型上,可以被多个实例共享。
缺点:父类的引用类型属性会被所有子类实例共享,容易造成数据污染;创建子类实例时,不能向父类构造函数传参。
二、构造函数继承(Constructor Stealing)
原理:在子类构造函数中调用父类构造函数,使用 call() 或 apply() 方法改变父类构造函数的执行上下文,从而实现继承。
示例:
function Parent(name) {
this.name = name;
this.colors = ["red", "blue"];
}
function Child(name, age) {
Parent.call(this, name); // 继承属性
this.age = age;
}
const child1 = new Child("Alice", 10);
child1.colors.push("green");
const child2 = new Child("Bob", 12);
console.log(child1.colors); // ["red", "blue", "green"]
console.log(child2.colors); // ["red", "blue"]
优点:
- 可以向父类构造函数传参。
- 每个实例都有独立的属性副本,避免了引用类型共享的问题。
缺点:
- 父类原型上的方法无法被子类访问。
- 方法不能共享,每次创建实例都会重新定义一次方法,效率低。
三、组合继承(Combination Inheritance)
组合继承结合了原型链继承和构造函数继承的优点,通过原型链继承方法,通过构造函数继承属性。
示例:
function Parent(name) {
this.name = name;
this.colors = ["red", "blue"];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name); // 继承属性
this.age = age;
}
// 继承方法
Child.prototype = new Parent();
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
console.log(this.age);
};
const child = new Child("Alice", 10);
child.sayName(); // Alice
child.sayAge(); // 10
优点:可以传参、属性独立,方法共享。是最常用的继承方式之一。
缺点:父类构造函数会被调用两次(一次在 new Parent(),一次在 Parent.call(this)),效率略低。
四、寄生组合继承(Parasitic Combination Inheritance)
为了解决组合继承中父类构造函数被调用两次的问题,可以使用寄生组合继承。它通过借用 Object.create() 来创建父类原型的副本,避免重复调用构造函数。
示例:
function Parent(name) {
this.name = name;
this.colors = ["red", "blue"];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name); // 继承属性
this.age = age;
}
// 寄生式继承
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
console.log(this.age);
};
优点:高效,只调用一次父类构造函数。属性独立,方法共享。是目前最推荐的继承方式。
五、ES6 类继承(Class Inheritance)
ES6 引入了 class 关键字,使得继承语法更接近传统面向对象语言,但其底层依然是基于原型的实现。
示例:
class Parent {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类构造函数
this.age = age;
}
sayAge() {
console.log(this.age);
}
}
const child = new Child("Alice", 10);
child.sayName(); // Alice
child.sayAge(); // 10
优点:语法简洁,易于理解和使用;支持静态方法、getter/setter、模块化等新特性;是现代 JavaScript 的主流写法。
缺点:仅适用于支持 ES6 的环境(可通过 Babel 转译);对原型链底层原理的封装可能使初学者难以理解继承的本质。
总结
| 继承方式 | 是否共享方法 | 是否共享属性 | 构造函数传参 | 推荐程度 |
|---|---|---|---|---|
| 原型链继承 | ✅ | ❌(引用类型共享) | ❌ | ⭐⭐ |
| 构造函数继承 | ❌ | ✅ | ✅ | ⭐⭐⭐ |
| 组合继承 | ✅ | ✅ | ✅ | ⭐⭐⭐⭐ |
| 寄生组合继承 | ✅ | ✅ | ✅ | ⭐⭐⭐⭐⭐ |
| ES6 类继承 | ✅ | ✅ | ✅ | ⭐⭐⭐⭐⭐ |