跟着Babel学JS的继承(下)
从对象介绍到继承的几种实现,现在才要真正的看看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的特性。额外的还建立两个构造方法的原型关系,后面借助这个关系隐式的调用父类构造法,实际操作上还能直接继承父类的静态方法。
当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »