再谈继承

在这里记录一下 JavaScript 实现继承的各种方法,直接参考自《JavaScript高级程序设计》这本书,红宝书写的实在是很好,涵盖了几乎基础和高级知识点。因此本文是算一篇笔记。

关于原型链实现继承我就不再赘述了,在前面的文章已经写过。

盗用构造函数

因为当原型中的属性为引用值时,其引用值会在所有实例间共享。如下:

//定义父类
function Base(){
  this.colors = ["red", "green", "blue"];
}
//定义子类
function Sub(){}
//原型链继承
Sub.prototype = new Base();
//创建子类实例1
let sub1 = new Sub();
sub1.colors.push("white"); // 修改colors
console.log(sub1.colors); // => "red", "green", "blue", "white"
//创建子类实例2
let sub2 = new Sub();
console.log(sub2.colors);// => "red", "green", "blue", "white" colors在各个实例中共享

盗用构造函数技术就是解决引用值导致的问题:在子类构造函数中调用父类构造函数。

//定义父类
function Base(){
  this.colors = ["red", "green", "blue"];
}
//定义子类 并在子类中调用父类构造函数
function Sub(){
  // 继承并传参
  Base.call(this,"args"); //绑定调用Base构造函数中的this为Sub的实例对象
}

//定义实例
let sub1 = new Sub();
sub1.colors.push("black");
console.log(sub1.colors); // "red","green","blue","black"

let sub2 = new Sub();
console.log(sub2.colors); // => "red","green","blue"

缺点:

  • 必须在构造函数中定义方法,因此函数不能复用

  • 子类不能访问父类原型上的方法

组合继承

综合了原型链和盗用构造函数,将两者的优点集中了起来。思路是盗用继承和原型链结合:

// 定义父类
function Base() {
  this.colors = ["red", "green", "blue"];
}
Base.prototype.sayHi = function() {console.log("Hi!")}; // 父类原型上的方法
// 定义子类
function Sub(name) {
  // 继承父类属性
  Base.call(this, "args"); // 第二次调用父类构造函数
  this.name = name;
}
// 原型链继承方法
Sub.prototype = new Base(); // 第一次调用父类构造函数
// 定义实例
let sub1 = new Sub("sub1");
sub1.sayHi(); // => "Hi!"
sub1.colors.pop();
console.log(sub1.colors); // => "red","green"

let sub2 = new Sub("sub2");
sub2.sayHi(); // => "Hi!"
console.log(sub2.colors); // => "red","green","blue"

组合继承弥补了原型链和盗用构造函数的不足,是 JavaScript 中使用最多的继承模式。而且组合继承也保留了 instanceof 操作符和 isPrototypeOf()方法识别合成对象的能力。

缺点:

  • 父类构造函数必须调用2次

com-extends.png

可以看到colors变量被存储了两次,分别在Sub的实例和Sub的原型对象上。

原型式继承

如下:

function object(o) {
  function F() {};
  F.prototype = o;
  return new F();
}
let person = {
  name: "",
  friends: ["李四", "王五", "老六"]
};
let anotherPerson = object(person);
anotherPerson.name = "老大";
anotherPerson.friends.push("田七");

let yetAnotherPerson = object(person);
yetAnotherPerson.name = "老二";
yetAnotherPerson.friends.push("老八");

console.log(person.friends); // "李四,王五,老六,田七,老八"

ECMAScript5中Object.create() 将原型式继承的概念规范化了。

缺点:

  • 属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。

寄生式继承

寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

function createAnother(original) {
  let clone = Object.create(original); // 生成继承original对象的clone对象
  clone.sayHi = function() {console.log("Hi!")}; // 增强对象
  return clone; // 返回该对象
}

缺点:

  • 寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。

寄生式组合继承

为了解决组合式继承的效率问题:父类构造函数始终会被调用两次。将寄生式和组合式结合起来。

实现继承如下:

function inheritPrototype(subType, superType) {
  let prototype = object(superType.prototype); // 创建(父类)对象
  prototype.constructor = subType; // 增强对象
  subType.prototype = prototype; // 赋值(父类)对象(原型链继承)
}

图解:

prarsitic-combination.png

完整示例:

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

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

function SubType(name, age) {
    SuperType.call(this, name); // 其实就是ES6中调用的super()
    //SubType.prototype.__proto__.constructor(name); 相当于这个
    // this.name = "李四";
    this.age = age;
}

inheritPrototype(SubType, SuperType);

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

let sub1 = new SubType("张三", 18);
sub1.sayName(); // => "张三"
sub1.sayAge(); // => 18
sub1.colors.push("black"); 
console.log(sub1.colors); // => 'red', 'green', 'blue', 'black'
console.log("------------------------------")
let sub2 = new SubType("李四", 21);
sub2.sayName(); // => "李四"
sub2.sayAge(); // => 21
console.log(sub2.colors); // => 'red', 'green', 'blue' 

Reference参考:

  • JavaScript高级程序设计