ES6 系列之 Babel 是如何编译 Class 的(下)

摘要: ## 前言 在上一篇 [《 ES6 系列 Babel 是如何编译 Class 的(上)》](https://github.com/mqyqingfeng/Blog/issues/105),我们知道了 Babel 是如何编译 Class 的,这篇我们学习 Babel 是如何用 ES5 实现 Class 的继承。 ## ES5 寄生组合式继承 ```js function Pare

前言

在上一篇 《 ES6 系列 Babel 是如何编译 Class 的(上)》,我们知道了 Babel 是如何编译 Class 的,这篇我们学习 Babel 是如何用 ES5 实现 Class 的继承。

ES5 寄生组合式继承

1function Parent (name) { 2 this.name = name; 3} 4 5Parent.prototype.getName = function () { 6 console.log(this.name) 7} 8 9function Child (name, age) { 10 Parent.call(this, name); 11 this.age = age; 12} 13 14Child.prototype = Object.create(Parent.prototype); 15 16var child1 = new Child('kevin', '18'); 17 18console.log(child1);

原型链示意图为:

关于寄生组合式继承我们在 《JavaScript深入之继承的多种方式和优缺点》 中介绍过。

引用《JavaScript高级程序设计》中对寄生组合式继承的夸赞就是:

这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

ES6 extend

Class 通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

以上 ES5 的代码对应到 ES6 就是:

