JavaScript 中的继承:ES三、ES5 和 ES6

选择一种继承方式

JavaScript 是一门动态语言,动态意味着高灵活性,而这尤为能够体如今继承上面。JavaScript 中的继承有不少种实现方式,能够分红下面四类:javascript

  1. Mixin 模式,即属性混入,从一个或多个对象中复制属性到新的对象中
  2. 方法借用模式,即经过 call 或 apply 实现方法的重用
  3. 原型模式,使用 Object.create 方法直接以一个对象为原型创造新的对象
  4. 类模式,其实是使用构造函数或 ES6 class

前三种有一个共同点,就是没有“类”的概念,它们在适当的场景下很是有用,不过也由于没有类,缺失了不少经典面向对象继承的要素。例如父子对象之间没有严格的传承关系,即不必定是 is-a 的关系,这决定了没法将它们直接应用在面向对象分析与设计方面,能够说它们并非真正的继承,而是介于继承和组合之间的代码复用方案。java

而第四种,类式继承,不管是使用构造函数仍是 ES6 加入的 class,都能表达明确的继承关系,在须要对继承重度使用的场景下,应该使用类式继承。接下来,本文讨论的都是类式继承。git

有一点须要牢记:继承是一种强耦合,应该谨慎使用。es6

如何学习 JavaScript 中的继承

理解 JavaScript 里面的类继承实现方式,我认为最好的方法是——找一门面向对象机制更为完善的语言,去理解其中的继承。实际上,JavaScript 中之前就有的 new 和 ES6 加入的 class,都是参考自 Java 语言。github

不过,这样的对照学习是有前提条件的,即首先掌握 JavaScript 中的原型、原型链和做用域,不然很容易误解 JavaScript 本质的执行机制。若是已经理解了这些前置知识,就能够探索一下 JavaScript 中的继承了。markdown

用 ES3 实现继承

实现要点:数据结构

  1. 利用 Person.call(this) 执行“方法借用”,获取 Person 的属性
  2. 利用一个空函数将 Person.prototype 加入原型链
function Person(name) {
  this.name = name;
}

Person.prototype.printName = function() {
  console.log(this.name);
};

function Bob() {
  Person.call(this, "Bob");
  this.hobby = "Histroy";
}

function inheritProto(Parent, Child) {
  var Fn = function() {};
  Fn.prototype = Parent.prototype;
  Child.prototype = new Fn();
  Child.prototype.constructor = Child;
}

inheritProto(Person, Bob);

Bob.prototype.printHobby = function() {
  console.log(this.hobby);
};

console.dir(new Bob());
复制代码

dir 输出:app

Bob
  |-- hobby:"Histroy"
  |-- name:"Bob"
  |-- __proto__:Person
      |-- printHobby:ƒ ()
      |-- constructor:ƒ Bob()
      |-- __proto__:
          |-- printName:ƒ ()
          |-- constructor:ƒ Person(name)
          |-- __proto__:Object
复制代码

用 ES5 实现继承

实现要点:函数

  1. 利用 Person.call(this) 执行“方法借用”,获取 Person 的属性
  2. 利用 ES5 增长的 Object.create 方法将 Person.prototype 加入原型链
function Person(name) {
  this.name = name;
}

Person.prototype.printName = function() {
  console.log(this.name);
};

function Bob() {
  Person.call(this, "Bob");
  this.hobby = "Histroy";
}

Bob.prototype  = Object.create(Person.prototype, {
  constructor: {
    value: Bob,
    enumerable: false,
    configurable: true,
    writable: true
  }
});

Bob.prototype.printHobby = function() {
  console.log(this.hobby);
};

console.dir(new Bob());
复制代码

dir 输出:学习

Bob
  |-- hobby:"Histroy"
  |-- name:"Bob"
  |-- __proto__:Person
      |-- printHobby:ƒ ()
      |-- constructor:ƒ Bob()
      |-- __proto__:
          |-- printName:ƒ ()
          |-- constructor:ƒ Person(name)
          |-- __proto__:Object
