这是JS 原生方法原理探究系列的第六篇文章。
都说 ES6 的 Class 是 ES5 的语法糖,那么 ES6 的 Class 是如何实现的呢?其实现继承的原理又是什么呢?不妨我们通过 Babel 转译代码的方式,看看其中有什么门道。
这篇文章会从最简单的代码入手,一步步剖析相关的原理以及每个函数的作用。代码的转译直接在 Babel 官网进行即可。
先从最简单的一个 Parent 类看起:
转译之后的结果是:
可以看到,这里的类实质上就是 ES5 中的构造函数,除了添加实例属性和实例方法之外,它还调用了一个 _classCallCheck
函数。
_classCallCheck
函数
这个函数会接受一个实例和构造函数作为参数,内部的 instance instanceof Constructor
用于判断这个类是不是通过 new 调用的,如果不是就抛出一个错误。
接下来我们尝试给这个类添加原型方法和静态方法:
转译后得到:
emmm 看起来好像有点复杂,不过没关系,我们一个一个函数理清楚就行了。
可以看到,此时的 Parent
变成了一个 IIFE,IIFE 执行之后仍然是返回 Parent 类,但内部还封装了一个 _createClass
函数的调用。
_createClass
函数
_createClass
函数做了什么事呢?首先,它可以接受三个参数:
Parent
类)getB
和 getC
)getD
和 getE
)接着,它会依次检查是否有传第二个和第三个参数,如果有,就调用 _defineProperties
函数,分别为类的原型定义原型方法,为类本身定义静态方法。
_defineProperties
函数
_defineProperties
函数做了什么事呢?它接受类(或者类的原型)和一个存放对象的数组作为参数,之后遍历数组中的每个对象,定义每个方法的特性,并将它们逐一添加到类(或者类的原型)上面。这里涉及到的特性包括:
enumberable
:该属性(方法)是否可枚举。如果方法本身已经定义了该特性,则采用该特性;如果没有定义,则定义该方法为不可枚举configurable
:该属性(方法)是否可以配置writable
:如果该属性是数据属性而不是访问器属性,那么会有一个 value
,此时设置该属性为可写好了,基本搞清楚一个 class 的原理之后,现在我们来看一下 ES6 是如何实现继承的。
将下面的代码进行转译:
就得到了:
emmm 好像越来越复杂了,没事,我们先稍稍简化一下(前面解释过的函数这里就直接略过了),再一个一个慢慢分析:
这里多出了很多新的函数,有的函数不是我们讨论的重点,而且也完全可以单独拎出来分析,所以这里先简单把它们的作用介绍了,之后如果忘记了函数的作用,翻到这里来看即可。
_typeof(obj)
这是 Babel 引入的一个工具函数,主要是为了对 Symbol
进行正确的处理。它首先会检查当前环境是否支持原生的 Symbol
,如果支持就直接返回 typeof obj
表达式的计算结果;如果不支持,再检查 obj
是不是通过 polyfill 实现的 Symbol
的一个实例,如果是就返回它的类型(也就是返回 "symbol"
),如果不是,就返回 typeof obj
的计算结果。在这里,这个函数假定了我们当前的环境是原生支持 Symbol
或者通过 polyfill 实现了支持的。
_setPrototypeOf()
首先检查当前环境是否支持直接调用 Object.setPrototypeOf()
方法,如果不支持,就通过 __proto__
手动给实例建立原型关系( __proto__
是一个暴露的内部属性,一般不提倡直接进行操作)。
_possibleConstructorReturn(self,call)
如果你看过 new
或者 [[Construct]]
的内部实现,就会知道,给构造函数指定了一个非空对象或者函数作为返回值之后,调用函数之后返回的将不是实例,而是这个对象或者函数。这里就是通过 _possibleConstructorReturn
这个函数来完成这件事的 —— 仔细看它的名字,意思不就是“构造函数可能返回的值”吗?
这个函数接受两个参数,self
代表构造函数的实例,call
代表构造函数的返回值。内部的判断也很简单,call && (_typeof(call) === "object" || typeof call === "function")
是检查 call
的类型,当它是一个对象(注意这里是使用 typeof
进行检查,需要排除可能为 null
的情况)或者函数的时候,直接将其作为返回值;否则就返回 _assertThisInitialized(self)
。等等,怎么又来了一个新函数呢?不要急,我们接着就来看这个函数是干什么用的。
_assertThisInitialized(self)
看这个函数的名字 —— “断言 this
已经初始化”,也就是说,在调用这个方法的时候,我们期望的结果是 this
已经得到初始化了。这里如果检查发现 this
是 undefined
,就会抛出一个错误,提示我们由于没有调用 super()
,所以无法得到 this
;否则就返回 this
。为什么要使用 void 0
而不是 undefined
呢?因为非严格模式下 undefined
可能会被重写,这里使用 void 0
更加保险。
_isNativeReflectConstruct()
这个方法用于检测当前环境是否支持原生的 Reflect
。为什么要做这个检查呢?后面我们再来解释。
好了,我们已经分析了这几个函数的作用,现在直接翻到最下面的代码,从 Son
子类看起:
这里的 Son
同样是一个 IIFE,并且实际上也是返回一个 Son
子类构造函数,不同的是,它内部还封装了其它方法的调用。我们逐一看一下这些方法的作用。
_inherits(Son,_Parent)
_inherit
是实现继承的其中一个核心方法,可以说它的本质就是 ES5 中的寄生组合式继承。这个方法接受一个父类和子类作为参数,首先会检查父类是不是函数或者 null
,如果不是,则抛出错误(为什么父类可以是 null
?从 extends 看 JS 继承这篇文章进行了解释,这里我就不重复了)。
接着,调用 Object.create
设置父类的原型为子类原型的 __proto__
。这里我们会看到还传入了第二个参数,这个参数是子类原型的属性的特性描述对象(descriptor),我们对 constructor
属性进行了设置,将它设置为可写、可配置,同时利用 value
修复了因重写子类原型而丢失的 constructor
指向。为什么不设置 enumerable: false
呢?因为默认就是不可枚举的,不设置也行。
最后,我们设置子类的 __proto__
指向父类,这是 ES5 中没有的,目的是让子类继承父类的静态方法(可以直接通过类调用的方法)。
可以看到,通过调用 _inherit
函数,我们已经成功让子类继承了父类的原型方法和静态方法。不过,实例上的属性怎么继承呢?这就要继续往下看了,接下来我们调用 _createSuper()
函数并传入派生类(子类),这不是重点,重点是它创建并返回的另一个函数 _super
。
_super.call(this)
这里的 _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__
的 constructor
是 NewTarget
,因此在某种程度上,你也可以说这就是一个子类实例,不过它拥有父类实例的所有属性。
可能你会说,这和下面的 Super.apply
(借用构造函数继承)不是没区别吗?非也。我们使用 Super.apply
的时候,其实 new.target 属性是会丢失的,就像下面这样:
但是如果使用 Reflect.consturct
来创建对象,则 new.target
不会丢失:
可以看到,即便没有通过 new
去调用 Super1
,new.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
构造函数之后返回的对象实际上并没有继承父类中声明的实例属性。类似下面这样:
到这里,我们的分析基本就结束了。希望你阅读完本文之后有所收获,若发现文章有错误,也欢迎评论区指正。