从对象介绍到继承的几种实现,现在才要真正的看看Babel是如何处理class和extends。

Babel如何翻译class

假设我们有如下ES6代码:

// demo.js
class SuperType {
  constructor(name) {
    this.superName = name;
    this.superFriends = ['Json'];
  }

  sayHi() {
    console.log("Super HI!");
  }

  static getVersion() {
    return "12.2";
  }
}

包含了基本类型值的属性,引用类型值的属性,实例方法和一个静态方法,来看一下Babel翻译后的代码,看看如何实现class这个语法糖。!注意!静态属性这里不讨论,虽然行为上不特殊,但是需要额外的支持,先关注核心的内容。

// demo_babel.js
"use strict";

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

function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i];
    descriptor.enumerable = descriptor.enumerable || false;
    descriptor.configurable = true;
    if ("value" in descriptor) descriptor.writable = true;
    Object.defineProperty(target, descriptor.key, descriptor);
  }
}

function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  if (staticProps) _defineProperties(Constructor, staticProps);
  return Constructor;
}

var SuperType =
  /*#__PURE__*/
  function () {
    function SuperType(name) {
      _classCallCheck(this, SuperType);

      this.superName = name;
      this.superFriends = ['Json'];
    }

    _createClass(SuperType, [{
      key: "sayHi",
      value: function sayHi() {
        console.log("Super HI!");
      }
    }], [{
      key: "getVersion",
      value: function getVersion() {
        return "12.2";
      }
    }]);

    return SuperType;
  }();

从上到下依次介绍:

_classCallCheck:这是一个常用的,判断是普通调用还是new调用的方式。根据new操作符的执行原理,this将是一个内在的已有原型指向的对象,构造法对其进行构造,从而形成对应类型的实例。而普通调用,在没有改变执行上下文的情况下,this都是顶层对象,所以通过instanceof来判断。

_defineProperties:是_createClass依赖的工具函数,从名称和签名上可以揣测一下,应该是和Object.defineProperty类似的功能。简单看一下逻辑,其实就是可以批量的defineProperty。

_createClass:创建class的核心方法,包含向原型添加属性,和向构造函数本身添加属性,前者是处理实例定义,后者是处理静态定义。

通过一个立即执行函数创建一个大的闭包,将整个过程隔离起来。假设我们用普通的写法自己实现class,大致会像这样:

function SuperType(name) {
    // 大部分时候都会忽略new调用和普通调用要区分开
  this.name = name;
  this.superFriends = ['Json'];
}

SuperType.getVersion = function () {
  return "12.1"
};

SuperType.prototype.sayHi = function() {
  console.log("Super HI!", this.name);
};

Babel实现的class一方面增加了调用检查,另一方面,在定义多个实例方法的使用,可以通过_defineProperties来批量实现。所以看上去其实区别并不很大。

Babel如何翻译class+extends

似乎终于来到了正题,为了减少贴太多代码,下面仅包含增量代码:

// demo.js
class SubType extends SuperType {
  constructor(name, age) {
    super(name);
    this.age = age;
  }

  sayHi() {
    console.log("Sub HI!", this.name, this.age);
  }
}

翻译后的代码,第一部分,先介绍工具函数:

// demo_babel.js
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);
}

// 上文介绍过的三个工具函数

// 上文列出过的SuperType的声明

_typeof:是对原生typeof的一个扩展,在那些没有Symbol特性的环境下,如果通过特殊手段实现了Symbol,该函数依然能够判断扩展的类型。注意else分支中的三元表达式。

_possibleConstructorReturn和_assertThisInitialized:这两个要一起看。在调用处会具体介绍。

*_getPrototypeOf和_setPrototypeOf*:在MDN文档中你会看到一些浏览器支持__proto__ 但不支持Object.getPrototypeOf,所以这两个方法用于兼容。

_inherits

你看到这个方法和别的工具方法不同,被H2了!单独把这个函数拎出来:

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

如果superClass,也就是父类这个参数,并没有传构造法,会抛出一个错误,合情合理。

接下来的代码你可能会有一些熟悉,上一篇文章介绍继承的集中方式式,最后一种“寄生组合”:

