JavaScript 继承的六种形式

很多面向对象语言都反对接口继承实现继承两种继承形式,前者只继承办法签名,后者继承理论的办法。接口继承在 ECMAScript 中是不可能的,因为函数没有签名。实现继承是 ECMAScript 惟一反对的继承形式,这次要是通过原型链实现的。

1. 原型链

1.1 思路

ECMAScript-262 把原型链定义为 ECMAScript 的次要继承形式。其根本思维是通过原型继承多个援用类型的属性和办法

构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回与之关联的构造函数,而实例有一个外部指针指向原型。

原型链:若原型是另一个类型的实例,则这个原型自身有一个外部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样在实例和原型之间就结构了一条原型链。

function SuperType () {  this.property = true;}SuperType.prototype.getSuperValue = function () {  return this.property;}function SubType () {  this.subproperty = false;}// 继承 SuperTypeSubType.prototype = new SuperType();SubType.prototype.getSubValue = function () {  return this.subproperty;}let instance = new SubType();console.log(instance.getSuperValue()); // true

SubType通过创立SuperType的实例并将其赋值给本人的原型SubType.prototype实现了对SuperType的继承,这一赋值重写了Subtype最后的原型,将其替换为SuperType的实例,即SuperType实例能够拜访的所有属性和办法也会存在于SubType.prototype,同时于是instance(通过外部的[[Prototype]])指向SubType.prototype,而SubType.prototype(作为SuperType的实例又通过外部的[[Prototype]])指向SuperType.prototype

留神:

  1. getSuperValue办法还在SuperType.prototype对象上,而property属性则在SubType.prototype上。因为getSuperValue是一个原型办法,而property是一个实例属性。SubType.prototype当初是SuperType的一个实例,因而其上会存储property属性。
  2. 因为SubType.prototypeconstructor属性被重写为指向SuperType,所以instance.constructor也指向SuperType
  3. 在通过原型链实现继承时,搜寻过程会沿着原型链持续向上,调用instance.getSuperValue会顺次搜寻instanceSubType.prototypeSuperType.prototype。在找不到属性或办法时,搜寻过程总是要到原型链末端才会停下。

1.2 扩大

  1. 默认原型

    默认状况下,所有援用类型都继承自Object,这也是通过原型链实现的。任何函数的默认原型都是一个Object的实例,即这个实例有一个外部指针指向Object.prototype。因而自定义类型可能继承包含toStringvalueOf在内的所有默认办法。

  2. 原型与继承的关系

    原型与继承的关系能够通过两种形式确定:

    • instanceof操作符:若一个实例的原型链中呈现过相应的构造函数,则instanceof操作符返回true

      console.log(instance instanceof Object); // trueconsole.log(instance instanceof SuperType); // trueconsole.log(instance instanceof SubType); // true
    • isPrototypeOf办法:原型链中的原型调用这个办法并传入实例时返回true。

      console.log(Object.prototype.isPrototypeof(instance)); // trueconsole.log(SuperType.prototype.isPrototypeof(instance)); // trueconsole.log(SubType.prototype.isPrototypeof(instance)); // true
  3. 对于办法

    • 若心愿笼罩父类的办法或减少父类没有的办法时,必须在原型赋值之后再增加到原型上。

      function SuperType () {  this.property = true;}SuperType.prototype.getSuperValue = function () {  return this.property;}function SubType () {  this.subproperty = false;}// 继承 SuperTypeSubType.prototype = new SuperType();// 新办法SubType.prototype.getSubValue = function () {  return this.subproperty;}// 笼罩已有办法SubType.prototype.getSuperValue = function () {  return false;}let instance = new SubType();console.log(instance.getSuperValue()); // false
    • 以对象字面量形式创立原型办法会毁坏之前的原型链,相当于重写了原型链,将原型设置为一个Object的实例。

      function SuperType () {  this.property = true;}SuperType.prototype.getSuperValue = function () {  return this.property;}function SubType () {  this.subproperty = false;}// 继承 SuperTypeSubType.prototype = new SuperType();SubType.prototype = {  getSubValue () {    return this.subproperty;  }      someOtherMethod () {    return false;  }}let instance = new SubType();console.log(instance.getSuperValue()); // error
  4. 原型链的问题

    • 次要问题是当原型中蕴含援用值时,该援用值会在实例间共享,这也是属性通常在构造函数中定义而不会在原型上定义的起因。

      function SuperType () {  this.colors = ['red', 'blue', 'green'];}function SubType () {}SubType.prototype = new SuperType();let instance1 = new SubType();instance1.colors.push('black');let instance1 = new SubType();console.log(instance1.colors); // 'red', 'blue', 'green', 'black'console.log(instance2.colors); // 'red', 'blue', 'green', 'black'

      SubType通过原型继承SuperType之后,SubType.prototype变成SuperType的一个实例,因此取得了本人的colors属性,相似于创立了SubType.prototype.colors属性,故SubType的所有实例都会共享这个colors属性。

    • 在实例化子类型时不能给父类型的构造函数传参。

