ES6_Extends如何对ES5的继承进行“糖化”

625 阅读8分钟

公所周知,JS在常规开发语言中。位于技术鄙视链顶端。说JS不好嘛,不是。Node的出现。预示着JS大有统一前后端的趋势。(这只是小弟的一个拙见,勿喷)或者卑微的说一句,JS能在后端也可以展示一下拳脚了。

其中有一点很让其他OOP语言诟病的就是:JS基于Prototype的继承。对于一个接触过C++JAVA,并在实际项目中使用过这些语言的卑微的我来说,第一次接触prototype的时候,那是尼克杨脸上都是问号(估计只有懂点NBA的人才会知道这个梗吧)。

但是自从ES6颁布以来,局面有一些好转,ES6也有了class/extends等相关语法实现。JS在众多OOP语言里,瞬间站了起来,有没有。

其实,作为一个接触过其他OOP语言的前端开发者,很喜欢用class来定义对象的结构,并用extends来实现继承。 但是几乎所有关于ES6特性介绍的文档中,你肯定会看到

ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到

其实在开发中直接用class定义对象结构,并用extends来实现继承。很常见,很方便。

既然人家都说了,这是一个。但是作为一个合格的开发者。要有打破砂锅问到底的心思。所以,我用我拙劣的代码来将这层的成分细细分析一下。

下面一些方法,都是基于class脱糖之后的分析。关于class是如何被脱糖的过程===>请先查看ES6-Class如何优雅的进行Prototype“糖化” 内容很精彩,但是不要忘记回来看这个。这个剧情也挺赤鸡的。

ES5基于prototype的继承

在进行继承的讲解的时候,还是需要明确一点。就是ES5的类 = 构造函数+prototpye。所以基于这点就存在两个继承方向。

  1. 继承构造函数中的实例属性和方法
  2. 继承原型对象上的属性和方法

关于ES5的继承有很多实现方式:例如原型链继承(原型继承)构造函数继承(实例属性)组合式继承原型式继承寄生式继承寄生组合式继承。 其中我们按组合式继承(原型链+构造函数继承)来简单说明一下实现原理。

原型链继承

继承原型对象上的属性和方法

将一个类型的实例,赋值给另外一个构造函数的原型

子类是继承父类的原型方式

function SuperObj(){
    this.objName = 'super';
}

SuperObj.prototype.getName = function(){
    return this.objName;
}

function SubObj(){
    this.objName = 'sub';
}
//将父类的实例,赋值给目标类的prototype
SubObj.prototype = new SuperObj();

var instance = new SubObj();

//继承父类的原型方法
instance.getName() //super

instance  instanceof SubObj //true
instance instanceof SuperOjb //true

构造函数继承

继承构造函数中的实例属性和方法

首先说明一点,构造函数继承,只是对目标构造函数中属性的继承,而不是真正基于prototype的继承。子类的实例如果用instanceof来进行判断的话,其实返回的是false

子类继承父类的属性和方法

function SuperObj(){
    this.nameArr = ['北宸','南蓁'];
}
//在子类的构造函数调用父类构造函数
function SubObj(){
    SuperOjb.call(this)
}

var instance1 = new SubObj();
instance1.nameArr.push('你好'); //['北宸','南蓁','你好']

var instance2  = new SubObj();

instance2.nameArr //['北宸','南蓁']

instance1 instanceof SuperObj //false 


组合继承

组合式继承其实就是构造函数继承+原型链继承 。也就是使用原型链实现对原型方法的继承,借用构造函数来实现对实例属性的继承

由于ES5实现一个完整的类,就需要构造函数+prototype

function SuperObj(allname){
    this.nameArr = ['北宸','南蓁'];
    this.allName =allname;
}

SuperObj.prototype.getAllName = function(){
    return this.allName;
}

//构造函数继承,继承目标构造函数的属性和方法
function SubObj(allname,age){  
    SuperObj.call(this,allname);
    this.age = age;
}

//原型链继承 
SubObj.prototype = new SuperObj('北宸');

var instance1 = new SubObj('instance1',1);

var instance2 = new SubOjb('instance2',2);

instance1 instanceof SuperObj //true
instance2 instanceof SuperObj //true

instance1.getAllName() //instance1
instance2.getAllName()//instance2

