JavaScript六种继承方式总结大全
作者:懒羊羊小可爱
1. 原型链继承
是什么?
这是最基础的继承方式。其核心是:让一个构造函数的 prototype 对象指向另一个构造函数的实例。这样,子类就能通过原型链访问到父类的属性和方法。
javascript
// 父类
function Parent() {
this.name = 'Parent';
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子类
function Child() {
this.type = 'Child';
}
// 关键!将Child的原型指向Parent的实例,建立原型链
Child.prototype = new Parent();
var child1 = new Child();
child1.sayName(); // 'Parent' (来自原型链)
console.log(child1.colors); // ['red', 'blue'] (来自原型链)
var child2 = new Child();
child1.colors.push('green'); // 修改child1的colors
console.log(child2.colors); // ['red', 'blue', 'green'] (问题出现了!)有什么作用?
实现属性和方法的继承。
实际开发运用场景?
在现代前端开发中,几乎不会单独使用原型链继承,因为它有致命缺陷。但它是一切其他继承方式的理论基础。
优点:
简单易懂,是理解JS继承的基础。
能够继承父类原型上的方法。
缺点:
引用类型属性被所有实例共享(如上例中的
colors数组)。一个实例修改了引用类型属性,所有实例都会受到影响。这是最大的问题。创建子类实例时,无法向父类构造函数传参(因为
new Parent()在初始化原型时就执行了)。
2. 借用构造函数继承(经典继承)
是什么?
为了解决原型链继承的缺点,这种方法的核心是:在子类构造函数的内部调用父类构造函数。这利用了 call() 或 apply() 方法,使父类的 this 指向子类的实例。
javascript
// 父类
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
// 注意:父类原型上的方法子类访问不到
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子类
function Child(name, type) {
// 关键!“借用”父类的构造函数来初始化属性
Parent.call(this, name); // 相当于执行了 this.name = name; this.colors = ['red', 'blue'];
this.type = type;
}
var child1 = new Child('小明', 'Child');
var child2 = new Child('小红', 'Child');
child1.colors.push('green');
console.log(child1.colors); // ['red', 'blue', 'green']
console.log(child2.colors); // ['red', 'blue'] (互不影响了!)
console.log(child1.sayName); // undefined (无法继承父类原型的方法)有什么作用?
解决原型链继承中“引用属性共享”和“无法传参”的问题。
实际开发运用场景?
通常不会单独使用,但它是组合继承的重要组成部分。
优点:
避免了引用属性共享的问题。
可以在子类中向父类传递参数。
缺点:
方法都在构造函数中定义,每次创建实例都会创建一遍方法,无法实现函数复用,效率低。
无法继承父类原型(prototype)上的方法(如上例中的
sayName)。
3. 组合继承
是什么?
组合继承结合了原型链继承和借用构造函数继承的优点,是 JavaScript 中最常用的继承模式。其核心是:
使用借用构造函数来继承属性(解决共享和传参问题)。
使用原型链来继承方法(实现方法复用)。
javascript
// 父类
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子类
function Child(name, type) {
// 1. 继承属性
Parent.call(this, name); // 第二次调用 Parent()
this.type = type;
}
// 2. 继承方法
Child.prototype = new Parent(); // 第一次调用 Parent()
// 修复构造函数指向,否则Child实例的constructor会指向Parent
Child.prototype.constructor = Child;
// 子类自己的方法
Child.prototype.sayType = function() {
console.log(this.type);
};
var child1 = new Child('小明', 'Child1');
child1.colors.push('green');
console.log(child1.colors); // ['red', 'blue', 'green']
child1.sayName(); // '小明'
child1.sayType(); // 'Child1'
var child2 = new Child('小红', 'Child2');
console.log(child2.colors); // ['red', 'blue'] (属性不共享)
child2.sayName(); // '小红' (方法可复用)有什么作用?
综合了两种模式的优点,成为 JavaScript 中一种非常实用的继承模式。
实际开发运用场景?
在 ES6 的 class 出现之前,这是最主流、最可靠的继承方式。常用于构建复杂的对象系统,如UI组件库、游戏引擎中的实体继承等。
优点:
实例拥有独立的属性,不会共享。
实例可以复用父类原型上的方法。
可以向父类构造函数传参。
缺点:
最大的缺点:会两次调用父类构造函数。
一次在
Parent.call(this)。一次在
new Parent()。
这导致子类实例和原型上存在两份相同的属性(一份在实例自身,一份在__proto__里),造成了一些不必要的浪费(虽然实例自身的属性会屏蔽原型上的属性,没有功能问题)。
4. 原型式继承
是什么?
道格拉斯·克罗克福德提出的方法。其核心是:创建一个临时的构造函数,将其原型指向某个对象,然后返回这个临时构造函数的实例。本质上是对传入的对象进行了一次浅复制。
javascript
// object() 就是 ES5 中 Object.create() 的模拟实现
function object(o) {
function F() {} // 创建一个临时构造函数
F.prototype = o; // 将其原型指向传入的对象o
return new F(); // 返回这个临时构造函数的实例
}
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court']
};
var person1 = object(person);
person1.name = 'Greg';
person1.friends.push('Rob');
var person2 = object(person);
person2.name = 'Linda';
person2.friends.push('Barbie');
console.log(person.friends); // ['Shelby', 'Court', 'Rob', 'Barbie'] (共享问题依然存在)有什么作用?
在不必兴师动众地创建构造函数的情况下,基于一个已有对象创建新对象。
实际开发运用场景?
适用于简单对象的浅拷贝继承。
ES5 的
Object.create()方法规范化了原型式继承。现在直接使用Object.create()即可。
优点:
无需创建构造函数,代码简洁。
缺点:
和原型链继承一样,存在引用属性共享的问题。
5. 寄生式继承
是什么?
寄生式继承的思路与寄生构造函数和工厂模式类似。其核心是:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式(如原型式继承)增强对象,最后返回这个对象。
javascript
function createAnother(original) {
var clone = object(original); // 1. 通过调用函数(如object)创建一个新对象(原型式继承)
clone.sayHi = function() { // 2. 以某种方式来增强这个对象
console.log('hi');
};
return clone; // 3. 返回这个对象
}
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court']
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // 'hi'有什么作用?
主要关注对象而不是自定义类型和构造函数,在主要考虑对象而非自定义类型和构造函数的情况下,实现简单的继承和扩展。
实际开发运用场景?
适用于为对象添加一些额外功能的场景,但不广泛使用。
优点:
可以在不创建构造函数的情况下,为对象添加函数。
缺点:
函数难以复用,效率低(跟借用构造函数模式一样)。
存在引用属性共享的问题(跟原型式继承一样)。
6. 寄生组合式继承
是什么?
这是组合继承的优化版本,也是目前公认的最理想的继承范式。它解决了组合继承调用两次父类构造函数的问题。
其核心是:
使用借用构造函数来继承属性。
使用寄生式继承来继承父类原型,并将其赋值给子类原型。
javascript
function inheritPrototype(child, parent) {
// 1. 创建父类原型的一个副本(原型式继承)
var prototype = Object.create(parent.prototype);
// 2. 修复副本的constructor指针
prototype.constructor = child;
// 3. 将子类的原型指向这个副本
child.prototype = prototype;
}
// 父类
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子类
function Child(name, type) {
// 只调用一次父类构造函数(继承属性)
Parent.call(this, name);
this.type = type;
}
// 关键!替换掉组合继承中的 `Child.prototype = new Parent()`
inheritPrototype(Child, Parent);
// 添加子类自己的方法
Child.prototype.sayType = function() {
console.log(this.type);
};
var child1 = new Child('小明', 'Child1');
// 实例的 __proto__ 指向 Child.prototype
// Child.prototype 的 __proto__ 指向 Parent.prototype
// 完美!有什么作用?
只调用一次父类构造函数,并且避免了在子类原型上创建不必要的、多余的属性。同时,原型链还能保持不变。
寄生组合式继承核心目标:修复“组合继承”的缺陷
要理解“寄生组合”,我们必须先回顾“组合继承”的问题。
组合继承的做法:
Parent.call(this):在子类构造函数里调用父类构造函数。这会在子类实例自身上创建一份父类的属性。Child.prototype = new Parent():将子类的原型指向一个父类的实例。这会在子类的原型对象上创建第二份父类的属性。
这就导致了:
子类实例上有
name和colors属性。子类实例的
__proto__(也就是Child.prototype)上也有name和colors属性。
虽然实例自身的属性会屏蔽掉原型上的同名属性,功能上没问题,但多创建了一份多余的属性,造成了内存浪费和不优雅。
寄生组合式继承的解决方案
它的核心思路非常巧妙:我们真的需要那个 new Parent() 实例来充当原型吗?不,我们只需要父类原型上的方法。
我们不需要 Parent 实例上的属性(因为我们已经通过 Parent.call(this) 在子类实例上得到了一份),我们只需要能通过原型链找到 Parent.prototype 上的方法。
所以,新的方案是:
继承属性:保持不变,依然在子类构造函数里用
Parent.call(this)在子类构造函数里调用父类构造函数,这会在子类实例自身上创建一份父类的属性。这保证了每个实例都有自己独立的属性。继承方法:不再用
new Parent(),而是直接创建一个纯净的、指向父类原型的对象,用它来作为子类的原型。
这个“纯净的、指向父类原型的对象”就是 Object.create(Parent.prototype) 所做的事情。
一步步拆解(超级详细版)
让我们把 inheritPrototype(Child, Parent) 这个函数里的三步拆开来看:
javascript
function inheritPrototype(child, parent) {
// 第一步:创建原型副本(核心)
var prototype = Object.create(parent.prototype);
// 第二步:修复constructor指向
prototype.constructor = child;
// 第三步:将子类的原型指向这个新创建的对象
child.prototype = prototype;
}第一步:var prototype = Object.create(parent.prototype);
Object.create()方法会创建一个新对象。这个新对象的
__proto__会指向你传入的参数,也就是parent.prototype。想象一下:这就好比我们凭空造了一个空对象
{},并且让这个空对象“认”Parent.prototype为它的爸爸(原型)。这个空对象自己没有任何属性(解决了属性重复的问题),但它可以顺着原型链找到 Parent.prototype 上的所有方法(sayName)。
第二步:prototype.constructor = child;
任何一个
prototype对象都有一个constructor属性,默认指向它关联的构造函数。因为我们用
Object.create()创建的新对象,它的constructor指向的是parent(因为它继承自parent.prototype,而parent.prototype.constructor指向parent)。这显然不对,我们希望子类原型的 constructor 指向子类自己 child。
所以我们需要手动纠正一下,让
prototype.constructor = child;。
第三步:child.prototype = prototype;
最后,我们把这个我们精心制作好的、纯净的、链接到了父类原型的、constructor指正确的
prototype对象,赋值给子类的prototype。从此,所有
new Child()出来的实例,它们的__proto__都指向我们这个prototype对象,从而可以顺利地通过原型链调用父类的方法。
终极比喻:“继承家产”的故事
父类 (
Parent):一个富豪老爹,他有金库(实例属性name,colors)和一本生意经(原型方法sayName)。组合继承:老爹先给你复制了一本完整的生意经(包括金库的地图),然后你又自己去金库里拿了一次金子。你手里有金子,书上也有金子的地图,地图是多余的。
寄生组合继承:一个聪明的律师(
Object.create)出现了。他没有复制整本生意经,而是只做了一张神奇的索引卡。这张索引卡本身是空白的(没有多余的属性),但它直接指向了老爹那本生意经的原始内容(Parent.prototype)。然后你又自己去金库里拿了一次金子。你手里有金子,也通过索引卡学会了老爹的生意经,完美!
总结:为什么它是最佳的?
| 特性 | 组合继承 | 寄生组合继承 | 优势 |
|---|---|---|---|
| 父类属性副本 | 2份 (实例上1份,原型上1份) | 1份 (仅在实例上) | 更高效,无浪费 |
| 父类方法继承 | 通过原型链继承 | 通过原型链继承 | 同样有效 |
| 父类构造函数调用 | 2次 | 1次 | 性能更优 |
| 原型链 | 保持正确 | 保持正确 | 同样正确 |
所以,寄生组合式继承的核心贡献就是:
它用一种极其巧妙的方式(Object.create)建立了子类和父类原型的直接联系,完全跳过了创建父类实例这个不必要的步骤,从而避免了创建多余的属性,完美地实现了继承。这也是为什么ES6的 class 和 extends 语法糖其底层实现原理是寄生组合继承的原因,因为它确实是理论上最完美的方案
实际开发运用场景?
这是实现基于构造函数的继承的最佳模式。在需要高度优化和避免不必要的内存开销的库或框架中可能会看到。但在日常开发中,我们更倾向于使用 ES6 的 class 和 extends,其底层原理就是寄生组合式继承。
优点:
只调用一次父类构造函数,效率高。
避免了在子类原型上创建不必要的属性。
原型链保持不变,能正常使用
instanceof和isPrototypeOf。
缺点:
实现起来相对复杂。但通常可以封装成一个函数(如上面的
inheritPrototype)来复用。
总结与建议
| 继承方式 | 核心思想 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 原型链继承 | 子类原型 = 父类实例 | 简单,方法可复用 | 引用共享,无法传参 | 基础学习,几乎不用 |
| 借用构造函数 | 在子类中 Parent.call(this) | 属性独立,可传参 | 方法不能复用 | 组合继承的一部分 |
| 组合继承 | 借用构造 + 原型链 | 属性独立,方法可复用,可传参 | 调用两次父类构造函数 | ES6前的主流方式 |
| 原型式继承 | Object.create() | 无需构造函数,简单 | 引用共享 | 对象浅拷贝 |
| 寄生式继承 | 工厂模式+增强对象 | 无需构造函数,可增强对象 | 方法不能复用,引用共享 | 为对象添加功能 |
| 寄生组合继承 | 借用构造 + 寄生式继承父类原型 | 近乎完美,只调用一次父类构造函数 | 实现稍复杂 | 理想的继承范式 |
现代开发建议:
直接使用 ES6 的 class 和 extends 关键字。它们的语法更清晰、更易于理解,并且其底层实现的就是寄生组合式继承这种最理想的方式。你不再需要手动处理原型链,避免了出错的可能。
javascript
// ES6 的写法,底层是寄生组合式继承
class Parent {
constructor(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
sayName() {
console.log(this.name);
}
}
class Child extends Parent {
constructor(name, type) {
super(name); // 相当于 Parent.call(this, name)
this.type = type;
}
sayType() {
console.log(this.type);
}
}到此这篇关于JavaScript六种继承方式的文章就介绍到这了,更多相关JS六种继承方式内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
