[面试] 从Babel的角度看ES6与ES5继承的区别

这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战

前言

ES5中有6种实现继承的模式,分别是:原型链继承盗用构造函数继承组合继承原型式继承寄生式继承以及寄生组合式继承,关于这6种继承方式,掘金上已经有很多文章介绍,这里就不在赘述,整体看下来,这六种继承方式中,寄生组合式继承是最优的继承方式。ES6 提供了继承的关键字 extends,虽然Class本质上只是一种语法糖,但是extends的工作原理与ES5的继承却有很大不同,从而导致继承不同类型的对象所变现出的差异。

内置对象的继承

自定义对象ES5ES6的继承行为都表现的一致,来看看内置对象的继承。

内置对象是指由js内置的构造函数创建的对象,通常是一些数据结构,主要有ArrayMapObjectDate等等。在ES5中,这些对象是无法被继承的,举个栗子,我们使用寄生组合继承来派生出一个Array的子类看看:

function _inherit(subClass,superClass) {
  subClass.prototype = Object.create(superClass.prototype,{
    constructor: {
      value: subClass,
      writable: true,
      configurable: true,
      enumerable: true
    }
  });
}


function _Array() {
  Array.apply(this, arguments);
}

_inherit(_Array,Array)

let myArr = new _Array(1,2,3)
console.log(myArr instanceof Array) // true
console.log(myArr.length)// 0
console.log(Array.isArray(myArr))// false
复制代码

myArr确实是Array的一个子类,并且没有新加任何属性,但是,这个类的length行为与Array完全不一致,传入的数据被忽略了,可以控制台运行上述代码看看。但是后续调用push方法依旧可以往里面添加数据,因为push操作数据的同时也会更新length属性,原本原型链顶层没有length的,但是push新建了一个length

再来看看ES6的继承实现:

class _Array extends Array {
  constructor(...args) {
    super(...args);
  }
}

let myArr = new _Array(1,2,3)
console.log(myArr instanceof Array) // true
console.log(myArr.length)// 3
console.log(Array.isArray(myArr))// true
复制代码

子类myArr与父类Array的行为完全一致,ES6能够实现对内置对象的继承。

这只是继承Array,没有添加属性也没有报错,但是如果试图用ES5的方式去继承MapSet这些ES6的数据结构,在创建实例时会报Constructor xxx requires 'new'的错误,继承Date之类的对象,可以创建实例,但是当你调用其方法时,则会报this is not a xxx object

差异的原因

是什么导致了这种行为差异?根本原因是子类无法获得内置对象构造函数的内部属性。在寄生组合继承中,获得父类的内部属性是通过call/apply绑定this这种鸠占鹊巢的方式来获得,但是内置对象的构造函数会忽略apply / call方法传入的this,也就是说,原生构造函数的this无法绑定,导致拿不到内部属性。

再来看看它们继承的过程,寄生组合继承先新建子类的实例对象this,再将父类的属性添加到子类上,而ES6的继承是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承,这也是为什么ES6继承一定要在子类构造函数中先调用super()的原因。

super代表了父类的构造函数,但是返回的是子类的实例,即super内部的this指向的是子类的实例,即super类似于A.prototype.constructor.call(B)。我个人这么理解,ES5的继承获取父类内部属性是通过call/apply这种强制改变父类构造函数的运行时来获得,而super则是相当于拿到父类构造函数的函数体放到当前运行时执行,使得父类的所有属性都可以继承。

Babel如何转译

Babel能够将高级js语法转译为ES5,从而帮助我们解决浏览器兼容问题,那么它是如何将Class...extends转译为ES5语法的呢。还是以Array的子类为例:Babel在线转译

class _Array extends Array{
  constructor() {
    super();
  }
}
复制代码

转译后???

// 确保构造函数只能作为构造函数使用而不能作为普通函数
function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

// 绑定原型链
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);
}

// 创建父类对象实例,类似于super()
function _createSuper(Derived) {
  var hasNativeReflectConstruct = _isNativeReflectConstruct();
  return function _createSuperInternal() {
    var Super = _getPrototypeOf(Derived),
      result;
    if (hasNativeReflectConstruct) {
      var NewTarget = _getPrototypeOf(this).constructor;
      result = Reflect.construct(Super, arguments, NewTarget);
    } else {
      result = Super.apply(this, arguments);
    }
    return _possibleConstructorReturn(this, result);
  };
}

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 _wrapNativeSuper(Class) {
  var _cache = typeof Map === "function" ? new Map() : undefined;
  _wrapNativeSuper = function _wrapNativeSuper(Class) {
    if (Class === null || !_isNativeFunction(Class)) return Class;
    if (typeof Class !== "function") {
      throw new TypeError("Super expression must either be null or a function");
    }
    if (typeof _cache !== "undefined") {
      if (_cache.has(Class)) return _cache.get(Class);
      _cache.set(Class, Wrapper);
    }
    function Wrapper() {
      return _construct(Class, arguments, _getPrototypeOf(this).constructor);
    }
    Wrapper.prototype = Object.create(Class.prototype, {
      constructor: {
        value: Wrapper,
        enumerable: false,
        writable: true,
        configurable: true
      }
    });
    return _setPrototypeOf(Wrapper, Class);
  };
  return _wrapNativeSuper(Class);
}

