《JavaScript高级程序设计》学习笔记(二)- JS 继承的六种方式

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;
}

// 继承 SuperType
SubType.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

《JavaScript高级程序设计》学习笔记(二)- JS 继承的六种方式_第1张图片

注意:

  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在内的所有默认方法。

    《JavaScript高级程序设计》学习笔记(二)- JS 继承的六种方式_第2张图片

  2. 原型与继承的关系

    原型与继承的关系可以通过两种方式确定:

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

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

      console.log(Object.prototype.isPrototypeof(instance)); // true
      console.log(SuperType.prototype.isPrototypeof(instance)); // true
      console.log(SubType.prototype.isPrototypeof(instance)); // true
  3. 关于方法

    • 若希望覆盖父类的方法或增加父类没有的方法时,必须在原型赋值之后再添加到原型上。

      function SuperType () {
        this.property = true;
      }
      
      SuperType.prototype.getSuperValue = function () {
        return this.property;
      }
      
      function SubType () {
        this.subproperty = false;
      }
      
      // 继承 SuperType
      SubType.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;
      }
      
      // 继承 SuperType
      SubType.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(); // 24

let 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两个实例属性,此时将遮蔽原型上同名的两个属性。

《JavaScript高级程序设计》学习笔记(二)- JS 继承的六种方式_第3张图片

《JavaScript高级程序设计》学习笔记(二)- JS 继承的六种方式_第4张图片

解决组合继承效率问题的方案是寄生组合继承。

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方法正常有效。寄生组合继承是引用类型继承的最佳模式。

你可能感兴趣的:(前端javascript)