ES5实现继承的多种方式详解

深入讲解ES5中实现继承的6种经典方式,包括原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承和寄生组合式继承

问题

在ES6的class语法出现之前,JavaScript通过原型链和构造函数来实现面向对象编程中的继承特性。本文将详细介绍ES5中实现继承的多种方式,分析每种方式的优缺点,帮助理解JavaScript继承的本质。

解答

1. 原型链继承

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

Parent.prototype.sayName = function() {
  console.log(this.name);
};

// 子类
function Child(age) {
  this.age = age;
}

// 实现继承:将父类的实例作为子类的原型
Child.prototype = new Parent('parent');
Child.prototype.constructor = Child;

Child.prototype.sayAge = function() {
  console.log(this.age);
};

优点: 简单易实现,可以继承父类原型上的方法

缺点:

  • 所有子类实例共享父类引用类型属性
  • 创建子类实例时无法向父类构造函数传参

2. 构造函数继承(借用构造函数)

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

Parent.prototype.sayName = function() {
  console.log(this.name);
};

// 子类
function Child(name, age) {
  // 调用父类构造函数
  Parent.call(this, name);
  this.age = age;
}

Child.prototype.sayAge = function() {
  console.log(this.age);
};

优点:

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

缺点:

  • 无法继承父类原型上的方法
  • 每次创建实例都会创建一遍方法

3. 组合继承(原型链 + 构造函数)

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

Parent.prototype.sayName = function() {
  console.log(this.name);
};

// 子类
function Child(name, age) {
  // 第二次调用Parent():继承实例属性
  Parent.call(this, name);
  this.age = age;
}

// 第一次调用Parent():继承原型方法
Child.prototype = new Parent();
Child.prototype.constructor = Child;

Child.prototype.sayAge = function() {
  console.log(this.age);
};

优点:

  • 融合了原型链继承和构造函数继承的优点
  • 既可以继承原型上的方法,又可以继承实例属性
  • 可以向父类传参,且不会共享引用属性

缺点: 调用了两次父类构造函数,造成性能浪费

4. 原型式继承

// 函数
function object(obj) {
  function F() {}
  F.prototype = obj;
  return new F();
}

// 父对象
var parent = {
  name: 'parent',
  colors: ['red', 'blue', 'green'],
  sayName: function() {
    console.log(this.name);
  }
};

// 创建子对象
var child1 = object(parent);
child1.name = 'child1';

var child2 = object(parent);
child2.name = 'child2';

说明: ES5的Object.create()方法就是原型式继承的实现

优点: 不需要创建构造函数,直接基于已有对象创建新对象

缺点: 引用类型属性会被所有实例共享

5. 寄生式继承

function createChild(original) {
  // 通过原型式继承创建新对象
  var clone = Object.create(original);
  
  // 增强对象,添加新方法
  clone.sayHi = function() {
    console.log('Hi');
  };
  
  return clone;
}

// 父对象
var parent = {
  name: 'parent',
  colors: ['red', 'blue', 'green']
};

// 创建子对象
var child = createChild(parent);

优点: 在原型式继承基础上增强对象

缺点:

  • 引用类型属性共享问题
  • 每次创建对象都会创建一遍方法,效率较低

6. 寄生组合式继承(最优方案)

// 函数:实现原型继承
function inheritPrototype(child, parent) {
  // 创建父类原型的副本
  var prototype = Object.create(parent.prototype);
  // 修正constructor指向
  prototype.constructor = child;
  // 将副本赋值给子类原型
  child.prototype = prototype;
}

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

Parent.prototype.sayName = function() {
  console.log(this.name);
};

// 子类
function Child(name, age) {
  // 继承实例属性
  Parent.call(this, name);
  this.age = age;
}

// 继承原型方法
inheritPrototype(Child, Parent);

Child.prototype.sayAge = function() {
  console.log(this.age);
};

优点:

  • 只调用一次父类构造函数,避免了组合继承的性能问题
  • 保持了原型链的完整性
  • 能够正常使用instanceof和isPrototypeOf()

这是ES5中实现继承的最优方案,也是ES6 class继承的底层实现原理

使用示例

// 使用寄生组合式继承
function inheritPrototype(child, parent) {
  var prototype = Object.create(parent.prototype);
  prototype.constructor = child;
  child.prototype = prototype;
}

// 定义父类
function Animal(name) {
  this.name = name;
  this.energy = 100;
}

Animal.prototype.eat = function() {
  console.log(this.name + ' is eating');
  this.energy += 10;
};

Animal.prototype.sleep = function() {
  console.log(this.name + ' is sleeping');
  this.energy += 20;
};

// 定义子类
function Dog(name, breed) {
  Animal.call(this, name); // 继承属性
  this.breed = breed;
}

inheritPrototype(Dog, Animal); // 继承方法

Dog.prototype.bark = function() {
  console.log(this.name + ' is barking: Woof!');
  this.energy -= 5;
};

// 创建实例
var dog1 = new Dog('旺财', '哈士奇');
var dog2 = new Dog('小黑', '拉布拉多');

dog1.eat();        // 旺财 is eating
dog1.bark();       // 旺财 is barking: Woof!
console.log(dog1.energy); // 105

dog2.sleep();      // 小黑 is sleeping
console.log(dog2.energy); // 120

// 验证继承关系
console.log(dog1 instanceof Dog);    // true
console.log(dog1 instanceof Animal); // true
console.log(Dog.prototype.isPrototypeOf(dog1)); // true
console.log(Animal.prototype.isPrototypeOf(dog1)); // true

关键点

  • 原型链是JavaScript继承的机制:通过__proto__prototype建立对象之间的继承关系

  • 构造函数继承解决属性继承问题:使用call()apply()在子类构造函数中调用父类构造函数

  • 原型继承解决方法继承问题:通过设置Child.prototype来继承父类原型上的方法

  • 寄生组合式继承是最优方案:结合构造函数继承和原型继承的优点,避免了重复调用父类构造函数

  • 必须修正constructor指向:继承后需要将Child.prototype.constructor指向Child本身

  • Object.create()的作用:创建一个新对象,使用现有对象作为新对象的__proto__,避免直接使用new Parent()

  • 理解继承的本质:JavaScript的继承是通过原型链实现的委托机制,而不是传统面向对象语言的类继承

  • ES6 class的本质:ES6的class语法只是语法糖,底层仍然是基于原型链和构造函数实现的寄生组合式继承