function _construct(Parent, args, Class) {
  if (_isNativeReflectConstruct()) {
    _construct = Reflect.construct;
  } else {
    _construct = function _construct(Parent, args, Class) {
      var a = [null];
      a.push.apply(a, args);
      var Constructor = Function.bind.apply(Parent, a);
      var instance = new Constructor();
      if (Class) _setPrototypeOf(instance, Class.prototype);
      return instance;
    };
  }
  return _construct.apply(null, arguments);
}

function _isNativeReflectConstruct() {
  if (typeof Reflect === "undefined" || !Reflect.construct) return false;
  if (Reflect.construct.sham) return false;
  if (typeof Proxy === "function") return true;
  try {
    Boolean.prototype.valueOf.call(
      Reflect.construct(Boolean, [], function () {})
    );
    return true;
  } catch (e) {
    return false;
  }
}

// 判断是否是内置方法
function _isNativeFunction(fn) {
  return Function.toString.call(fn).indexOf("[native code]") !== -1;
}

// Object.setPrototypeOf特性检测
function _setPrototypeOf(o, p) {
  _setPrototypeOf =
    Object.setPrototypeOf ||
    function _setPrototypeOf(o, p) {
      o.__proto__ = p;
      return o;
    };
  return _setPrototypeOf(o, p);
}

// Object.getPrototypeOf 特性检测
function _getPrototypeOf(o) {
  _getPrototypeOf = Object.setPrototypeOf
    ? Object.getPrototypeOf
    : function _getPrototypeOf(o) {
        return o.__proto__ || Object.getPrototypeOf(o);
      };
  return _getPrototypeOf(o);
}

// 核心
var _Array = /*#__PURE__*/ (function (_Array2) {
  "use strict";

  _inherits(_Array, _Array2);

  var _super = _createSuper(_Array);

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

    return _super.call(this);
  }

  return _Array;
})(/*#__PURE__*/ _wrapNativeSuper(Array));//对父类构造函数做了一层包装
复制代码

转译后就是这么大一坨,不过大部分都是特性检测的代码,我们先把它拿去测试一下,看下是否能达到我们想要的效果,直接复制转译后的代码到浏览器,然后创建一个_Array实例。

let myArr = new _Array(1,2,3)
console.log(myArr instanceof Array) // true
console.log(myArr.length)// 0
console.log(Array.isArray(myArr))// true
复制代码

emmm,Array.isArray判断子类是个数组对象了,但是传入的数据依旧被忽略了。再来看看继承Map这种构造函数必须通过new调用的对象转译后能不能使用。这里就不贴完整转译的代码,只讨论结果,可以去在线转译看看。

/**其他转译代码忽略**/

let _Map = /*#__PURE__*/ (function (_Map2) {
  "use strict";

  _inherits(_Map, _Map2);

  var _super = _createSuper(_Map);

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

    return _super.call(this);
  }

  return _Map;
})(/*#__PURE__*/ _wrapNativeSuper(Map));

let map = new _Map([["name","jaylenl"]])
console.log(map.has("name"))// false
map.set("name","jaylenl")
console.log(map.has("name"))// true
复制代码

转移后可以new调用了,但是和_Array一样,传入构造函数的参数依旧会被忽略。看来Babel转译仍旧无法解决根本问题:子类无法获取父类的内部属性。即ES5层面就不可能完全实现内置函数的继承。

我们来详细看看Babel是怎样基于ES5实现继承的。

var _Array = /*#__PURE__*/ (function (_Array2) {
  "use strict";
  
  // 原型链继承
  _inherits(_Array, _Array2);

  //
  var _super = _createSuper(_Array);

  function _Array() {
    // 验证是否是 _Array 构造出来的 this
    _classCallCheck(this, _Array);
    
    return _super.call(this);
  }
  return _Array;
})(/*#__PURE__*/ _wrapNativeSuper(Array));
复制代码

从上面的核心代码中看出,Babel也是采用寄生组合继承的模式,也证明了这种模式是较优的继承解决方案,不过它做了一些优化,我们重点看_wrapNativeSuper_inherits_createSuper

