通过阅读规范,我们可以了解 this
真正运行的机制,彻底解决this丢失问题并放心的使用箭头函数替换掉代码中 _this
, that
这些多余难看且不宜维护的变量。
前置知识见上文: ECMAScript 规范浅析与实践 --- 1.闭包的形成及相关概念浅析
this 关键字及其执行逻辑
如果我们查看规范中对于 this 关键字的执行逻辑,发现它只是简单的在由环境记录器 outerEnv
字段构成的链条(即我们熟知的作用域链)中寻找第一个 hasThisBinding()
方法返回 true 的环境记录器,并且取该环境记录器的 getThisBinding()
返回值作为 this 值:
this 表达式首先调用 ResolveThisBinding
方法:
ResolveThisBinding
会调用 GetThisEnvironment
方法获取环境记录器,获取到之后会调用环境记录器中的 getThisBinding
作为 this 的返回值:
GetThisEnvironment
中逻辑如下:
- 以正在指向上下文的词法记录器作为起点,查看记录器的 hasThisBinding 方法是否返回 true
- 如果为 true 则返回当前环境记录器,否则沿着 outerEnv 链条继续询问下一个词法记录器
- 直到联调尾部的全局记录器,它的 hasThisBinding 一定为 true
this 的查找过程就是这么简单,但是每个环境记录器 hasThisBinding
和 getThisBinding
的返回值却比较复杂,这里我们主要关注的还是全局环境记录器和函数式环境记录器。
全局作用域下 this 值的绑定
首先对于全局范围的全局环境记录器, 其 hasThisBinding
一定为true:
并且其 getThisBinding
返回的是全局对象(浏览器中为window):
所以在全局作用域中,我们直接使用 this 关键字,打印的就是全局对象。
函数中 this 的绑定
其次是 函数式记录器,这个比较复杂,函数式记录器是在调用函数时,为函数创建执行上下文的同时作为函数上下文的环境记录器属性创建的,其创建时会根据被调用的函数是否是箭头函数来决定 hasThisBinding
是 true 还是 false:
在创建函数记录器时会记录下该函数对象是否为箭头函数:
如果该函数是箭头函数, 该记录器的 hasThisBinding 会返回 false , 否则返回 true:
由于 函数式记录器 对于 箭头函数的特殊处理,使得 this 关键字在作用域上决定返回值时,遇到箭头函数生成的函数式环境记录器就会直接跳过,去链条的下一个节点询问 hasThisBinding
是否能返回 true ,这就是我们常说的 "箭头函数没有自己的 this"。
而对于非箭头函数,在函数执行时会根据传入的 thisArgument 尝试绑定该函数式环境记录器的 [[ThisValue]]
属性,而函数式环境记录器的 getThisBinding
方法返回的正是该属性。
而传入的 thisArgument 有这几种情况:
- 如果是对象.方法(),传入的就是该对象。
- 如果是直接调用,传入的是 undefined。
- 如果是作为构造器调用,传入的是新创建的对象。
- 如果是 apply 显示绑定 this , 传入的就是绑定的参数。
详情如下:
这个过程只是尝试绑定 [[ThisValue]], 真正绑定的值根据函数对象是否处于严格模式略有不同,如果是严格模式,就直接将传入值作为 [[ThisValue]] , 如果是非严格模式,则将传入值转换为对象,再作为 [[ThisValue]]。
流程梳理
函数的 this 的查找即绑定规则大致如上,我们简单梳理一下整体流程:
首先,在函数被创建时,函数对象有一个 [[ThisMode]]
字段来记录它是否是箭头函数 (取值 'lexical ') , 亦或是处于严格模式的普通函数 (取值 'strict '),以及非严格模式的普通函数 (取值 'global')。
其次,在函数调用时,会根据调用方式,传入不同的 thisArgument
, 并且根据函数对象的 [[ThisMode]]
取值,来决定新建的函数式环境记录器 hasThisBinding
返回值,箭头函数返回 false , 其余返回 true , 同时会根据是否处于严格模式,将 thisArgument
转换并绑定到函数式环境记录器的 [[ThisValue]]
字段上。
最后,函数体执行到 this 关键字时,会将正在执行上下文的词法环境记录器作为起点 ,找到第一个 hasThisBinding
返回 true 的记录器节点 ,调用其 getThisBinding
方法, (函数式环境记录器返回其 [[ThisValue]]
值,全局环境记录器返回全局对象)。
代码实践
让我们使用一段简单代码来判断 this 绑定:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
const vm = {}
const options = {
data() {
return {
info: 'Good Joe'
}
},
mounted() {
this.onSomethingHandler()
},
methods: {
onSomethingHandler() {
const func = () => {
console.log(this.info)
}
setTimeout(() => {
func()
} , 0)
window.addEventListener('resize' , func)
func.call(window)
}
}
}
vm._data = options.data.call(vm)
for(let k in vm._data) {
Object.defineProperty(vm , k , {
get() {
return this._data[k]
},
set(val) {
this._data[k] = val
}
})
}
for(let k in options.methods) {
vm[k] = options.methods[k]
// vm[k] = options.methods[k].bind(vm) // vue source
}
options.mounted.call(vm)
</script>
</body>
</html>
这段代码模拟了在 vue 的 mounted
声明周期钩子中调用 methods
中某个函数 onSomethingHandler
, 并且函数中使用了箭头函数 func
作为定时器以及全局监听事件的回调,那么在这个箭头函数中, this 究竟是指的是谁呢?
我们按照之前梳理的逻辑分析一下:
首先,onSomethingHandler
被调用,其函数对象 [[ThisMode]]
取值为 global
, 创建的函数式环境记录器(下称 A)hasThisBinding
为 true , 绑定的 [[ThisValue]]
由于是在 mounted
中 this.onSomethingHandler()
调用的, 为 mounted
执行时的 this
, 又因为 mounted
是被 options.mounted.call(vm)
调用的,且处于非严格模式,所以为 vm
。
其次,箭头函数创建,其函数对象 [[ThisMode]]
取值为 lexical , [[Environment]]
指向 A;在箭头函数被执行时,创建的函数式环境记录器(下称 B) outerEnv
也就指向了 A , 并且其 hasThisBinding
为 false 。
最后在 console.log(this.info)
确定 this 的值时,首先以 B 作为起点,发现 hasThisBinding
为 false , 继续向上索引,找到 A ,发现 hasThisBinding
为 true , 然后调用 A.getThisBinding()
返回 A.[[ThisValue]]
即 vm 。 所以无论箭头函数被直接调用还是当成回调,亦或是使用 bind , call , 其 this 都不会丢失
,因为 B 的hasThisBinding
为 false , 根本不会从它这里取得 this 值。