何为 class
众所周知,JavaScript
是没有类的,class
也只是语法糖,这篇文章旨在于理清我们常常挂着嘴边的语法糖,究竟指的是什么。
ES6
与 ES5
写法对比
class Parent {
static nation = 'China'
isAdult = true
get thought() {
console.log('Thought in head is translate to Chinese.')
return this._thought
}
set thought(newVal) {
this._thought = newVal
}
constructor(name) {
this.name = name
}
static live() {
console.log('live')
}
talk() {
console.log('talk')
}
}
这是一个很完整的写法,我们已经习惯于这么方便地写出一个类了,那么对应到 ES5
中的写法又是如何呢
function Parent(name) {
this.name = name
this.isAdult = true
}
Parent.nation = 'China'
Parent.live = function() {
console.log('live')
}
Parent.prototype = {
get thought() {
return this._thought
},
set thought(newVal) {
this._thought = newVal
},
talk: function() {
console.log('talk')
}
}
可以很清晰地看到
-
ES6
中Parent
类的constructor
对应的就是ES5
中的构造函数Parent
; - 实例属性
name
和isAdult
,无论在ES6
中采用何种写法,在ES5
中依然都是挂在this
下; -
ES6
中通过关键字static
修饰的静态属性和方法nation
和live
,则都被直接挂在类Parent
上; - 值得注意的是 getter 和 setter
tought
和 方法talk
是被挂在 原型对象Parent.prototype
上的。
Babel
是如何进行编译的
我们可以通过将代码输入到 Babel
官网的 Try it out 来查看编译后的代码,这个部分我们循序渐进,一步一步来进行编译,拆解 Babel
的编译过程:
过程一
我们此时只观察 属性 相关的编译结果,
编译前:
class Parent {
static nation = 'China'
isAdult = true
constructor(name) {
this.name = name
}
}
编译后:
'use strict'
// 封装后的 instanceof 操作
function _instanceof(left, right) {
if (
right != null &&
typeof Symbol !== 'undefined' &&
right[Symbol.hasInstance]
) {
return !!right[Symbol.hasInstance](left)
} else {
return left instanceof right
}
}
// ES6 的 class,必须使用 new 操作来调用,
// 这个方法的作用就是检查是否通过 new 操作调用,使用到了上面封装的 _instanceof 方法
function _classCallCheck(instance, Constructor) {
if (!_instanceof(instance, Constructor)) {
throw new TypeError('Cannot call a class as a function')
}
}
// 封装后的 Object.defineProperty
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
})
} else {
obj[key] = value
}
return obj
}
var Parent = function Parent(name) {
// 检查是否通过 new 操作调用
_classCallCheck(this, Parent)
// 初始化 isAdult
_defineProperty(this, 'isAdult', true)
// 根据入参初始化 name
this.name = name
}
// 初始化静态属性 nation
_defineProperty(Parent, 'nation', 'China')
从编译后的代码中可以发现,Babel
为了其严谨度,封装了一些方法,其中 可能有点迷惑的是 _instanceof(left, right)
这个方法里的 Symbol.hasInsance
,从 MDN 和 ECMAScript6入门 中可以知道,这个属性可以用来自定义 instanceof
操作符在某个类上的行为。这里还有一个重点关注对象 _classCallCheck(instance, Constructor)
,这个方法用来检查是否通过 new 操作调用。
过程二
编译前:
class Parent {
static nation = 'China'
isAdult = true
get thought() {
console.log('Thought in head is translate to Chinese.')
return this._thought
}
set thought(newVal) {
this._thought = newVal
}
constructor(name) {
this.name = name
}
static live() {
console.log('live')
}
talk() {
console.log('talk')
}
}
编译后:
'use strict'
// 封装后的 instanceof 操作
function _instanceof(left, right) {
// .....
}
// ES6 的 class,必须使用 new 操作来调用,
// 这个方法的作用就是检查是否通过 new 操作调用,使用到了上面封装的 _instanceof 方法
function _classCallCheck(instance, Constructor) {
// ......
}
// 封装 Object.defineProperty 来添加属性
function _defineProperties(target, props) {
// 遍历 props
for (var i = 0; i < props.length; i++) {
var descriptor = props[i]
// enumerable 默认为 false
descriptor.enumerable = descriptor.enumerable || false
descriptor.configurable = true
if ('value' in descriptor) descriptor.writable = true
Object.defineProperty(target, descriptor.key, descriptor)
}
}
// 为 Constructor 添加原型属性或者静态属性并返回
function _createClass(Constructor, protoProps, staticProps) {
// 如果是原型属性,添加到原型对象上
if (protoProps) _defineProperties(Constructor.prototype, protoProps)
// 如果是静态属性,添加到构造函数上
if (staticProps) _defineProperties(Constructor, staticProps)
return Constructor
}
// 封装后的 Object.defineProperty
function _defineProperty(obj, key, value) {
// ......
}
var Parent =
/*#__PURE__*/
(function() {
// 添加 getter/setter
_createClass(Parent, [
{
key: 'thought',
get: function get() {
console.log('Thought in head is translate to Chinese.')
return this._thought
},
set: function set(newVal) {
this._thought = newVal
}
}
])
function Parent(name) {
// 检查是否通过 new 操作调用
_classCallCheck(this, Parent)
// 初始化 isAdult
_defineProperty(this, 'isAdult', true)
// 根据入参初始化 name
this.name = name
}
// 添加 talk 和 live 方法
_createClass(
Parent,
[
{
key: 'talk',
value: function talk() {
console.log('talk')
}
}
],
[
{
key: 'live',
value: function live() {
console.log('live')
}
}
]
)
return Parent
})()
// 初始化静态属性 nation
_defineProperty(Parent, 'nation', 'China')
与过程一相比,编译后的代码, Babel
多生成了一个 _defineProperties(target, props)
和 _createClass(Constructor, protoProps, staticProps)
的辅助函数,这两个主要用来添加原型属性和静态属性,并且通过 Object.defineProperty
的方法,对数据描述符和存取描述符都可以进行控制。
值得注意的是,ES6
中的 class
里的所有方法都是不可遍历的(enumerable: false
),这里有一个小细节: 如果有使用 TypeScript
,在设置 compileOptions
中的 target
时,如果设置为 es5
,那么会发现编译后的 方法可以通过 Object.keys()
遍历到,而设置为es6
时就无法被遍历。
总结
Babel
通过 AST
抽象语法树分析,然后添加以下
_instanceof(left, right) // 封装后的 instanceof 操作
_classCallCheck(instance, Constructor) // 检查是否通过 new 操作调用
_defineProperties(target, props) // 封装 Object.defineProperty 来添加属性
_createClass(Constructor, protoProps, staticProps) // 为 Constructor 添加原型属性或者静态属性并返回
_defineProperty(obj, key, value) // // 封装后的 Object.defineProperty
五个辅助函数,来为 Parent
构造函数添加属性和方法,转换 名为 class
的语法糖为 ES5
的代码。
何为 extends
既然 ES6
没有类,那又应该如何实现继承呢,相信聪明的你已经知道了,其实和 class
一样,extends
也是语法糖,接下来我们一步一步接着把这层语法糖也拆开。
ES5
的 寄生组合式继承
从 从 Prototype 开始说起(上)—— 图解 ES5 继承相关 这里知道,相对完美的继承实现是 寄生组合式继承,为了方便阅读,这里再次附上源码和示意例图:
function createObject(o) {
function F() {}
F.prototype = o
return new F()
}
function Parent(name) {
this.name = name
}
function Child(name) {
Parent.call(this, name)
}
Child.prototype = createObject(Parent.prototype)
Child.prototype.constructor = Child
var child = new Child('child')
ES6
和 ES5
写法对比
如果参考上面的继承实现,我们可以轻松地写出两种版本的继承形式
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类的 constructor(name)
this.age = age;
}
}
function Child (name, age) {
Parent.call(this, name)
this.age = age
}
Child.prototype = createObject(Parent.prototype)
Child.prototype.constructor = Child
Babel
是如何进行编译的
一些细节
- 子类必须在
constructor
方法中调用super
方法,否则新建实例时会报错。这是因为子类没有自己的this
对象,而是继承父类的this
对象,然后对其进行加工。如果不调用super
方法,子类就得不到this
对象。也正是因为这个原因,在子类的构造函数中,只有调用
super
之后,才可以使用this
关键字,否则会报错。
- 在
ES6
中,父类的静态方法,可以被子类继承。class
作为构造函数的语法糖,同时有prototype
属性和__proto__
属性,因此同时存在两条继承链。
编译过程
同样的,我们将代码输入到 Babel
官网的 Try it out 来查看编译后的代码:
'use strict'
// 封装后的 typeof
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)
}
// 调用父类的 constructor(),并返回子类的 this
function _possibleConstructorReturn(self, call) {
if (
call &&
(_typeof(call) === 'object' || typeof call === 'function')
) {
return call
}
return _assertThisInitialized(self)
}
// 检查 子类的 super() 是否被调用
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError(
"this hasn't been initialised - super() hasn't been called"
)
}
return self
}
// 封装后的 getPrototypeOf
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)
}
// 封装后的 setPrototypeOf
function _setPrototypeOf(o, p) {
_setPrototypeOf =
Object.setPrototypeOf ||
function _setPrototypeOf(o, p) {
o.__proto__ = p
return o
}
return _setPrototypeOf(o, p)
}
// 检查是否通过 new 操作调用
function _classCallCheck(instance, Constructor) {
if (!_instanceof(instance, Constructor)) {
throw new TypeError('Cannot call a class as a function')
}
}
var Child =
/*#__PURE__*/
(function(_Parent) {
// 继承操作
_inherits(Child, _Parent)
function Child(name, age) {
var _this
_classCallCheck(this, Child)
// 调用父类的 constructor(),并返回子类的 this
_this = _possibleConstructorReturn(
this,
_getPrototypeOf(Child).call(this, name)
)
// 根据入参初始化子类自己的属性
_this.age = age
return _this
}
return Child
})(Parent)
_inherits(subClass, superClass)
我们来细看一下这个实现继承的辅助函数的细节:
function _inherits(subClass, superClass) {
// 1. 检查 extends 的继承目标(即父类),必须是函数或者是 null
if (typeof superClass !== 'function' && superClass !== null) {
throw new TypeError(
'Super expression must either be null or a function'
)
}
// 2. 类似于 ES5 的寄生组合式继承,使用 Object.create,
// 设置子类 prototype 属性的 __proto__ 属性指向父类的 prototype 属性
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true, configurable: true }
})
// 3. 设置子类的 __proto__ 属性指向父类
if (superClass) _setPrototypeOf(subClass, superClass)
}
这个方法主要分为3步,其中第2步,通过寄生组合式继承在实现继承的同时,新增了一个名为 constructor
的不可枚举的属性;第3步实现了上文说的第二条原型链,从而达到静态方法也能被继承的效果。
_possibleConstructorReturn(self, call)
这个辅助函数主要是用来实现 super()
的效果,对应到寄生组合式继承上则是借用构造函数继承的部分,有所不同的是,该方法返回一个 this
并赋给子类的 this
。具体细节可以在 ES6 系列之 Babel 是如何编译 Class 的(下) 查看。
总结
和 class
一样,Babel
通过 AST
抽象语法树分析,然后添加一组辅助函数,在我看来可以分为两类,第一类:
_typeof(obj) // 封装后的 typeof
_getPrototypeOf(o) // 封装后的 getPrototypeOf
_setPrototypeOf(o, p) // 封装后的 setPrototypeOf
这种为了健壮性的功能辅助函数
第二类:
_assertThisInitialized(self) // 检查 子类的 super() 是否被调用
_possibleConstructorReturn(self, call) // 调用父类的 constructor(),并返回子类的 this
_classCallCheck(instance, Constructor) // 检查是否通过 new 操作调用
_inherits(subClass, superClass) // 实现继承的辅助函数
这种为了实现主要功能的流程辅助函数,从而实现更完善的寄生组合式继承。
后记
从 Prototype 开始说起 一共分为两篇,从两个角度来讲述 JavaScript 原型相关的内容。
- 从 Prototype 开始说起(上)—— 图解 ES5 继承相关
- 从 Prototype 开始说起(下)—— ES6 中的 class 与 extends
参考资料
- ES6 系列之 Babel 是如何编译 Class 的(上)
- ES6 系列之 Babel 是如何编译 Class 的(下)