_wrapNativeSuper

function _wrapNativeSuper(Class) {
  var _cache = typeof Map === "function" ? new Map() : undefined;
  _wrapNativeSuper = function _wrapNativeSuper(Class) {
    if (Class === null || !_isNativeFunction(Class)) return Class;
    if (typeof Class !== "function") {
      throw new TypeError("Super expression must either be null or a function");
    }
    if (typeof _cache !== "undefined") {
      if (_cache.has(Class)) return _cache.get(Class);
      _cache.set(Class, Wrapper);
    }
    function Wrapper() {
      return _construct(Class, arguments, _getPrototypeOf(this).constructor);
    }
    Wrapper.prototype = Object.create(Class.prototype, {
      constructor: {
        value: Wrapper,
        enumerable: false,
        writable: true,
        configurable: true
      }
    });
    return _setPrototypeOf(Wrapper, Class);
  };
  return _wrapNativeSuper(Class);
}
复制代码

在进行继承前,会先对父类用此函数进行包装成一个Wrapper,这个包装对象会绑定父类的原型链以及父类构造函数的自身的所有属性和方法。后续继承会以这个包装对象为基础。这样继承得到的子类除了拿到父类原型链上的属性和方法也能获得父类的静态方法和属性。

_inherits()

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绑定回子类构造函数
    constructor: { value: subClass, writable: true, configurable: true }
  });
  // _setPrototypeOf相当于Obeject.setPrototypeOf,但这是es6新增的方法。
  // 用于设置一个指定的对象的原型,即内部[[Prototype]]属性)到另一个对象上
  if (superClass) _setPrototypeOf(subClass, superClass);
}
复制代码

理解上面代码得先了解Object.createObeject.setPrototypeOf,这里就不介绍了,请看MDN,代码做的事情就是改变子类构造函数的原型prototype,改变的方式就是用一个基于父类原型链的创建的对象进行替换,由于函数prototype下有一个constructor属性来指向自身,因此再为这个对象新建一个指向子类构造函数的constructor属性。其实这就是原型链继承的内容,很好理解。

_createSuper

function _createSuper(Derived) {
  var hasNativeReflectConstruct = _isNativeReflectConstruct();
  return function _createSuperInternal() {
    // 获取子类原型链prototype,由_inherits()从父类那得来的,相当于父类prototype的深克隆。
    var Super = _getPrototypeOf(Derived),
      result;
    if (hasNativeReflectConstruct) {
      var NewTarget = _getPrototypeOf(this).constructor;
      result = Reflect.construct(Super, arguments, NewTarget);
    } else {
      // 盗用构造函数继承,绑定父类的内部属性。
      result = Super.apply(this, arguments);
    }
    return _possibleConstructorReturn(this, result);
  };
}
复制代码

这一步利用构造函数继承来获得父类构造函数的内部属性,也是使用apply传递this来实现,对于内置对象这是无效的,这也就说明了为什么子类构造函数的参数无效的原因,正如上面所说,传递this这种方式会被内置对象构造函数忽略,因此Babel依旧不能完全实现内置对象的继承。

T.J. Crowder 在 How does Babel.js create compile a class declaration into ES2015? 中谈到 Babel 是如何将class 转化为 ES5 兼容代码时谈到了几点:

  • ES6classconstructor 变成构造函数;
  • 所有非构造函数、非静态方法都成为原型方法;
  • 静态方法会成为构造函数的属性,其他属性保持不变;
  • 创建对象以放置在派生构造函数的prototype属性上是通过object.create(Base.prototype)而不是new Base()完成的。
  • constructor调用基类是第一步操作;
  • ES5中对应ES6的 super() 的写法是 Base.prototype.constructor.call(this); ,这种操作不仅繁琐而且容易出错。

总结

ES6ES5继承的区别:

  • ES5先创建子类实例this,再实例化父类并添加到子类this中。
  • ES6先创建父类实例this,再实例化子类this用来修饰父类实例。

对于自定义对象的继承,ES5ES6都能很好解决,但对于内置对象,ES5不可能完全实现,ES6extends才是终极解决方案,即便Babel有对class...extends语法的转译,但终究不能解决子类无法获得内置对象构造函数的内部属性这个根本问题,它只是在运行时层面使用现有语言特性对高级特性的一种 Polyfill

但毕竟转译成ES5是为了兼容低版本的浏览器,而继承内置对象的使用场景很少见,例如Web Components就需要继承内置DOM对象,但这种特性本身就是为现代浏览器设计的。

创作不易,如果这篇文章对你有帮助,欢迎点赞!文中如果有错误,感谢评论区斧正。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享