2. 盗用构造函数

盗用构造函数(constructor stealing)技术也称为对象假装或经典继承。

2.1 思路

基本思路是在子类构造函数中调用父类构造函数。因为函数就是在特定上下文中执行代码的简略对象,所以能够应用applycall办法以新创建的对象为上下文执行构造函数。

function SuperType () {  this.colors = ['red', 'blue', 'green'];}function SubType () {  // 继承 SuperType  SuperType.call(this);}let instance1 = new SubType();instance1.colors.push('black');let instance1 = new SubType();console.log(instance1.colors); // 'red', 'blue', 'green', 'black'console.log(instance2.colors); // 'red', 'blue', 'green'

SuperType构造函数在为SubType的实例创立的新对象的上下文中执行,相当于新的SubType对象上运行了SuperType构造函数中的所有初始化代码,即每个实例都会有本人的colors属性。

2.2 扩大

  1. 传递参数

    盗用构造函数的长处是能够在子类构造函数中向父类构造函数传参。

    function SuperType (name) {  this.name = name;}function SubType () {  SuperType.call(this, 'Stan');  this.age = 24;}let instance = new SubType();console.log(instance.name); // 'Stan'console.log(instance.age); // 24

    SubType构造函数中调用SuperType构造函数时传入参数,实际上会在SubType的实例上定义name属性。

    为确保SuperType构造函数不会笼罩SubType定义的属性,能够在调用父类构造函数之后再给子类实例增加额定的属性。

  2. 问题

    盗用构造函数的次要毛病也是应用构造函数模式自定义类型的问题,即必须在构造函数中定义方法,因而函数不能重用。此外,子类也不能拜访父类原型上定义的办法,因而所有类型只能应用构造函数模式。

3. 组合继承

组合继承也称为伪经典继承,综合了原型链和盗用结构函树的长处。组合继承补救了原型链和盗用构造函数的有余,是 JavaScript 中应用最多的继承模式。同时,组合继承也保留了instanceof操作符和isPrototypeOf办法辨认合成对象的能力。

3.1 思路

组合继承的基本思路是应用原型链继承原型上的属性和办法,而通过盗用构造函数继承实例属性。

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();SubType.prototype.constructor = SubType;SubType.prototype.sayAge = function () {  console.log(this.age);}let instance1 = new SubType('Stan', 24);instance1.colors.push('black');console.log(instance1.colors); // 'red', 'blue', 'green', 'black'instance1.sayName(); // 'Stan'instance1.sayAge(); // 24let instance2 = new SubType('Greg', 20);console.log(instance2.colors); // 'red', 'blue', 'green'instance2.sayName(); // 'Greg'instance2.sayAge(); // 20

3.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.call(this, name);    this.age = age;}// 第一次调用SubType.prototype = new SuperType();SubType.prototype.constructor = SubType;SubType.prototype.sayAge = function () {  console.log(this.age);}

以上代码执行后,SubType.prototype上会有namecolors两个属性,二者是SuperType的实例属性,当初成为了SubType的原型属性。在调用SubType构造函数时,也会调用SuperType构造函数,此时会在新对象上创立namecolors两个实例属性,此时将遮蔽原型上同名的两个属性。

解决组合继承效率问题的计划是寄生组合继承。

4. 原型式继承

4.1 思路

最后,原型式继承(prototypal inheritance)是一种不波及严格意义上构造函数的继承办法,出发点是即便不自定义类型也能够通过原型实现对象之间的信息共享。

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