ES6利用extends的"糖化"继承

最简单的继承

Talk is cheap,show you the code


class A {
    
}

class B extends A{
    
}

这算是最简单的一个继承。或者从严格意义上,B是对A复制了一份。因为A没有任何的实例属性和方法,也没有原型属性。

但是我们可以看看脱糖之后的代码。


"use strict";

function _typeof(obj) {
  if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
    _typeof = function _typeof(obj) {
      return typeof obj;
    };
  } else {
    _typeof = function _typeof(obj) {
      return obj &&
        typeof Symbol === "function" &&
        obj.constructor === Symbol &&
        obj !== Symbol.prototype
        ? "symbol"
        : typeof obj;
    };
  }
  return _typeof(obj);
}

function _possibleConstructorReturn(self, call) {
  if (call && (_typeof(call) === "object" || typeof call === "function")) {
    return call;
  }
  return _assertThisInitialized(self);
}

function _assertThisInitialized(self) {
  if (self === void 0) {
    throw new ReferenceError(
      "this hasn't been initialised - super() hasn't been called"
    );
  }
  return self;
}

function _getPrototypeOf(o) {
  _getPrototypeOf = Object.setPrototypeOf
    ? Object.getPrototypeOf
    : function _getPrototypeOf(o) {
        return o.__proto__ || Object.getPrototypeOf(o);
      };
  return _getPrototypeOf(o);
}

function _inherits(subClass, superClass) {
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError("Super expression must either be null or a function");
  }
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: { value: subClass, writable: true, configurable: true }
  });
  if (superClass) _setPrototypeOf(subClass, superClass);
}

function _setPrototypeOf(o, p) {
  _setPrototypeOf =
    Object.setPrototypeOf ||
    function _setPrototypeOf(o, p) {
      o.__proto__ = p;
      return o;
    };
  return _setPrototypeOf(o, p);
}

function _instanceof(left, right) {
  if (
    right != null &&
    typeof Symbol !== "undefined" &&
    right[Symbol.hasInstance]
  ) {
    return !!right[Symbol.hasInstance](left);
  } else {
    return left instanceof right;
  }
}

function _classCallCheck(instance, Constructor) {
  if (!_instanceof(instance, Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var A = function A() {
  _classCallCheck(this, A);
};

var B =
  /*#__PURE__*/
  (function(_A) {
    _inherits(B, _A);

    function B() {
      _classCallCheck(this, B);

      return _possibleConstructorReturn(
        this,
        _getPrototypeOf(B).apply(this, arguments)
      );
    }

    return B;
  })(A);

我们来分析一波。

var A = function A() {
  _classCallCheck(this, A);
};

这个就不用在多说啥了。就是简单定义了一个构造函数。

其实最关键的还是下面的代码: 首先映入眼帘的是一个IIFE,接收刚被定义的构造函数A

这里直接给大家把对应的注释给加上了。

(function(_A) {
    //继承原型对象上的属性和方法
    _inherits(B, _A);
    
    function B() {
      _classCallCheck(this, B);
      //继承构造函数中的实例属性和方法
      return _possibleConstructorReturn(
        this,
        _getPrototypeOf(B).apply(this, arguments)
      );
    }

    return B;
  })(A);

通过代码我们看到_inherits(B, _A)用于继承原型对象上的属性和方法,而_possibleConstructorReturn则是继承构造函数中的实例属性和方法。所以很符合我们上面讲的。

然后我们继续分析其中的原委:

继承原型对象上的属性和方法(_inherits)

Talk is cheap,show you the code:

//_inherits(B, _A)
function _inherits(subClass, superClass) {
    //对superClass进行类型判断
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError("Super expression must either be null or a function");
  }
  //子类的prototype继承父类的prototype
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: { value: subClass, writable: true, configurable: true }
  });
  //子类是父类构建出的函数对象,需要指定对象的__proto__
  if (superClass) _setPrototypeOf(subClass, superClass);
}