复制代码

用 ES6 实现继承

实现要点:

  1. 利用 ES6 增长的 class 和 extends 实现比之前更完善的继承
class Person {
  constructor(name) {
    this.name = name;
  }

  printName() {
    console.log(this.name);
  }
}

class Bob extends Person {
  constructor() {
    super("Bob");
    this.hobby = "Histroy";
  }

  printHobby() {
    console.log(this.hobby);
  }
}

console.dir(new Bob());
复制代码

dir 输出:

Bob
  |-- hobby:"Histroy"
  |-- name:"Bob"
  |-- __proto__:Person
      |-- constructor:class Bob
      |-- printHobby:ƒ printHobby()
      |-- __proto__:
          |-- constructor:class Person
          |-- printName:ƒ printName()
          |-- __proto__:Object
复制代码

从 class 和 super 看 JavaScript 与 Java 的继承

编写代码时,ES6 class 带来的最明显的两个便利是:

  1. 隐藏原型链的拼接过程,将代码的重点放在类型之间的传承
  2. 使用 super 来实现更简化、更灵活的多态方法

实际上,ES6 围绕 class 增长了不少新功能,好比继承这件事情上,与以前不一样的是:用 class 实现的继承,既包括类实例的继承关系,也包括类自己的继承关系。这里的类实际上是特殊的 JavaScript 函数,而在 JavaScript 中,函数是对象的子类型,即函数对象,因此也可以体现出原型继承。

例如,用前面的代码来讲明就是:

// 类实例的继承关系
Bob.prototype.__proto__ === Person.prototype // true

// 类自己的继承关系
Bob.__proto__ === Person // true
复制代码

再来看 ES6 中的 super,子类的方法想借助父类的方法完成一部分工做时,super 就能够派上用场了,这是比继承更为细粒度的代码复用,不过耦合性也也变得更强了。实际上 super 也有不少功能,既能够看成函数使用,也能够看成对象使用。将 class 和 super 结合起来看,就能够领会一下 JavaScript 与 Java 在继承上的异同了。

与 Java 相同或很是相似的是:

  • 在子类构造方法中调用父类的构造方法。ES6 中,子类的构造器中必须调用父类的构造器来完成初始化,子类的实例是基于父类实例的加工。正是所以,父类的全部行为均可以继承。因此,ES6 中能够继承原生数据结构的完整功能,在此基础上定义本身的数据结构。就像 Java 中继承 HashMap 类,JavaScript 能够继承 Number、Array 等构造函数。

与 Java 不一样的是:

  • 在普通方法中,super 能够调用的是父类的原型对象上的方法(能够理解为 super 此时指向父类的原型对象);在静态方法中,super 能够调用父类的静态方法(能够理解为 super 此时指向父类)。而在 Java 中,经过 super 能够访问父类中被覆盖的同名变量或者方法,要访问静态方法则是经过“类名.方法名”或“对象名.方法名”。

比较后可见,真的是和 Java 很是相似。

结合前面的内容,能够发现从 ES3 到 ES6,JavaScript 中的面向对象部分一直是在向 Java 靠拢的。尤为增长了 class 和 extends 关键字以后,靠拢了一大步。但这些并无改变 JavaScript 是基于原型这一实质。Java 中的类就像对象的设计图,每次调用 new 建立一个新的对象,就产生一个独立的对象占用独立的内存空间;而在 JavaScript,继承所作工做其实是在构造原型链,全部子类的实例共享的是同一个原型。因此 JavaScript 中调用父类的方法其实是在不一样的对象上调用同一个方法,即“方法借用”,这种行为其实是“委托(delegation)”调用。

扩展阅读

  1. ECMAScript 6 入门:Class 的继承 - super 关键字

参考资料

  1. 《JavaScript模式》- 类式继承 vs 现代继承模式
  2. 《JavaScript高级程序设计(第3版)》- 面向对象的程序设计
  3. 《你不知道的JavaScript》- this和对象原型
相关文章
相关标签/搜索