热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:从Babel转译过程浅谈ES6实现继承的原理

篇首语:本文由编程笔记#小编为大家整理,主要介绍了从Babel转译过程浅谈ES6实现继承的原理相关的知识,希望对你有一定的参考价值。都说ES6的Class

篇首语:本文由编程笔记#小编为大家整理,主要介绍了从 Babel 转译过程浅谈 ES6 实现继承的原理相关的知识,希望对你有一定的参考价值。


都说 ES6 的 Class 是 ES5 的语法糖,那么 ES6 的 Class 是如何实现的呢?其实现继承的原理又是什么呢?不妨我们通过 Babel 转译代码的方式,看看其中有什么门道。

这篇文章会从最简单的代码入手,一步步剖析相关的原理以及每个函数的作用。代码的转译直接在 Babel 官网进行即可。

ES6 的 Class 是如何实现的

先从最简单的一个 Parent 类看起:

class Parent{
constructor(){
this.a = 1
this.getA = function(){}
}
}

转译之后的结果是:

function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Parent = function Parent() {
"use strict";
_classCallCheck(this, Parent);
this.a = 1;
this.getA = function () {};
};

可以看到,这里的类实质上就是 ES5 中的构造函数,除了添加实例属性和实例方法之外,它还调用了一个 _classCallCheck 函数。

_classCallCheck 函数

这个函数会接受一个实例和构造函数作为参数,内部的 instance instanceof Constructor 用于判断这个类是不是通过 new 调用的,如果不是就抛出一个错误。

接下来我们尝试给这个类添加原型方法和静态方法:

class Parent{
constructor(){
this.a = 1
this.getA = function(){}
}
getB(){}
getC(){}
static getD(){}
static getE(){}
}

转译后得到:

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 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 Parent = /*#__PURE__*/ (function () {
"use strict";
function Parent() {
_classCallCheck(this, Parent);
this.a = 1;
this.getA = function () {};
}
_createClass(
Parent,
[
{
key: "getB",
value: function getB() {}
},
{
key: "getC",
value: function getC() {}
}
],
[
{
key: "getD",
value: function getD() {}
},
{
key: "getE",
value: function getE() {}
}
]
);
return Parent;
})();

emmm 看起来好像有点复杂,不过没关系,我们一个一个函数理清楚就行了。

可以看到,此时的 Parent 变成了一个 IIFE,IIFE 执行之后仍然是返回 Parent 类,但内部还封装了一个 _createClass 函数的调用。

_createClass 函数