// 本函数只是在原理上介绍寄生组合,并没有考虑constructor这个属性的本身细节
function inheritPrototype(subType, superType) {
    let prototype = Object.create(superType.prototype);
    prototype.constructor = subType;
    subType.prototype = prototype;
}

注意到在恢复constructor的步骤上,Babel更加严谨的使用Object.create第二个参数,没有配置enumerable特性,所以默认是false的,这个属性并不会被迭代。具体可以执行一下Object.getOwnPropertyDescriptor(Object.prototype, "constructor") 。这个时候子类的原型,是一个以父类原型为原型的对象,但是后来执行了一个过程“subClass.__proto__=superClass”,直观上看是为了继承父类的静态方法,具体作用只能先这么理解了。

等具体看到后面的代码,你会发现Babel确实是寄生组合实现的继承。

具体继承

_inherits函数基本上指明了组合寄生的道路,那么具体的实现看看还有哪些细节:

// demo_babel.js
var SubType =
  /*#__PURE__*/
  function (_SuperType) {
    _inherits(SubType, _SuperType);

    function SubType(name, age) {
      var _this;

      _classCallCheck(this, SubType);

      _this = _possibleConstructorReturn(this, _getPrototypeOf(SubType).call(this, name));
      _this.age = age;
      return _this;
    }

    _createClass(SubType, [{
      key: "sayHi",
      value: function sayHi() {
        console.log("Sub HI!", this.name, this.age);
      }
    }]);

    return SubType;
  }(SuperType);

寄生组合的思想主要是两部分:1.借用父类构造函数构造自身;2.正确处理原型关系。_inherits函数处理了2。上一篇文章的例子,通过SuperClass.call的方式,将构造过程转移到子类实例上,而Babel的做法更为精妙,仔细看:

_this = _possibleConstructorReturn(this, _getPrototypeOf(SubType).call(this, name));

先看第二个参数_getPrototypeOf(SubType).call(this, name) 会不会好奇SubType的原型为什么能用call,回想一下_inherits函数的最后一步,SubType的原型就是SuperType。这一步就可以认为是SuperType.call的过程,而_possibleConstructorReturn 函数检查了第二个参数,如果第二个参数是个对象或者函数,那么说明父类构造法有覆盖实例的情况;然后会通过_assertThisInitialized 函数来校验this 究竟有没有被父类实例化过。按照这样的逻辑,_getPrototypeOf(SubType).call(this, name)这一步理论会对this正确的父类实例化,但是这种校验不是没有道理的。

假设我们将super(name) 注释掉,也许你的编辑器会给你报错,但是这个代码是可以正确翻译的,会变成这样:

var SubType =
/*#__PURE__*/
function (_SuperType) {
  _inherits(SubType, _SuperType);

  function SubType(name, age) {
    var _this;

    _classCallCheck(this, SubType);

    // super(name);
    _this.age = age;
    return _possibleConstructorReturn(_this);
  }

  _createClass(SubType, [{
    key: "sayHi",
    value: function sayHi() {
      console.log("Sub HI!", this.name, this.age);
    }
  }]);

  return SubType;
}(SuperType);

这也解释了为什么会有一个_this出现,就是用来标记this是被父类初始化过的。可以简单的看成_this才是子类实例的一个等待状态,而this 必须要经过父类实例过程,才能使_this真正生效。

总结

至此Babel翻译class+extends基本就介绍完了,上一篇文章基本上是对《高程》第六章的复习,从理解对象,到构造对象,重点把握工厂模式,构造函数模式(new关键字的作用过程),原型以及原型对象(__proto__和prototype)。

JavaScript在ES6中增加了class和extends关键字,作为类型的语法糖。Babel的实现可能更巧妙的处理实例方法。到了继承,先从逻辑上梳理各种继承的模式,核心还是原型链的问题。通过父类实例建立原型关系,通过Object.create建立原型关系等等。Babel翻译的继承采用寄生组合,寄生表现在SuperClass.call的实现方式,Babel还增加了必要的检查,也就是super()是否调用;组合体现在原型链的构建上,Object.create直接建立关系,同时照顾constructor的特性。额外的还建立两个构造方法的原型关系,后面借助这个关系隐式的调用父类构造法,实际操作上还能直接继承父类的静态方法。

标签: none

添加新评论