JavaScript继承的六种方式及优缺点对比
深入解析JavaScript实现继承的六种经典模式:原型链继承、借用构造函数、组合继承、原型式继承、寄生式继承以及寄生组合式继承。通过代码示例、原型内存图解及优缺点全方位对比,理清JS面向对象演进逻辑。
假设你正在封装一套通用的业务模型:基类 User 拥有姓名、ID以及权限校验方法,而子类 Admin 需要在此基础上扩展超级管理权限。在 ES6 出现 class 和 extends 之前,JavaScript 是通过原型链和构造函数来模拟面向对象继承特性的。即使在现代前端开发中,ES6 的类继承本质上依然是基于寄生组合式继承的语法糖。本文将依次剖析经典的六种继承实现方案,理清各方案背后的内存机制、演进动力与缺陷权衡。
一、原型链继承 (Prototype Chain)
原型链继承是最直观的继承思路:将子类的原型对象直接赋值为父类的一个新实例。如此一来,子类实例在自身查找不到某些属性或方法时,就会沿着隐式原型链 __proto__ 向父类实例以及父类原型检索。
1. 实现代码
function Parent() {
this.name = 'Parent';
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function() {
return this.name;
};
function Child() {
this.type = 'Child';
}
// 核心:子类原型指向父类实例
Child.prototype = new Parent();
// 修正 constructor 指向
Child.prototype.constructor = Child;
const c1 = new Child();
const c2 = new Child();
c1.colors.push('black');
console.log(c2.colors); // ['red', 'blue', 'green', 'black'] (受到污染)
2. 优缺点对比
✔ 优点
- 简单直接:符合原型链直觉,代码实现只有一行赋值。
- 方法复用:父类原型上定义的方法能被所有子类实例复用,不会重复创建函数。
✖ 缺点
- 引用属性共享篡改:父类构造函数中的引用属性变成了子类原型属性,任意一个实例修改该引用,所有实例都会受到影响。
- 无法传参:在实例化子类时,无法在不影响其他实例的前提下向父类构造函数传递个性化参数。
二、借用构造函数继承 (Constructor Stealing)
为了解决原型链继承中“引用值共享”以及“无法向父类传参”的两大痛点,开发者提出了借用构造函数(也称经典继承)。核心思想是在子类构造函数内部通过 call() 或 apply() 调用父类构造函数。
1. 实现代码
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
function Child(name, age) {
// 核心:在子类上下文中借调父类构造函数
Parent.call(this, name);
this.age = age;
}
const c1 = new Child('Tom', 18);
const c2 = new Child('Jerry', 20);
c1.colors.push('black');
console.log(c1.colors); // ['red', 'blue', 'green', 'black']
console.log(c2.colors); // ['red', 'blue', 'green'] (相互独立,互不影响)
2. 优缺点对比
✔ 优点
- 独立属性:避免了引用类型属性被所有实例共享的问题,每个实例都有一份属于自己的独立拷贝。
- 支持传参:子类可以在
Parent.call(this, ...args)中灵活传递参数给父类构造函数。
✖ 缺点
- 函数无法复用:所有方法都必须在构造函数中定义,每次
new Child()都会重复开辟内存创建相同的函数对象。 - 丢失父类原型方法:子类实例与父类原型之间没有建立链接,无法访问
Parent.prototype上定义的方法。
三、组合继承 (Combination Inheritance)
组合继承(又称伪经典继承)结合了原型链继承与借用构造函数的各自长处:利用借用构造函数来继承实例私有属性并支持传参,再通过将子类原型重写为父类实例来继承原型上的共享方法。
1. 实现代码
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.sayName = function() {
console.log('My name is ' + 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('Age: ' + this.age);
};
const c1 = new Child('Alice', 22);
c1.colors.push('purple');
console.log(c1.colors); // ['red', 'blue', 'green', 'purple']
const c2 = new Child('Bob', 24);
console.log(c2.colors); // ['red', 'blue', 'green'] (未受污染,且可调用 sayName)
2. 优缺点对比
✔ 优点
- 属性独立且方法共享:既能保证实例拥有独立的属性与引用副本,又能让方法挂载在原型上实现内存复用。
- 类型识别完整:
instanceof和isPrototypeOf()能正确识别子类及父类类型。
✖ 缺点
- 性能开销:父类构造函数会被调用两次(一次在原型赋值
new Parent(),一次在实例创建Parent.call(this))。 - 属性冗余与屏蔽:子类原型对象上存在一份冗余的父类属性(被子类实例自身的同名属性屏蔽),造成轻微的内存浪费。
四、原型式继承 (Prototypal Inheritance)
由前端大师 Douglas Crockford 提出。如果你的目标不是构建复杂的类和类型层次,而仅仅是希望基于一个现有对象派生出一个结构相似的新对象,原型式继承便是最轻量的选择。ECMAScript 5 的 Object.create() 正是该模式的官方标准化实现。
1. 实现代码
// Crockford 的原始实现
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
// 使用场景
const person = {
name: 'Default',
friends: ['Shelby', 'Court']
};
const anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');
const yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');
console.log(person.friends); // ['Shelby', 'Court', 'Rob', 'Barbie'] (浅拷贝共享)
2. 优缺点对比
✔ 优点
- 极度轻量:无需自定义繁琐的构造函数,适合快速在已有对象基础上实现浅层次的原型委托。
- 语言内置原生支持:
Object.create()原生 API 性能优秀且语法简洁。
✖ 缺点
- 引用值依然共享:与原型链继承一样,原型对象中的引用类型属性仍然在所有派生对象之间共享。
- 无法像类那样定制构造过程:缺乏构造函数参数校验和独立的内部状态初始化环节。
五、寄生式继承 (Parasitic Inheritance)
寄生式继承与原型式继承紧密相连。其核心策略是:创建一个仅用于封装继承过程的工厂函数,在函数内部使用原型式继承克隆对象,然后直接对该对象进行属性或方法“增强”,最后将其返回。
1. 实现代码
function createAnother(original) {
// 通过任意可以返回新对象的函数克隆一个对象
const clone = Object.create(original);
// 寄生扩展:为新对象挂载专属方法(增强对象)
clone.sayHi = function() {
console.log('Hi, I am ' + this.name);
};
return clone;
}
const user = {
name: 'Kobe',
hobbies: ['basketball']
};
const athlete = createAnother(user);
athlete.sayHi(); // "Hi, I am Kobe"
2. 优缺点对比
✔ 优点
- 封装性良好:将对象的克隆与功能增强统一封装在工厂函数中,使用便利。
- 适合纯对象模式:针对无需构造函数的场景,能够灵活追加业务特性。
✖ 缺点
- 方法无法复用:每次调用
createAnother()为对象添加的方法都是独立创建的闭包函数,降低了内存利用率。 - 引用属性共享隐患:底层基于原型式继承,原型上的引用属性依然存在跨对象污染风险。
六、寄生组合式继承 (Parasitic Combination Inheritance)
前面提到,组合继承几乎是理想的继承模式,唯一的瑕疵在于:为了让子类原型继承父类原型上的方法,执行了一次多余的 Child.prototype = new Parent(),不仅造成了多余的构造函数开销,还在 Child.prototype 上挂载了一组无用的实例属性。
寄生组合式继承的核心突破在于:我们根本不需要为了继承父类原型而实例化父类。我们只需要一个干净的、以父类原型为原型的中间空对象即可!这被公认为 ES6 之前最完美、最成熟的继承模式,也是 Babel 将 ES6 class ... extends 转译为 ES5 时的底层实现方案。
1. 标准实现代码
// 核心工具函数:继承原型的标准化步骤
function inheritPrototype(subType, superType) {
// 1. 创建父类原型的一个纯净副本(无多余自身属性)
const prototype = Object.create(superType.prototype);
// 2. 增强对象:修正由于重写原型导致丢失的 constructor 指向
prototype.constructor = subType;
// 3. 将新创建的副本赋值给子类的原型
subType.prototype = prototype;
}
// 父类
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.sayName = function() {
console.log('Parent Name: ' + this.name);
};
// 子类
function Child(name, age) {
// 仅在子类实例化时调用一次父类构造函数
Parent.call(this, name);
this.age = age;
}
// 核心调用:建立原型链接
inheritPrototype(Child, Parent);
Child.prototype.sayAge = function() {
console.log('Child Age: ' + this.age);
};
// 验证
const c1 = new Child('Jack', 19);
c1.colors.push('yellow');
console.log(c1.colors); // ['red', 'blue', 'green', 'yellow']
const c2 = new Child('Rose', 18);
console.log(c2.colors); // ['red', 'blue', 'green'] (独立私有)
console.log(c1 instanceof Child); // true
console.log(c1 instanceof Parent); // true
2. 优缺点对比
✔ 优点 (最优解)
- 只调用一次父类构造函数:极大优化了运行期性能与内存占用。
- 避免原型冗余属性:子类原型是干净的空对象桥接,不会出现重复同名属性的遮蔽。
- 原型链保持完整:类型检测完全兼容
instanceof和isPrototypeOf。
✖ 缺点
- 实现复杂度略高:需要额外的辅助函数
inheritPrototype或显式处理原型对象的桥接与constructor修复。
七、六种继承方式综合对比与选型维度
为了让不同场景下的技术选型更加清晰,下表从多个工程关键维度对上述六种方案进行了全方位的横向对比:
| 继承方式 | 属性独立性 | 方法复用度 | 向父类传参 | 父构造调用次数 | 原型冗余属性 | 推荐程度 |
|---|---|---|---|---|---|---|
| 原型链继承 | 差 (引用共享) | 支持 (原型共享) | 不支持 | 1 次 | 无 | 不推荐 |
| 借用构造函数 | 优 (实例独享) | 无法复用 | 支持 | 1 次 | 无 | 不推荐 |
| 组合继承 | 优 (实例独享) | 支持 (原型共享) | 支持 | 2 次 (开销) | 存在冗余 | 可用 (经典) |
| 原型式继承 | 差 (引用共享) | 支持 | 无构造概念 | 0 次 | 无 | 特定轻量场景 |
| 寄生式继承 | 差 (引用共享) | 无法复用 | 无构造概念 | 0 次 | 无 | 较少使用 |
| 寄生组合式继承 | 优 (实例独享) | 支持 (原型共享) | 支持 | 仅 1 次 | 无冗余 | 最强推荐 (标准) |
八、总结与现代实践
回顾 JavaScript 继承的演化史,本质上是社区围绕“属性隔离”、“方法复用”以及“调用成本最小化”三大目标反复平衡的过程:
- 原型链继承 实现了方法复用,却栽在引用属性共享与传参缺失上;
- 借用构造函数 实现了独立属性与个性化传参,却丢掉了原型方法的共享优势;
- 组合继承 兼收并蓄,但留下了“执行两次父构造”与“原型冗余属性遮蔽”的瑕疵;
- 原型式与寄生式 开辟了对象浅拷贝与轻量增强的便捷路径;
- 寄生组合式继承 以简洁巧妙的空对象桥接(
Object.create)消除了组合继承的最后一道性能瓶颈,成为当之无愧的典范。
在日常业务开发中,我们应当优先采用语法清晰、内建完备的 class extends;而在设计大型框架插件架构、深入排查原型链遮蔽 Bug 或编写底层代码时,深刻掌握寄生组合继承的思想将成为你的核心技术护城河。
JavaScript入门书籍避坑:这些书不适合初学者
2026-11-05 16:31:15
JS使用Math.round实现保留两位小数的原理与代码
2026-11-05 15:35:55
JavaScript 原生 Date 对象怎么实现日期加减
2026-11-05 14:30:00
JavaScript函数定义的三种方式:声明、表达式与箭头函数
2026-11-05 13:50:55
JavaScript中escape、encodeURI和encodeURIComponent对空格的处理差异
2026-11-05 12:47:23
相关标签:







![JS输出对象显示[object Object]怎么办?JSON.stringify格式化打印技巧](https://img.javascript.cn/tit/articles/13236/20261105/6eaec9baed78171fe0bc2b74184ba5b7b80ebd52de7d3323ff152268aa2df294.webp)
