ES5 六种继承方式

3 阅读1分钟

===

参考资料:《JavaScript 高级程序设计(第4版)》(红宝书)第 8 章 —— 对象、类与面向对象编程
作者:Nicholas C. Zakas

📌 前置知识

JavaScript 的继承基于原型链实现。每个对象都有一个内部属性 [[Prototype]],指向其原型对象。当访问一个属性时,引擎会沿原型链向上查找,直到找到或到达 null

  实例对象 → 构造函数.prototypeObject.prototypenull

一、原型链继承

原理

将子类的 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. 1. 引用类型属性共享:父类实例上的引用类型属性(如数组)被所有子类实例共享
  2. 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. 1. 解决了引用类型属性共享问题
  2. 2. 可以向父类构造函数传参

❌ 缺点

  1. 1. 无法继承父类原型上的方法
  2. 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. 1. 解决了引用类型属性共享问题
  2. 2. 可以向父类传参
  3. 3. 可以继承父类原型上的方法
  4. 4. 是 ES5 中最常用的继承方式

❌ 缺点

  • 父类构造函数被调用两次(一次 call,一次 new),子类原型上会有多余的父类实例属性

四、原型式继承

原理

道格拉斯·克罗克福德(Douglas Crockford)提出,不使用构造函数,直接基于已有对象创建新对象。ES5 将其规范化为 Object.create()

代码示例

  // 原始实现(Crockford 2006function 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. 1. 父类构造函数只调用一次,效率更高
  2. 2. 原型链完整,instanceofisPrototypeOf() 正常工作
  3. 3. 是引用类型继承的最佳模式(红宝书原话)

❌ 缺点

  • • 实现稍复杂(但 ES6 的 class extends 底层就是这个原理)

📊 六种继承方式对比

继承方式

引用类型共享

可传参

继承原型方法

父类调用次数

推荐度

原型链继承

❌ 共享

1次

借用构造函数

✅ 独立

1次

组合继承

✅ 独立

2次

⭐⭐⭐

原型式继承

❌ 共享

0次

⭐⭐

寄生式继承

❌ 共享

0次

⭐⭐

寄生组合式继承

✅ 独立

1次

⭐⭐⭐⭐⭐

🧠 记忆口诀

"原型链共享有缺陷,借用构造不继承方法;组合继承最常用,但调两次有浪费;寄生组合最完美,ES6 class 就是它。"

🎤 常见面试题

Q1:ES5 中最常用的继承方式是什么?

组合继承(原型链 + 借用构造函数)是最常用的,但最理想的是寄生组合式继承,因为它只调用一次父类构造函数,效率更高,也是红宝书推荐的最佳实践。

Q2:组合继承有什么缺点?如何解决?

:组合继承会调用两次父类构造函数:

  1. 1. SubType.prototype = new SuperType() — 第一次
  2. 2. SuperType.call(this, ...) — 第二次

导致子类原型上存在多余的父类实例属性(被子类实例属性遮蔽)。

解决方案:使用寄生组合式继承,用 Object.create(SuperType.prototype) 替代 new SuperType(),避免第一次调用。

Q3:原型链继承的缺点是什么?

  1. 1. 引用类型属性共享:父类实例上的引用类型(如数组、对象)会被所有子类实例共享,修改一个会影响所有实例
  2. 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 #继承 #原型链 #面试题