Note: 有一点需要注意的就是:
大多数浏览器的 ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。
(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

继承构造函数中的实例属性和方法(_possibleConstructorReturn)

/*
_possibleConstructorReturn(
        this,//指向子类构造函数
        //_getPrototypeOf(B)用于获取指定对象的父类
        _getPrototypeOf(B).apply(this, arguments)
      );
*/
function _possibleConstructorReturn(self, call) {
  if (call && (_typeof(call) === "object" || typeof call === "function")) {
    return call;
  }
  return _assertThisInitialized(self);
}

function _assertThisInitialized(self) {
  if (self === void 0) {
    throw new ReferenceError(
      "this hasn't been initialised - super() hasn't been called"
    );
  }
  return self;
}

在进行_possibleConstructorReturn调用的时候,其实处理classconstructor的实例属性和方法的继承。当父类存在constructor就需要_getPrototypeOf(B).apply(this, arguments)将父类的属性和方法复制到新的构造函数中。实现继承。

Note:存在_getPrototypeOf(B).apply(this, arguments)是父类存在constructor。如果不存在,直接就是_possibleConstructorReturn(this)

要点汇总

其实ES6extends实现继续还是基于ES5的组合继承(构造函数继承+原型链继承)

  1. _inherits(B, _A)实现原型链的继承
  2. 在子类的构造函数中的_possibleConstructorReturn实现构造函数的继承

复杂的类

上面讲的那个例子就是一个空类AB继承。其实就是B将A复制了一遍,这是利用的extends的语法。来看基本的语法实现。

ES6示例

现在我们来构建一个比较常规的类:拥有实例方法的继承

Talk is cheap ,show you the code:

class A {
  static name = 'superClass'
  constructor(x,y){
    this.x =x;
    this.y =y;
  }
}

class B extends A{
  	
	constructor(x,y,z){
    	super(x,y);
		this.y = y;
    }
}

ES5脱糖

"use strict";

function _typeof(obj) {
  if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
    _typeof = function _typeof(obj) {
      return typeof obj;
    };
  } else {
    _typeof = function _typeof(obj) {
      return obj &&
        typeof Symbol === "function" &&
        obj.constructor === Symbol &&
        obj !== Symbol.prototype
        ? "symbol"
        : typeof obj;
    };
  }
  return _typeof(obj);
}

function _possibleConstructorReturn(self, call) {
  if (call && (_typeof(call) === "object" || typeof call === "function")) {
    return call;
  }
  return _assertThisInitialized(self);
}

function _assertThisInitialized(self) {
  if (self === void 0) {
    throw new ReferenceError(
      "this hasn't been initialised - super() hasn't been called"
    );
  }
  return self;
}

function _getPrototypeOf(o) {
  _getPrototypeOf = Object.setPrototypeOf
    ? Object.getPrototypeOf
    : function _getPrototypeOf(o) {
        return o.__proto__ || Object.getPrototypeOf(o);
      };
  return _getPrototypeOf(o);
}

function _inherits(subClass, superClass) {
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError("Super expression must either be null or a function");
  }
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: { value: subClass, writable: true, configurable: true }
  });
  if (superClass) _setPrototypeOf(subClass, superClass);
}

function _setPrototypeOf(o, p) {
  _setPrototypeOf =
    Object.setPrototypeOf ||
    function _setPrototypeOf(o, p) {
      o.__proto__ = p;
      return o;
    };
  return _setPrototypeOf(o, p);
}

function _instanceof(left, right) {
  if (
    right != null &&
    typeof Symbol !== "undefined" &&
    right[Symbol.hasInstance]
  ) {
    return !!right[Symbol.hasInstance](left);
  } else {
    return left instanceof right;
  }
}

function _classCallCheck(instance, Constructor) {
  if (!_instanceof(instance, Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }
  return obj;
}

var A = function A(x, y) {
  _classCallCheck(this, A);

  this.x = x;
  this.y = y;
};

_defineProperty(A, "name", "superClass");

var B =
  /*#__PURE__*/
  (function(_A) {
    _inherits(B, _A);

    function B(x, y, z) {
      var _this;

      _classCallCheck(this, B);

      _this = _possibleConstructorReturn(
        this,
        _getPrototypeOf(B).call(this, x, y)
      );
      _this.y = y;
      return _this;
    }

    return B;
  })(A);


看到代码之后,其实大致的实现流程和B继承一个空类A是一样的。都是_inherits()实现原型继承,_possibleConstructorReturn()实现构造函数继承。

但是这里多了一个内部_this

这里需要额外的提出:ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

也就是说,如果在子类的构造函数中想调用this必须先调用super()