深入浅出JavaScript继承(多继承篇)

1,487 阅读4分钟

一.前言

由于JavaScript语言的特性,并没有提供实现多继承方式的API。但是只要我们了解继承的原理,从底层实现出发,还是可以实现js的多继承的。

这里有一种假多继承,我们不讨论,即 C------> B ------> A; 这种方式虽然 C有类B和类A的实例属性和方法,但是这不在我们的范畴之类不属于真正意义上的多继承。

二. 核心思想

在上一章中,我们介绍了继承的实现方式。在实现多继承之前我们思考一下如下问题:

  1. 我们能否通过借用构造函数中调用多个基类的构造函数来实现属性的多继承
  2. 我们能否通过将原型对象中原型属性赋值为 多个基类原型对象的合并对象
  3. 多个基类的同名方法该怎么处理?

从以上问题出发,我们得到核心思想:

利用借用构造函数中调用多个基类构造函数完成实例属性的多继承,通过多个基类原型对象的合并对象来完成方法的多继承

三. 理论与实践

在上一章中,我们了解了继承的两个非常重要的基本知识。

  1. 通过借用构造函数来完成实例属性的继承。
  2. 通过原型对象完成方法的复用。

首先拥有以下父类:

形状类定义文件(Shape.js) :

   // 父类 Shape(形状) Shape.js
 function Shape(shapeName) {
  this.shapeName = shapeName || "";
  console.log("Shape constructed");
}
Shape.prototype = {
  getShapeName() {
    return this.shapeName;
  },
  setShapName(shapeName) {
    this.shapeName = shapeName;
  },
  print() {
    console.log("this shape:", this.shapeName);
  }
};

export default Shape;

颜色类定义文件(Color.js):

// 颜色类定义文件(Color.js)
function Color(colorName) {
  this.colorName = colorName || "无颜色";
}
Color.prototype = {
  getColor() {
    console.log("调用得到颜色方法");
    return this.colorName;
  },
  setColor(colorName) {
    console.log("设置颜色为", colorName);
    this.colorName = colorName;
  },
  print() {
    console.log("this is Color:", this.colorName);
  }
};

export default Color;

子类文件(Rectangle.js)

 // 矩形类定义文件
import Shape from "./Shape";
import Color from "./Color";

export default function Rectangle(name, color) {
  // 这里完成属性的多继承。
  Shape.call(this, name);
  Color.call(this, color);
  this.width = 5;
  this.height = 6;
  console.log("Rectangle construted");
  this.getArea = () => {
    console.log("get Area:", this.width * this.height);
    return this.width * this.height;
  };
  this.getC = () => {
    console.log("get C:", (this.width + this.height) * 2);
    return (this.width + this.height) * 2;
  };
}

// 创建合并原型对象
const mergePrototype = Object.assign({}, Shape.prototype, Color.prototype);

Rectangle.prototype = Object.create(mergePrototype);

// 这里标致我们继承与哪儿
Rectangle.prototype.__proto__.constructor = [Shape, Color];

// 自己的函数方法复用定义位置
Rectangle.prototype.getParentClass = function() {
  const parents = this.__proto__.constructor || [];
  console.log("我继承于:", parents.map(item => item.name));
};

**测试文件:index.js

import Rectangle from "./inherit/rectangle";
console.log("Rectangle.prototype.__proto__:", Rectangle.prototype.__proto__);

let rec2 = new Rectangle("矩形2");
console.log("rec2 is:", rec2);
rec2.getArea();
rec2.getC();
rec2.setColor("red");
rec2.getColor();
rec2.getParentClass();

**代码运行结果如下:

image.png

由图可知:

  1. rec2实例拥有自己的实例方法getArea,getC,实例属性width,height
  2. 继承而来的colorName,shapeName属性;
  3. Reactangle实例公有的得到继承父类方法getParentClass
  4. 从Color类继承而来的getColor,setColorprint方法(因为覆盖了Shape中的print)
  5. 从Shape类继承而来的getShapeName,setShapeName方法

基本上一个多继承就实现完毕了。

tips:关于同名方法覆盖问题,这个其实在Java语言和C++语言中也会存在;这属于多继承中的另一个问题,即二义性问题,js语言,作者暂时没有好的办法处理,只能避免声明同名属性。而C++中提供了作用域运算符,方便的指定调用哪个基类的同名方法.

作者这里提供一个思路:我们在创建合并对象时,遇到同名方法,做以下操作:

遍历分别保存各自的同名方法。 如Rectangle.notSingle = [print,...], Rectangle.notSingle = [print,...]

  1. 创建方法名Of类名的方法,如printOfColor,printOfShape

  2. 创建一个新方法print(__range__),__range__理解为作用域,指明调用哪个父类的同名方法。print这个函数的逻辑将会根据__range__的值决定具体调用哪个函数。最好给定默认值

  3. 我们调用print时传递一个__range__属性即可。

总结

1. 通过借用构造函数来完成属性的继承。

2. 通过合并原型对象完成方法的继承。

如图所示:

image.png

此方法的缺点

  1. 无法使用instance of 运算符;这一点其实也很好改; instanceof是干什么的

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

即遍历实例对象的原型属性,看看是否跟构造函数的prototype相等,instanceof即返回true。

根据instanceof原理, 改造一下合并对象即可。有兴趣的读者可以自己尝试一波。

根据神三元大佬的博客;我们也可以用一下自己实现instanceof运算符。

image.png

参考

MDN Obect.assign