class
众所周知,JavaScript
是没有类的,class
也只是语法糖,这篇文章旨在于理清咱们经常挂着嘴边的语法糖,究竟指的是什么。javascript
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
中的写法又是如何呢java
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') } }
能够很清晰地看到react
ES6
中 Parent
类的 constructor
对应的就是 ES5
中的构造函数 Parent
;name
和 isAdult
,不管在 ES6
中采用何种写法,在 ES5
中依然都是挂在 this
下;ES6
中经过关键字 static
修饰的静态属性和方法 nation
和 live
,则都被直接挂在类 Parent
上;tought
和 方法 talk
是被挂在 原型对象 Parent.prototype
上的。Babel
是如何进行编译的咱们能够经过将代码输入到 Babel
官网的 Try it out 来查看编译后的代码,这个部分咱们按部就班,一步一步来进行编译,拆解 Babel
的编译过程:git
咱们此时只观察 属性 相关的编译结果,
编译前:es6
class Parent { static nation = 'China' isAdult = true constructor(name) { this.name = name } }
编译后:github
'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 操做调用。express
编译前:segmentfault
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') } }
编译后:babel
'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 原型相关的内容。