解析JavaScript继承方式:从原型链到寄生组合

15 阅读6分钟

JavaScript 常见继承方式详解

JavaScript 作为一门基于原型的语言,其继承机制与传统的基于类的语言有所不同。本文将详细讲解 JavaScript 中六种常见的继承方式,包括它们的实现原理、优缺点以及适用场景。

1. 原型链继承

原型链继承是 JavaScript 中最基本的继承方式,它利用原型链的特性实现继承。

实现原理

javascript

function Parent() {
  this.name = 'parent';
  this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function() {
  return this.name;
};

function Child() {
  this.type = 'child';
}

// 关键步骤:将 Child 的原型指向 Parent 的实例
Child.prototype = new Parent();

const child1 = new Child();
console.log(child1.getName()); // 输出: parent

底层机制

  1. 当访问一个对象的属性时,JavaScript 引擎会首先在对象自身查找
  2. 如果没有找到,则会沿着 __proto__ 指针向上查找
  3. 通过将 Child.prototype 设置为 Parent 的实例,建立了原型链
  4. 这样 Child 实例就可以访问 Parent 实例及其原型上的属性和方法

优缺点

优点:

  • 简单易实现
  • 父类新增原型方法/属性,子类都能访问到

缺点:

  • 所有子类实例共享父类实例的属性,如果属性是引用类型,一个子类实例修改会影响其他实例
  • 创建子类实例时,无法向父类构造函数传参

2. 构造函数继承(借助 call)

构造函数继承通过在子类构造函数中调用父类构造函数来实现继承。

实现原理

javascript

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function() {
  return this.name;
};

function Child(name, age) {
  // 关键步骤:在子类构造函数中调用父类构造函数
  Parent.call(this, name);
  this.age = age;
}

const child1 = new Child('Tom', 18);
console.log(child1.name); // 输出: Tom
console.log(child1.getName); // 输出: undefined

底层机制

  1. 使用 call 或 apply 方法在子类构造函数中调用父类构造函数
  2. 这样父类构造函数中的 this 指向的是子类实例
  3. 父类实例属性会被复制到子类实例上
  4. 但父类原型上的方法不会被继承

优缺点

优点:

  • 避免了引用类型属性被所有实例共享
  • 可以在子类中向父类传递参数

缺点:

  • 方法都在构造函数中定义,每次创建实例都会创建一遍方法
  • 不能继承父类原型上的属性和方法

3. 组合继承

组合继承结合了原型链继承和构造函数继承的优点,是 JavaScript 中最常用的继承模式。

实现原理

javascript

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function() {
  return this.name;
};

function Child(name, age) {
  // 第二次调用 Parent()
  Parent.call(this, name);
  this.age = age;
}

// 第一次调用 Parent()
Child.prototype = new Parent();
// 手动挂上构造器,指向自己的构造函数
Child.prototype.constructor = Child;

const child1 = new Child('Tom', 18);
console.log(child1.getName()); // 输出: Tom

底层机制

  1. 使用构造函数继承父类的实例属性
  2. 使用原型链继承父类原型上的方法和属性
  3. 通过这种方式,既可以让每个实例拥有自己的属性,又可以共享方法

优缺点

优点:

  • 融合了原型链继承和构造函数继承的优点
  • 可以继承实例属性/方法,也可以继承原型属性/方法
  • 不存在引用属性共享问题
  • 可传参

缺点:

  • 调用了两次父类构造函数,生成了两份实例(子类实例和子类原型各有一份)

4. 原型式继承

原型式继承基于已有对象创建新对象,是 ES5 Object.create() 的规范化实现。

实现原理

javascript

function createObj(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

const parent = {
  name: 'parent',
  colors: ['red', 'blue', 'green'],
  getName: function() {
    return this.name;
  }
};

const child1 = createObj(parent);
console.log(child1.getName()); // 输出: parent

底层机制

  1. 创建一个临时构造函数
  2. 将传入的对象作为这个构造函数的原型
  3. 返回这个临时类型的新实例
  4. 本质上是对传入对象进行了一次浅复制

优缺点

优点:

  • 简单,不需要单独创建构造函数
  • 适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场景

缺点:

  • 包含引用类型的属性值始终会共享相应的值
  • 无法实现代码复用(新实例属性都是后面添加的)

5. 寄生式继承

寄生式继承是在原型式继承的基础上增强对象,返回新对象。

实现原理

javascript

function createAnother(original) {
  const clone = Object.create(original); // 通过调用函数创建一个新对象
  clone.sayHi = function() { // 以某种方式增强这个对象
    console.log('hi');
  };
  return clone; // 返回这个对象
}

const parent = {
  name: 'parent',
  colors: ['red', 'blue', 'green'],
  getName: function() {
    return this.name;
  }
};

const child1 = createAnother(parent);
child1.sayHi(); // 输出: hi

底层机制

  1. 创建一个基于原对象的对象
  2. 增强这个对象(添加新方法/属性)
  3. 返回增强后的对象

优缺点

优点:

  • 可以在不修改原对象的情况下增强对象
  • 适合主要关注对象而不是自定义类型和构造函数的场景

缺点:

  • 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率
  • 同原型式继承一样,引用类型属性会被共享

6. 寄生组合式继承

寄生组合式继承是目前最理想的继承方式,它通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

实现原理

javascript

function inheritPrototype(child, parent) {
  const prototype = Object.create(parent.prototype); // 创建对象
  prototype.constructor = child; // 增强对象
  child.prototype = prototype; // 指定对象
}

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function() {
  return this.name;
};

function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}

// 关键步骤
inheritPrototype(Child, Parent);

const child1 = new Child('Tom', 18);
console.log(child1.getName()); // 输出: Tom

底层机制

  1. 使用构造函数继承父类的实例属性
  2. 使用寄生式继承来继承父类的原型
  3. 通过创建一个父类原型的副本并将其赋值给子类原型来实现
  4. 避免了组合继承中调用两次父类构造函数的问题

优缺点

优点:

  • 只调用一次父类构造函数
  • 避免了在子类原型上创建不必要的、多余的属性
  • 原型链保持不变
  • 能够正常使用 instanceof 和 isPrototypeOf

缺点:

  • 实现相对复杂

总结

JavaScript 的继承方式各有特点:

  1. 原型链继承:简单但引用属性共享
  2. 构造函数继承:解决属性共享问题但无法继承原型方法
  3. 组合继承:常用但会调用两次父类构造函数
  4. 原型式继承:适合简单对象继承
  5. 寄生式继承:增强对象但不适合代码复用
  6. 寄生组合式继承:最理想的继承范式

在实际开发中,寄生组合式继承是最为推荐的方式,它避免了组合继承的缺点,同时保持了继承的完整性和高效性。ES6 的 class 继承语法在底层也是使用类似寄生组合式继承的方式实现的。