在上一篇文章中,咱们提到 ES6 的 class
语法糖是个近乎完美的方案,而且讲解了实现继承的许多内部机制,如 prototype
/__proto__
/constructor
等等。这篇,咱们就以实际的 babel 代码为例子,来验证上节所言不虚。此外,本文还解释了 React 组件中你须要 bind
一下类方法的原理所在。javascript
原文连接:blog.linesh.tw/#/post/2018…java
Github:github.com/linesh-simp…react
class
+ 字段声明class
+ 方法声明class
+ 字段声明先来看个最简单的例子,咱们仅仅使用了 class
关键字并定义了一个变量:git
class Animal {
constructor(name) {
this.name = name || 'Kat'
}
}
复制代码
最后 babel 编译出来的代码以下。这里笔者用的是 Babel 6 的稳定版 6.26,不一样版本编译出来可能有差别,但不至于有大的结构变更。github
'use strict'
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError('Cannot call a class as a function')
}
}
var Animal = function Animal(name) {
_classCallCheck(this, Animal)
this.name = name || 'Kat'
}
复制代码
确实十分简单,对吧。这段代码值得留意的点有两个:express
一个是,使用 class
声明的 Animal
最后实际上是被编译为一个函数。证实 class
跟类不要紧,只是个语法糖。数组
另外一个地方是,编译器帮咱们插入了一个 _classCallCheck
函数调用,它会检查你有没有用 new Animal()
操做符来初始化这个函数。如有,则 this
会是被实例化的 Animal
对象,天然能经过 animal instanceof Animal
检查;如果直接调用函数,this
会被初始化为全局对象,天然不会是 Animal
实例,从而抛出运行时错误。这个检查,正解决了上一篇文章提到的问题:若是忘记使用 new
去调用一个被设计构造函数的函数,没有任何运行时错误的毛病。babel
class
+ 方法声明让咱们再扩展一下例子,给它加两个方法。闭包
class Animal {
constructor(name) {
this.name = name || 'Kat'
}
move() {}
getName() {
return this.name
}
}
复制代码
'use strict'
var _createClass = (function() {
function defineProperties(target, props) {
for (var i = 0; i < props.length; 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)
}
}
return function(Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps)
if (staticProps) defineProperties(Constructor, staticProps)
return Constructor
}
})()
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError('Cannot call a class as a function')
}
}
var Animal = (function() {
function Animal(name) {
_classCallCheck(this, Animal)
this.name = name || 'Kat'
}
_createClass(Animal, [
{
key: 'move',
value: function move() {},
},
{
key: 'getName',
value: function getName() {
return this.name
},
},
])
return Animal
})()
复制代码
例子长了很多,但其实主要的变化只有两个:一是 Animal
被包了一层而不是直接返回;二是新增的方法 move
和 getName
是经过一个 _createClass()
方法来实现的。它将两个方法以 key
/value
的形式做为数组传入,看起来,是要把它们设置到 Animal
的原型链上面,以便后续继承之用。函数
为啥 Animal
被包了一层呢,这是个好问题,但答案咱们将留到后文揭晓。如今,咱们先看一下这个长长的 _createClass
实现是什么:
var _createClass = (function() {
function defineProperties(target, props) {
for (var i = 0; i < props.length; 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)
}
}
return function(Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps)
if (staticProps) defineProperties(Constructor, staticProps)
return Constructor
}
})()
复制代码
它是个当即执行函数,执行又返回了另外一个函数。说明啥,必定用了闭包,说明里面要封装些「私有」变量,那就是 defineProperties
这个函数。这很好,一是这个函数只会生成一次,二是明确了这个函数只与 _createClass
这个事情相关。
再细看这个返回的函数,接受 Constructor
、protoProps
和 staticProps
三个参数。staticProps
咱们暂时不会用到,回头再讲;咱们传入的数组是经过 protoProps
接受的。接下来,看一下 defineProperties
作了啥事。
它将每个传进来的 props 作了以下处理:分别设置了他们的 enumerable
、configurable
、writable
属性。而传进来的 target
是 Animal.prototype
,至关于,这个函数最后的执行效果会是这样:
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
// 前面处理其实获得这样这个 descriptor 对象:
var descriptor = {
...props[i],
enumerable: false,
configurable: true,
writable: true,
}
Object.defineProperty(target, descriptor.key, descriptor)
}
}
复制代码
看到这里就很明白了,它就是把你定义的 move
、getName
方法经过 Object.defineProperty
方法设置到 Animal.prototype
上去。前面咱们说过,prototype
是用来存储公共属性的。也就是说,这两个方法在你使用继承的时候,能够被子对象经过原型链上溯访问到。也就是说,咱们这个小小的例子里,声明的两个方法已经具有了继承能力了。
至于 enumerable
、configurable
、writable
属性是什么东西呢,查一下语言规范就知道了。简单来讲,writable
为 false
时,其值不能经过 setter
改变;enumerable
为 false
时,不能出如今 for-in
循环中。固然,这里是粗浅的理解,暂时不是这篇文章的重点。
class Animal {
constructor(name) {
this.name = name || 'Kat'
}
}
class Tiger extends Animal {
constructor(name, type) {
super(name)
this.type = type || 'Paper'
}
}
复制代码
加一层继承和字段覆盖能看到啥东西呢?能看到继承底下的实现机制是怎么样的,以及它的 constructor
和 __proto__
属性将如何被正确设置。带着这两个问题,咱们一块儿来看下编译后的源码:
'use strict'
function _possibleConstructorReturn(self, call) {
if (!self) {
throw new ReferenceError(
"this hasn't been initialised - super() hasn't been called"
)
}
return call && (typeof call === 'object' || typeof call === 'function')
? call
: self
}
function _inherits(subClass, superClass) {
if (typeof superClass !== 'function' && superClass !== null) {
throw new TypeError(
'Super expression must either be null or a function, not ' +
typeof superClass
)
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true,
},
})
if (superClass)
Object.setPrototypeOf
? Object.setPrototypeOf(subClass, superClass)
: (subClass.__proto__ = superClass)
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError('Cannot call a class as a function')
}
}
var Animal = function Animal(name) {
_classCallCheck(this, Animal)
this.name = name || 'Kat'
}
var Tiger = (function(_Animal) {
_inherits(Tiger, _Animal)
function Tiger(name, type) {
_classCallCheck(this, Tiger)
var _this = _possibleConstructorReturn(
this,
(Tiger.__proto__ || Object.getPrototypeOf(Tiger)).call(this, name)
)
_this.type = type || 'Paper'
return _this
}
return Tiger
})(Animal)
复制代码
相比无继承的代码,这里主要增长了几个函数。_possibleConstructorReturn
顾名思义,可能不是很重要,回头再读。精华在 _inherits(Tiger, Animal)
这个函数,咱们按顺序来读一下。首先是一段异常处理,简单地检查了 superClass
要么是个函数,要么得是个 null。也就是说,若是你这样写那是不行的:
const Something = 'not-a-function'
class Animal extends Something {}
// Error: Super expression must either be null or a function, not string
复制代码
接下来这句代码将 prototype
和 constructor
一并设置到位,是精华。注意,这个地方留个问题:为何要用 Object.create(superClass.prototype)
,而不是直接这么写:
function _inherits(subClass, superClass) {
subClass.prototype = superClass && superClass.prototype
subClass.prototype.constructor = { ... }
}
复制代码
很明显,是为了不任何对 subClass.prototype
的修改影响到 superClass.prototype
。使用 Object.create(asPrototype)
出来的对象,其实上是将 subClass.prototype.__proto__ = superClass.prototype
,这样 subClass
也就继承了 superClass
,能够达到这样两个目的:
subClass
上没有的属性时,会自动往 superClass
上找;这样 superClass.prototype
原型上发生的修改都能实时反映到 subClass
上subClass.prototype
自己是个新的对象,能够存放 subClass
本身的属性,这样 subClass.prototype
上的任何修改不会影响到 superClass.prototype
最后,若是 superClass
不为空,那么将 subClass.__proto__
设置为 superClass
。这点我并非很理解。
至此,一个简单的继承就完成了。在使用了 extends
关键字后,实际上背后发生的事情是:
prototype
上的 __proto__
被正确设置,指向父「类」的 prototype
: subClass.prototype = { __proto__: superClass.prototype }
prototype
上的 constructor
被正确初始化,这样 instanceof
关系能获得正确结果好,要点看完了。后面内容跟继承关系不大,但既然源码扒都扒了,咱们不妨继续深刻探索一些场景:
看一个简单的代码:
class Animal {
static create() {
return new Animal()
}
}
复制代码
首先要知道,这个「静态」一样不是强类型类继承语言里有的「静态」的概念。所谓静态,就是说它跟实例是不要紧的,而跟「类」自己有关系。好比,你能够这样调用:Animal.create()
,但不能这样用:new Animal().create
。什么场景下会用到这种模式呢?好比说:
Object.create
、Object.keys
等经常使用方法既然只有经过构造函数自己去调用,而不能经过实例来调用,指望它们被绑定到函数自己上彷佛很天然。咱们来看看上面这段代码将被如何编译:
'use strict'
var _createClass = (function() {
function defineProperties(target, props) {
for (var i = 0; i < props.length; 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)
}
}
return function(Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps)
if (staticProps) defineProperties(Constructor, staticProps)
return Constructor
}
})()
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError('Cannot call a class as a function')
}
}
var Animal = (function() {
function Animal() {
_classCallCheck(this, Animal)
}
_createClass(Animal, null, [
{
key: 'create',
value: function create() {},
},
])
return Animal
})()
复制代码
熟悉的函数,熟悉的配方。与本文的第二个例子相比,仅有一个地方的不一样:create
方法是做为 _createClass
方法的第三个参数被传入的,这正是咱们上文提到的 staticProps
参数:
var _createClass = (function() {
function defineProperties(target, props) { ... }
return function(Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps)
if (staticProps) defineProperties(Constructor, staticProps)
return Constructor
}
})()
_createClass(Animal, null, [
{
key: 'create',
value: function create() {},
},
])
复制代码
能够看见,create
方法是直接被建立到 Animal
上的:defineProperties(Animal, [{ key: 'create', value: function() {} }])
,最终会将函数赋给 Animal.create
。咱们的猜想并无错误。
class Tiger {
static TYPE = 'REAL'
}
复制代码
还有个小例子。若是是静态变量的话,一样由于不但愿在实例对象上所使用,咱们会看到编译出来的代码中它是直接被设置到函数上。代码已经很熟悉,没必要再讲。
'use strict'
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError('Cannot call a class as a function')
}
}
var Tiger = function Tiger() {
_classCallCheck(this, Tiger)
}
Tiger.TYPE = 'REAL'
复制代码
有趣的是,静态变量会不会被「子类」继承呢?这个可请读者本身作个实验,验证验证。
写 React 的东西,必定碰见过这个问题:
class Button extends React.Component {
constructor() {
super()
this.state = {
isToggleOn: true,
}
// 画重点 👇👇👇👇👇👇👇👇👇👇👇👇
this.toggleButton = this.toggleButton.bind(this)
}
static propTypes = {
text: PropTypes.string,
}
// ❌❌❌ Uncaught TypeError: this.setState is not a function
toggleButton() {
this.setState({
isToggleOn: !this.state.isToggleOn,
})
}
render() {
return <button onClick={this.toggleButton}>Toggle Me</button>
}
}
复制代码
为何会有这个问题呢?由于你扔进去的 this.toggleButton
函数,在 button
内部必定是经过 onClick()
这样的方式来调用的,这样的话,this
引用就会丢失为 undefined
,那么 React.Component
上的 setState
就调用不到。
能够直接去 React 官方示例看看:codepen.io/gaearon/pen…
class Button extends React.Component {
...
// ✅✅✅ This will work!
toggleButton = () => {
this.setState({ ... })
}
...
}
复制代码
解决方案呢,天然也有不少种,好比引用 @autobind
、使用 ES7 的 ::this.toggleButton
、使用箭头函数等。好比上面 👆 这种最经常使用的解决方案。那么同窗们有没有想过这个问题,为何这样写 this
应用就能够正确拿到呢?「由于箭头函数将 this
绑定到词法做用域的上下文中了呀~」那谁来给我解释一下这句话呢?反正我是历来没理解过这个「外层」的做用域,应该是绑定到哪里。所以,只好另辟路径,直接看源码来理解这个写法的含义。
我写了个简单的例子,足以复现这个问题:
class Button {
constructor() {
this.value = 1
}
increment = () => {
this.value += 2
}
render() {
const onClick = this.increment
onClick()
}
}
复制代码
当咱们调用 render()
时,increment()
这样的调用方式会使 this
引用没法被初始化,这也正是咱们传入的 onClick
在 React 中会被调用的方式。而上图的 increment
写法能够从新拯救失去的 this
引用!让咱们来看看源代码,一探究竟。
'use strict'
var _createClass = (function() {})()
function _classCallCheck(instance, Constructor) {}
var Button = (function() {
function Button() {
var _this = this
_classCallCheck(this, Button)
this.increment = function() {
_this.value += 2
}
this.value = 1
}
_createClass(Button, [
{
key: 'render',
value: function render() {
var increment = this.increment
increment()
},
},
])
return Button
})()
复制代码
我略去了你们耳熟能详的代码,只留下关键的部分。能够看到,编译后的代码中,对 Button
实例的 this
引用被闭包保存了下来!这种写法,与之前咱们 var that = this
的写法是一致的,我也终于理解「再也不须要 that 引用了」以及各类语焉不详的做用域啊最外层变量啊这些理论。其实,就是 this
引用会始终被绑定到构造函数上,而这底下是经过闭包实现的。只是把你之前手写的代码自动化生成而已。
在本文的第二个例子中,咱们留意到 Animal()
构造函数被额外包了一层,当时不得其解。看到这里,咱们也许能够理解它的意图:就是为了将你在类中编写的箭头函数作个闭包,将 this
引用存储下来,以作后用。