ECMAScript 规范浅析与实践 — 2.this 关键字及其执行逻辑

通过阅读规范,我们可以了解 this 真正运行的机制,彻底解决this丢失问题并放心的使用箭头函数替换掉代码中 _this , that 这些多余难看且不宜维护的变量。

前置知识见上文: ECMAScript 规范浅析与实践 --- 1.闭包的形成及相关概念浅析

this 关键字及其执行逻辑

如果我们查看规范中对于 this 关键字的执行逻辑,发现它只是简单的在由环境记录器 outerEnv 字段构成的链条(即我们熟知的作用域链)中寻找第一个 hasThisBinding() 方法返回 true 的环境记录器,并且取该环境记录器的 getThisBinding() 返回值作为 this 值:

this 表达式首先调用 ResolveThisBinding 方法:

ResolveThisBinding 会调用 GetThisEnvironment 方法获取环境记录器,获取到之后会调用环境记录器中的 getThisBinding 作为 this 的返回值:

GetThisEnvironment 中逻辑如下:

  1. 以正在指向上下文的词法记录器作为起点,查看记录器的 hasThisBinding 方法是否返回 true
  2. 如果为 true 则返回当前环境记录器,否则沿着 outerEnv 链条继续询问下一个词法记录器
  3. 直到联调尾部的全局记录器,它的 hasThisBinding 一定为 true

this 的查找过程就是这么简单,但是每个环境记录器 hasThisBindinggetThisBinding 的返回值却比较复杂,这里我们主要关注的还是全局环境记录器和函数式环境记录器

全局作用域下 this 值的绑定

首先对于全局范围的全局环境记录器, 其 hasThisBinding 一定为true:

并且其 getThisBinding 返回的是全局对象(浏览器中为window):

所以在全局作用域中,我们直接使用 this 关键字,打印的就是全局对象

函数中 this 的绑定

其次是 函数式记录器,这个比较复杂,函数式记录器是在调用函数时,为函数创建执行上下文的同时作为函数上下文的环境记录器属性创建的,其创建时会根据被调用的函数是否是箭头函数来决定 hasThisBinding 是 true 还是 false:

在创建函数记录器时会记录下该函数对象是否为箭头函数

如果该函数是箭头函数, 该记录器的 hasThisBinding 会返回 false , 否则返回 true:

由于 函数式记录器 对于 箭头函数的特殊处理,使得 this 关键字在作用域上决定返回值时,遇到箭头函数生成的函数式环境记录器就会直接跳过,去链条的下一个节点询问 hasThisBinding 是否能返回 true ,这就是我们常说的 "箭头函数没有自己的 this"。

而对于非箭头函数,在函数执行时会根据传入的 thisArgument 尝试绑定该函数式环境记录器的 [[ThisValue]] 属性,而函数式环境记录器的 getThisBinding 方法返回的正是该属性。

而传入的 thisArgument 有这几种情况:

  1. 如果是对象.方法(),传入的就是该对象
  2. 如果是直接调用,传入的是 undefined
  3. 如果是作为构造器调用,传入的是新创建的对象
  4. 如果是 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]] 由于是在 mountedthis.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 值。

参考资料

ECMAScript 规范

everyone-can-read-spec

相关推荐
噢,我明白了1 小时前
同源策略:为什么XMLHttpRequest不能跨域请求资源?
javascript·跨域
sanguine__1 小时前
APIs-day2
javascript·css·css3
关你西红柿子2 小时前
小程序app封装公用顶部筛选区uv-drop-down
前端·javascript·vue.js·小程序·uv
济南小草根2 小时前
把一个Vue项目的页面打包后再另一个项目中使用
前端·javascript·vue.js
小木_.2 小时前
【python 逆向分析某有道翻译】分析有道翻译公开的密文内容,webpack类型,全程扣代码,最后实现接口调用翻译,仅供学习参考
javascript·python·学习·webpack·分享·逆向分析
Aphasia3113 小时前
一次搞懂 JS 对象转换,从此告别类型错误!
javascript·面试
m0_748256563 小时前
Vue - axios的使用
前端·javascript·vue.js
m0_748256343 小时前
QWebChannel实现与JS的交互
java·javascript·交互
胡西风_foxww3 小时前
【es6复习笔记】函数参数的默认值(6)
javascript·笔记·es6·参数·函数·默认值
胡西风_foxww3 小时前
【es6复习笔记】生成器(11)
javascript·笔记·es6·实例·生成器·函数·gen