_createClass 函数做了什么事呢?首先,它可以接受三个参数:

  • 第一个参数: 类(这里是 Parent 类)
  • 第二个参数:存放对象的数组,每个对象都是关于类的原型方法的特性描述对象(这里是 getBgetC
  • 第三个参数:存放对象的数组,每个对象都是关于类的静态方法的特性描述对象(这里是 getDgetE

接着,它会依次检查是否有传第二个和第三个参数,如果有,就调用 _defineProperties 函数,分别为类的原型定义原型方法,为类本身定义静态方法。

_defineProperties 函数

_defineProperties 函数做了什么事呢?它接受类(或者类的原型)和一个存放对象的数组作为参数,之后遍历数组中的每个对象,定义每个方法的特性,并将它们逐一添加到类(或者类的原型)上面。这里涉及到的特性包括:

  • enumberable:该属性(方法)是否可枚举。如果方法本身已经定义了该特性,则采用该特性;如果没有定义,则定义该方法为不可枚举
  • configurable:该属性(方法)是否可以配置
  • writable:如果该属性是数据属性而不是访问器属性,那么会有一个 value,此时设置该属性为可写

ES6 的继承是如何实现的

好了,基本搞清楚一个 class 的原理之后,现在我们来看一下 ES6 是如何实现继承的。

将下面的代码进行转译:

class Parent{
constructor(){
this.a = 1
this.getA = function(){}
}
getB(){}
getC(){}
static getD(){}
static getE(){}
}
class Son extends Parent{
constructor(){
super()
}
}

就得到了:

"use strict";
function _typeof(obj) {
"@babel/helpers - typeof";
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 _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 _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 _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 _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf
? Object.getPrototypeOf
: function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
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 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 Parent = /*#__PURE__*/ (function () {
function Parent() {
_classCallCheck(this, Parent);
this.a = 1;
this.getA = function () {};
}
_createClass(
Parent,
[
{
key: "getB",
value: function getB() {}
},
{
key: "getC",
value: function getC() {}
}
],
[
{
key: "getD",
value: function getD() {}
},
{
key: "getE",
value: function getE() {}
}
]
);
return Parent;
})();
var Son = /*#__PURE__*/ (function (_Parent) {
_inherits(Son, _Parent);
var _super = _createSuper(Son);
function Son() {
_classCallCheck(this, Son);
return _super.call(this);
}
return Son;
})(Parent);

emmm 好像越来越复杂了,没事,我们先稍稍简化一下(前面解释过的函数这里就直接略过了),再一个一个慢慢分析:

"use strict";
function _typeof(obj) { ... }
function _inherits(subClass, superClass) { ... }
function _setPrototypeOf(o, p) { ... }
function _createSuper(Derived) { ... }
function _possibleConstructorReturn(self, call) { ... }
function _assertThisInitialized(self) { ... }
function _isNativeReflectConstruct() { ... }
function _getPrototypeOf(o) { ... }
function _classCallCheck() { ... }
function _defineProperties() { ... }
function _createClass() { ... }
var Parent = /*#__PURE__*/ (function () {
function Parent() { ... }
_createClass(...);
return Parent;
})();
var Son = /*#__PURE__*/ (function (_Parent) {
_inherits(Son, _Parent);
var _super = _createSuper(Son);
function Son() {
_classCallCheck(this, Son);
return _super.call(this);
}
return Son;
})(Parent);

这里多出了很多新的函数,有的函数不是我们讨论的重点,而且也完全可以单独拎出来分析,所以这里先简单把它们的作用介绍了,之后如果忘记了函数的作用,翻到这里来看即可。

_typeof(obj)

function _typeof(obj) {
"@babel/helpers - typeof";
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);
}

这是 Babel 引入的一个工具函数,主要是为了对 Symbol 进行正确的处理。它首先会检查当前环境是否支持原生的 Symbol,如果支持就直接返回 typeof obj 表达式的计算结果;如果不支持,再检查 obj 是不是通过 polyfill 实现的 Symbol 的一个实例,如果是就返回它的类型(也就是返回 "symbol"),如果不是,就返回 typeof obj 的计算结果。在这里,这个函数假定了我们当前的环境是原生支持 Symbol 或者通过 polyfill 实现了支持的。

_setPrototypeOf()

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

首先检查当前环境是否支持直接调用 Object.setPrototypeOf() 方法,如果不支持,就通过 __proto__ 手动给实例建立原型关系( __proto__ 是一个暴露的内部属性,一般不提倡直接进行操作)。

_possibleConstructorReturn(self,call)

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

如果你看过 new 或者 [[Construct]] 的内部实现,就会知道,给构造函数指定了一个非空对象或者函数作为返回值之后,调用函数之后返回的将不是实例,而是这个对象或者函数。这里就是通过 _possibleConstructorReturn 这个函数来完成这件事的 —— 仔细看它的名字,意思不就是“构造函数可能返回的值”吗?

这个函数接受两个参数,self 代表构造函数的实例,call 代表构造函数的返回值。内部的判断也很简单,call && (_typeof(call) === "object" || typeof call === "function") 是检查 call 的类型,当它是一个对象(注意这里是使用 typeof 进行检查,需要排除可能为 null 的情况)或者函数的时候,直接将其作为返回值;否则就返回 _assertThisInitialized(self)。等等,怎么又来了一个新函数呢?不要急,我们接着就来看这个函数是干什么用的。

_assertThisInitialized(self)

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

看这个函数的名字 —— “断言 this 已经初始化”,也就是说,在调用这个方法的时候,我们期望的结果是 this 已经得到初始化了。这里如果检查发现 thisundefined,就会抛出一个错误,提示我们由于没有调用 super(),所以无法得到 this;否则就返回 this 。为什么要使用 void 0 而不是 undefined 呢?因为非严格模式下 undefined 可能会被重写,这里使用 void 0 更加保险。

_isNativeReflectConstruct()

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

这个方法用于检测当前环境是否支持原生的 Reflect。为什么要做这个检查呢?后面我们再来解释。

好了,我们已经分析了这几个函数的作用,现在直接翻到最下面的代码,从 Son 子类看起:

var Son = /*#__PURE__*/ (function (_Parent) {
_inherits(Son, _Parent);
var _super = _createSuper(Son);
function Son() {
_classCallCheck(this, Son);
return _super.call(this);
}
return Son;
})(Parent);

这里的 Son 同样是一个 IIFE,并且实际上也是返回一个 Son 子类构造函数,不同的是,它内部还封装了其它方法的调用。我们逐一看一下这些方法的作用。

_inherits(Son,_Parent)

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

_inherit 是实现继承的其中一个核心方法,可以说它的本质就是 ES5 中的寄生组合式继承。这个方法接受一个父类和子类作为参数,首先会检查父类是不是函数或者 null,如果不是,则抛出错误(为什么父类可以是 null?从 extends 看 JS 继承这篇文章进行了解释,这里我就不重复了)。

接着,调用 Object.create 设置父类的原型为子类原型的 __proto__。这里我们会看到还传入了第二个参数,这个参数是子类原型的属性的特性描述对象(descriptor),我们对 constructor 属性进行了设置,将它设置为可写、可配置,同时利用 value 修复了因重写子类原型而丢失的 constructor 指向。为什么不设置 enumerable: false 呢?因为默认就是不可枚举的,不设置也行。

最后,我们设置子类的 __proto__ 指向父类,这是 ES5 中没有的,目的是让子类继承父类的静态方法(可以直接通过类调用的方法)。

可以看到,通过调用 _inherit 函数,我们已经成功让子类继承了父类的原型方法和静态方法。不过,实例上的属性怎么继承呢?这就要继续往下看了,接下来我们调用 _createSuper() 函数并传入派生类(子类),这不是重点,重点是它创建并返回的另一个函数 _super

_super.call(this)

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

这里的 _createSuperInternal 就是 _super,调用的时候我们绑定了其内部的 this 为子类实例。

它首先会根据之前的 _isNativeReflectConstruct 检查当前环境是否支持 Reflect,如果支持,则执行 result = Reflect.construct(Super, arguments, NewTarget),否则执行 result = Super.apply(this, arguments)

解释一下这里为什么要优先使用 Reflect。当执行 Reflect.construct(Super, arguments, NewTarget)的时候,最终会返回一个基于 Super 父类构造函数创建的实例,相当于执行了 new Super(...arguments),但是,这个实例的 __proto__constructorNewTarget,因此在某种程度上,你也可以说这就是一个子类实例,不过它拥有父类实例的所有属性。

可能你会说,这和下面的 Super.apply (借用构造函数继承)不是没区别吗?非也。我们使用 Super.apply 的时候,其实 new.target 属性是会丢失的,就像下面这样:

function Super(){
console.log(new.target)
}
new Super() // Super
Super.apply({}) // undefined

但是如果使用 Reflect.consturct 来创建对象,则 new.target 不会丢失:

function Super1(){
console.log(\'Super1\')
console.log(new.target)
}
function Super2(){
console.log(\'Super2\')
console.log(new.target)
}
const obj1 = Reflect.construct(Super1,{})
// \'Super1\'
// Super1
const obj2 = Reflect.construct(Super1,{},Super2)
// \'Super1\'
// Super2

可以看到,即便没有通过 new 去调用 Super1new.target 也仍然指向 Super1;而在传了第三个参数之后,new.target 也没有丢失,只是指向了 Super2(前面我们说过了,某种程度上,可以说 obj1 就是 Super2 的实例)。

所以,这里优先使用 Reflect,是为了保证 new.target 不会丢失。

之后,result 可能有三种取值:

  • 一个继承了父类实例所有属性的子类实例
  • 父类构造函数的调用结果,可能是父类构造函数中自定义返回的一个非空对象
  • 父类构造函数的调用结果,可能是默认返回的 undefined

如何处理这些不同的情况呢?这里调用了前面讲过的 _possibleConstructorReturn(this,result)函数,如果判断 result 是一个非空对象,也就是第一种和第二种取值情况,那么就直接返回 result;否则就是第三种情况了,此时就对当初传进去的子类实例(已经通过 Super.apply 对它进行了增强),也就是 this,进行断言,然后返回出去。

现在,让我们再回到 Son 构造函数。可以看到,调用它之后返回的正是 _super.call(this),也就是返回 result 或者经过增强的this。这里的 result 我们知道也有两种取值,如果是一个继承了父类实例所有属性的子类实例,那么实际上等价于经过增强的 this;如果是父类构造函数中自定义返回的一个非空对象,则意味着调用 Son构造函数之后返回的对象实际上并没有继承父类中声明的实例属性。类似下面这样:

function Parent(){
this.a = 1
return {b:1}
}
function Son(){
return Parent.call(this)
}
Son.prototype.__proto__ = Parent.prortotype
const obj = new Son()
console.log(obj)
// {b:1}
// 这里 `Son` 同样也是返回父类 `Parent` 的调用结果(一个对象),它并没有继承在父类上声明的实例属性 `a`。

到这里,我们的分析基本就结束了。希望你阅读完本文之后有所收获,若发现文章有错误,也欢迎评论区指正。


推荐阅读
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • C# 7.0 新特性:基于Tuple的“多”返回值方法
    本文介绍了C# 7.0中基于Tuple的“多”返回值方法的使用。通过对C# 6.0及更早版本的做法进行回顾,提出了问题:如何使一个方法可返回多个返回值。然后详细介绍了C# 7.0中使用Tuple的写法,并给出了示例代码。最后,总结了该新特性的优点。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • IhaveconfiguredanactionforaremotenotificationwhenitarrivestomyiOsapp.Iwanttwodiff ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • 给定一个二维平面上的一些点,通过计算曼哈顿距离,求连接所有点的最小总费用。只有任意两点之间有且仅有一条简单路径时,才认为所有点都已连接。给出了几个示例并给出了对应的输出。 ... [详细]
  • 本文介绍了在Linux下安装Perl的步骤,并提供了一个简单的Perl程序示例。同时,还展示了运行该程序的结果。 ... [详细]
author-avatar
sawachan_107
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有