1class Parent { 2 constructor(name) { 3 this.name = name; 4 } 5} 6 7class Child extends Parent { 8 constructor(name, age) { 9 super(name); // 调用父类的 constructor(name) 10 this.age = age; 11 } 12} 13 14var child1 = new Child('kevin', '18'); 15 16console.log(child1);

值得注意的是:

super 关键字表示父类的构造函数,相当于 ES5 的 Parent.call(this)。

子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类就得不到 this 对象。

也正是因为这个原因,在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。

子类的 __proto__

在 ES6 中,父类的静态方法,可以被子类继承。举个例子:

1class Foo { 2 static classMethod() { 3 return 'hello'; 4 } 5} 6 7class Bar extends Foo { 8} 9 10Bar.classMethod(); // 'hello'

这是因为 Class 作为构造函数的语法糖,同时有 prototype 属性和 __proto__ 属性,因此同时存在两条继承链。

(1)子类的 __proto__ 属性,表示构造函数的继承,总是指向父类。

(2)子类 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的 prototype 属性。

1class Parent { 2} 3 4class Child extends Parent { 5} 6 7console.log(Child.__proto__ === Parent); // true 8console.log(Child.prototype.__proto__ === Parent.prototype); // true

ES6 的原型链示意图为:

我们会发现,相比寄生组合式继承,ES6 的 class 多了一个 Object.setPrototypeOf(Child, Parent)的步骤。

继承目标

extends 关键字后面可以跟多种类型的值。

1class B extends A { 2}

上面代码的 A,只要是一个有 prototype 属性的函数,就能被 B 继承。由于函数都有 prototype 属性(除了 Function.prototype 函数),因此 A 可以是任意函数。

除了函数之外,A 的值还可以是 null,当 extend null 的时候:

1class A extends null { 2} 3 4console.log(A.__proto__ === Function.prototype); // true 5console.log(A.prototype.__proto__ === undefined); // true

Babel 编译

那 ES6 的这段代码:

1class Parent { 2 constructor(name) { 3 this.name = name; 4 } 5} 6 7class Child extends Parent { 8 constructor(name, age) { 9 super(name); // 调用父类的 constructor(name) 10 this.age = age; 11 } 12} 13 14var child1 = new Child('kevin', '18'); 15 16console.log(child1);

Babel 又是如何编译的呢?我们可以在 Babel 官网的 Try it out 中尝试:

1'use strict'; 2 3function _possibleConstructorReturn(self, call) { 4 if (!self) { 5 throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 6 } 7 return call && (typeof call === "object" || typeof call === "function") ? call : self; 8} 9 10function _inherits(subClass, superClass) { 11 if (typeof superClass !== "function" && superClass !== null) { 12 throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); 13 } 14 subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); 15 if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 16} 17 18function _classCallCheck(instance, Constructor) { 19 if (!(instance instanceof Constructor)) { 20 throw new TypeError("Cannot call a class as a function"); 21 } 22} 23 24var Parent = function Parent(name) { 25 _classCallCheck(this, Parent); 26 27 this.name = name; 28}; 29 30var Child = function(_Parent) { 31 _inherits(Child, _Parent); 32 33 function Child(name, age) { 34 _classCallCheck(this, Child); 35 36 // 调用父类的 constructor(name) 37 var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name)); 38 39 _this.age = age; 40 return _this; 41 } 42 43 return Child; 44}(Parent); 45 46var child1 = new Child('kevin', '18'); 47 48console.log(child1);

我们可以看到 Babel 创建了 _inherits 函数帮助实现继承,又创建了 _possibleConstructorReturn 函数帮助确定调用父类构造函数的返回值,我们来细致的看一看代码。

_inherits

1function _inherits(subClass, superClass) { 2 // extend 的继承目标必须是函数或者是 null 3 if (typeof superClass !== "function" && superClass !== null) { 4 throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); 5 } 6 7 // 类似于 ES5 的寄生组合式继承,使用 Object.create,设置子类 prototype 属性的 __proto__ 属性指向父类的 prototype 属性 8 subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); 9 10 // 设置子类的 __proto__ 属性指向父类 11 if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 12}

关于 Object.create(),一般我们用的时候会传入一个参数,其实是支持传入两个参数的,第二个参数表示要添加到新创建对象的属性,注意这里是给新创建的对象即返回值添加属性,而不是在新创建对象的原型对象上添加。

举个例子:

1// 创建一个以另一个空对象为原型,且拥有一个属性 p 的对象 2const o = Object.create({}, { p: { value: 42 } }); 3console.log(o); // {p: 42} 4console.log(o.p); // 42

再完整一点:

1const o = Object.create({}, { 2 p: { 3 value: 42, 4 enumerable: false, 5 // 该属性不可写 6 writable: false, 7 configurable: true 8 } 9}); 10o.p = 24; 11console.log(o.p); // 42

那么对于这段代码:

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

作用就是给 subClass.prototype 添加一个可配置可写不可枚举的 constructor 属性,该属性值为 subClass。

_possibleConstructorReturn

函数里是这样调用的:

var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name));

我们简化为:

var _this = _possibleConstructorReturn(this, Parent.call(this, name));

_possibleConstructorReturn 的源码为:

1function _possibleConstructorReturn(self, call) { 2 if (!self) { 3 throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 4 } 5 return call && (typeof call === "object" || typeof call === "function") ? call : self; 6}

在这里我们判断 Parent.call(this, name) 的返回值的类型,咦?这个值还能有很多类型?

对于这样一个 class:

1class Parent { 2 constructor() { 3 this.xxx = xxx; 4 } 5}

Parent.call(this, name) 的值肯定是 undefined。可是如果我们在 constructor 函数中 return 了呢?比如:

1class Parent { 2 constructor() { 3 return { 4 name: 'kevin' 5 } 6 } 7}

我们可以返回各种类型的值,甚至是 null:

1class Parent { 2 constructor() { 3 return null 4 } 5}

我们接着看这个判断:

call && (typeof call === "object" || typeof call === "function") ? call : self;

注意,这句话的意思并不是判断 call 是否存在,如果存在,就执行 (typeof call === "object" || typeof call === "function") ? call : self

因为 && 的运算符优先级高于 ? :,所以这句话的意思应该是:

(call && (typeof call === "object" || typeof call === "function")) ? call : self;

对于 Parent.call(this) 的值,如果是 object 类型或者是 function 类型,就返回 Parent.call(this),如果是 null 或者基本类型的值或者是 undefined,都会返回 self 也就是子类的 this。

这也是为什么这个函数被命名为 _possibleConstructorReturn

总结

1var Child = function(_Parent) { 2 _inherits(Child, _Parent); 3 4 function Child(name, age) { 5 _classCallCheck(this, Child); 6 7 // 调用父类的 constructor(name) 8 var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name)); 9 10 _this.age = age; 11 return _this; 12 } 13 14 return Child; 15}(Parent);

最后我们总体看下如何实现继承:

首先执行 _inherits(Child, Parent),建立 Child 和 Parent 的原型链关系,即 Object.setPrototypeOf(Child.prototype, Parent.prototype) 和 Object.setPrototypeOf(Child, Parent)

然后调用 Parent.call(this, name),根据 Parent 构造函数的返回值类型确定子类构造函数 this 的初始值 _this。

最终,根据子类构造函数,修改 _this 的值,然后返回该值。

ES6 系列

ES6 系列目录地址:https://github.com/mqyqingfeng/Blog

ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。

原文链接

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )