===
参考资料:《JavaScript 高级程序设计(第4版)》(红宝书)第 8 章 —— 对象、类与面向对象编程
作者:Nicholas C. Zakas
📌 前置知识
JavaScript 的继承基于原型链实现。每个对象都有一个内部属性 [[Prototype]],指向其原型对象。当访问一个属性时,引擎会沿原型链向上查找,直到找到或到达 null。
实例对象 → 构造函数.prototype → Object.prototype → null
一、原型链继承
原理
将子类的 prototype 指向父类的实例,从而继承父类的属性和方法。
代码示例
function SuperType() { this.property = true; this.colors = ['red', 'blue', 'green'];}SuperType.prototype.getSuperValue = function() { return this.property;};function SubType() { this.subproperty = false;}// 核心:将 SubType 的原型设置为 SuperType 的实例SubType.prototype = new SuperType();SubType.prototype.getSubValue = function() { return this.subproperty;};var instance1 = new SubType();var instance2 = new SubType();console.log(instance1.getSuperValue()); // true// ⚠️ 问题:引用类型属性被所有实例共享instance1.colors.push('black');console.log(instance1.colors); // ['red', 'blue', 'green', 'black']console.log(instance2.colors); // ['red', 'blue', 'green', 'black'] ← 被污染!
✅ 优点
- • 简单,能继承父类原型上的方法
❌ 缺点
- 1. 引用类型属性共享:父类实例上的引用类型属性(如数组)被所有子类实例共享
- 2. 无法向父类构造函数传参
二、借用构造函数继承(经典继承)
原理
在子类构造函数中,通过 call() 或 apply() 调用父类构造函数,将父类的属性复制到子类实例上。
代码示例
function SuperType(name) { this.name = name; this.colors = ['red', 'blue', 'green'];}SuperType.prototype.sayName = function() { console.log(this.name);};function SubType(name, age) { // 核心:借用父类构造函数 SuperType.call(this, name); // 继承 SuperType 的属性 this.age = age;}var instance1 = new SubType('Alice', 25);var instance2 = new SubType('Bob', 30);instance1.colors.push('black');console.log(instance1.colors); // ['red', 'blue', 'green', 'black']console.log(instance2.colors); // ['red', 'blue', 'green'] ← 互不影响 ✅console.log(instance1.name); // 'Alice'console.log(instance2.name); // 'Bob'// ⚠️ 问题:无法继承原型上的方法console.log(instance1.sayName); // undefined
✅ 优点
- 1. 解决了引用类型属性共享问题
- 2. 可以向父类构造函数传参
❌ 缺点
- 1. 无法继承父类原型上的方法
- 2. 方法都在构造函数中定义,每次创建实例都会重新创建函数,无法复用
三、组合继承(最常用 ⭐)
原理
结合原型链继承和借用构造函数继承:
- • 用借用构造函数继承实例属性(解决引用类型共享问题)
- • 用原型链继承原型方法(实现方法复用)
代码示例
function SuperType(name) { this.name = name; this.colors = ['red', 'blue', 'green'];}SuperType.prototype.sayName = function() { console.log(this.name);};function SubType(name, age) { // 第二次调用 SuperType():继承实例属性 SuperType.call(this, name); this.age = age;}// 第一次调用 SuperType():继承原型方法SubType.prototype = new SuperType();SubType.prototype.constructor = SubType; // 修正 constructor 指向SubType.prototype.sayAge = function() { console.log(this.age);};var instance1 = new SubType('Alice', 25);var instance2 = new SubType('Bob', 30);instance1.colors.push('black');console.log(instance1.colors); // ['red', 'blue', 'green', 'black']console.log(instance2.colors); // ['red', 'blue', 'green'] ✅instance1.sayName(); // 'Alice' ✅instance1.sayAge(); // 25 ✅instance2.sayName(); // 'Bob' ✅
✅ 优点
- 1. 解决了引用类型属性共享问题
- 2. 可以向父类传参
- 3. 可以继承父类原型上的方法
- 4. 是 ES5 中最常用的继承方式
❌ 缺点
- • 父类构造函数被调用两次(一次
call,一次new),子类原型上会有多余的父类实例属性
四、原型式继承
原理
道格拉斯·克罗克福德(Douglas Crockford)提出,不使用构造函数,直接基于已有对象创建新对象。ES5 将其规范化为 Object.create()。
代码示例
// 原始实现(Crockford 2006)function object(o) { function F() {} F.prototype = o; return new F();}// 等价于 ES5 的 Object.create()var person = { name: 'Alice', friends: ['Bob', 'Charlie']};var anotherPerson = object(person);// 或者:var anotherPerson = Object.create(person);anotherPerson.name = 'Dave';anotherPerson.friends.push('Eve');var yetAnotherPerson = object(person);yetAnotherPerson.name = 'Frank';yetAnotherPerson.friends.push('Grace');console.log(person.friends); // ['Bob', 'Charlie', 'Eve', 'Grace']// ⚠️ 引用类型属性仍然共享
✅ 优点
- • 不需要构造函数,适合简单的对象继承场景
❌ 缺点
- • 引用类型属性仍然共享(与原型链继承相同的问题)
- • 无法传参
五、寄生式继承
原理
在原型式继承的基础上,创建一个封装继承过程的函数,在函数内部增强对象,然后返回该对象。
代码示例
function createAnother(original) { var clone = Object.create(original); // 基于原型式继承创建新对象 // 增强对象:添加新方法 clone.sayHi = function() { console.log('Hi!'); }; return clone;}var person = { name: 'Alice', friends: ['Bob', 'Charlie']};var anotherPerson = createAnother(person);anotherPerson.sayHi(); // 'Hi!' ✅console.log(anotherPerson.name); // 'Alice' ✅
✅ 优点
- • 在原型式继承基础上可以增强对象
❌ 缺点
- • 方法在函数内部定义,每次创建实例都会重新创建函数,无法复用
- • 引用类型属性仍然共享
六、寄生组合式继承(最理想 ⭐⭐)
原理
解决组合继承中父类构造函数被调用两次的问题:
- • 用借用构造函数继承实例属性
- • 用寄生式继承来继承父类原型(不调用父类构造函数,直接复制原型)
代码示例
// 核心函数:寄生组合式继承的关键function inheritPrototype(subType, superType) { var prototype = Object.create(superType.prototype); // 创建父类原型的副本 prototype.constructor = subType; // 修正 constructor 指向 subType.prototype = prototype; // 赋值给子类原型}function SuperType(name) { this.name = name; this.colors = ['red', 'blue', 'green'];}SuperType.prototype.sayName = function() { console.log(this.name);};function SubType(name, age) { SuperType.call(this, name); // 只调用一次父类构造函数 ✅ this.age = age;}// 使用寄生组合式继承(替代 SubType.prototype = new SuperType())inheritPrototype(SubType, SuperType);SubType.prototype.sayAge = function() { console.log(this.age);};var instance1 = new SubType('Alice', 25);var instance2 = new SubType('Bob', 30);instance1.colors.push('black');console.log(instance1.colors); // ['red', 'blue', 'green', 'black']console.log(instance2.colors); // ['red', 'blue', 'green'] ✅instance1.sayName(); // 'Alice' ✅instance1.sayAge(); // 25 ✅// 验证原型链console.log(instance1 instanceof SubType); // trueconsole.log(instance1 instanceof SuperType); // true
✅ 优点
- 1. 父类构造函数只调用一次,效率更高
- 2. 原型链完整,
instanceof和isPrototypeOf()正常工作 - 3. 是引用类型继承的最佳模式(红宝书原话)
❌ 缺点
- • 实现稍复杂(但 ES6 的
class extends底层就是这个原理)
📊 六种继承方式对比
继承方式
引用类型共享
可传参
继承原型方法
父类调用次数
推荐度
原型链继承
❌ 共享
❌
✅
1次
⭐
借用构造函数
✅ 独立
✅
❌
1次
⭐
组合继承
✅ 独立
✅
✅
2次
⭐⭐⭐
原型式继承
❌ 共享
❌
✅
0次
⭐⭐
寄生式继承
❌ 共享
❌
✅
0次
⭐⭐
寄生组合式继承
✅ 独立
✅
✅
1次
⭐⭐⭐⭐⭐
🧠 记忆口诀
"原型链共享有缺陷,借用构造不继承方法;组合继承最常用,但调两次有浪费;寄生组合最完美,ES6 class 就是它。"
🎤 常见面试题
Q1:ES5 中最常用的继承方式是什么?
答:组合继承(原型链 + 借用构造函数)是最常用的,但最理想的是寄生组合式继承,因为它只调用一次父类构造函数,效率更高,也是红宝书推荐的最佳实践。
Q2:组合继承有什么缺点?如何解决?
答:组合继承会调用两次父类构造函数:
- 1.
SubType.prototype = new SuperType()— 第一次 - 2.
SuperType.call(this, ...)— 第二次
导致子类原型上存在多余的父类实例属性(被子类实例属性遮蔽)。
解决方案:使用寄生组合式继承,用 Object.create(SuperType.prototype) 替代 new SuperType(),避免第一次调用。
Q3:原型链继承的缺点是什么?
答:
- 1. 引用类型属性共享:父类实例上的引用类型(如数组、对象)会被所有子类实例共享,修改一个会影响所有实例
- 2. 无法向父类传参:在创建子类实例时,无法向父类构造函数传递参数
Q4:Object.create() 和 new 的区别?
// Object.create(proto):创建一个以 proto 为原型的新对象,不调用构造函数var obj1 = Object.create(SuperType.prototype);// obj1.__proto__ === SuperType.prototype,但 SuperType 构造函数未执行// new SuperType():创建实例,调用构造函数,执行初始化逻辑var obj2 = new SuperType();// obj2.__proto__ === SuperType.prototype,且 SuperType 构造函数已执行
Q5:ES6 的 class extends 和寄生组合式继承有什么关系?
答:ES6 的 class extends 在底层就是基于寄生组合式继承实现的,只是语法更简洁。两者的原型链结构完全相同。
// ES6 写法(等价于寄生组合式继承)class SuperType { constructor(name) { this.name = name; this.colors = ['red', 'blue', 'green']; } sayName() { console.log(this.name); }}class SubType extends SuperType { constructor(name, age) { super(name); // 等价于 SuperType.call(this, name) this.age = age; } sayAge() { console.log(this.age); }}
📚 相关知识点
- • [[原型与原型链]] - 理解继承的底层机制
- • [[构造函数与new操作符]] - new 的执行过程
- • [[Object方法]] - Object.create()、Object.getPrototypeOf() 等
- • [[ES6 Class]] - ES6 类语法与继承
- • [[delete操作符]] - 属性删除
创建时间:2026年3月
参考:《JavaScript高级程序设计(第4版)》第8章
标签:#JavaScript #ES5 #继承 #原型链 #面试题