object函数通过创立一个长期构造函数,并将传入的对象赋值给这个构造函数的原型,最初返回这个长期类型的一个实例,实现继承。实质上object函数对传入的对象执行了一次浅拷贝。

let person = {  name: 'Stan',  friends: ['xiaoming', 'xiaohong']};let anotherPerson = object(person);anotherPerson.name = 'xiaobai';anotherPerson.friends.push('xiaohei');let yetAnotherPerson = object(person);yetAnotherPerson.name = 'xiaohei';yetAnotherPerson.friends.push('xiaohei');console.log(person.friends); // 'xiaoming', 'xiaohong', 'xiaobai', 'xiaohei'

原型式继承实用于有一个对象并心愿在其根底上再创立一个新对象的状况。把这个对象传给object函数,而后再对返回的对象进行适当批改。

4.2 扩大

  1. Object.create办法

    ECMAScript5 减少了Object.create办法对原型式继承的概念进行了标准。Object.create办法接管两个参数,作为新对象原型的对象,以及给新对象定义额定属性的对象(可选)。在只有第一个参数时,Object.create办法与之前的object办法成果雷同。

    let person = {  name: 'Stan',  friends: ['xiaoming', 'xiaohong']}let anotherPerson = Object.create(person);anotherPerson.name = 'xiaobai';anotherPerson.friends.push('xiaohei');let yetAnotherPerson = Object.create(person);yetAnotherPerson.name = 'xiaohei';yetAnotherPerson.friends.push('xiaohei');console.log(person.friends); // 'xiaoming', 'xiaohong', 'xiaobai', 'xiaohei'

    Object.create办法的第二个参数与Object.defineProperties办法的第二个参数一样,每个新增属性都通过各自的描述符来形容。

    let person = {  name: 'Stan',  friends: ['xiaoming', 'xiaohong']}let anotherPerson = Object.create(person, {  name: {    value: 'Greg'  }});console.log(anotherPerson.name); // 'Greg'
  2. 问题

    原型式继承实用于不须要独自创立构造函数,但依然须要在对象间共享信息的场合。但与原型链一样,属性中蕴含的援用值始终会在相干对象间共享。

5. 寄生式继承

寄生式继承(parasitic inheritance)是一种与原型式继承比拟靠近的继承形式。

5.1 思路

寄生式继承的思路相似于寄生构造函数和工厂模式,即创立一个实现继承的函数,以某种形式加强对象,最初返回这个对象。

function createAnother (original) {  // 创立一个新对象  let clone = object(original);  // 以某种形式加强对象  clone.sayHi = function () {    console.log('hi');  };  return clone;}let person = {  name: 'Stan'}let anotherPerson = createAnother(person);anotherPerson.sayHi(); // 'hi'

5.2 扩大

  • 寄生式继承同样实用于次要关注对象,而不在乎类型和构造函数的场景。
  • object函数不是寄生式继承所必须的,能够应用任何返回新对象的函数。
  • 通过寄生式继承给对象增加函数会导致函数难以重用,与构造函数模式相似。

6. 寄生组合继承

寄生组合继承通过盗用构造函数继承属性,但应用混合式原型链继承办法。

寄生组合继承的基本思路是不通过调用父类构造函数给子类原型赋值,而是获得父类原型的一个正本,即应用寄生式继承来继承父类原型,再将返回的新对象赋值给子类原型。

function inheritPrototype (SubType, SuperType) {  let prototype = object(SuperType.prototype);  prototype.constructor = SubType;  SubType.prototype = prototype;}

inheritPrototype函数实现了寄生组合继承的外围逻辑。函数接管子类构造函数和父类构造函数两个参数。在函数外部,首先创立父类原型的正本;其次给返回的prototype对象设置constructor属性,解决因为重写原型导致默认constructor失落的问题;将新创建的对象赋值给子类型的原型。

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;}inheritPrototype(SubType, SuperType);SubType.prototype.sayAge = function () {  console.log(this.age);};

应用寄生组合继承时,原型链依然放弃不变,instanceof操作符和isPrototypeOf办法失常无效。寄生组合继承是援用类型继承的最佳模式。