理解JavaScript的核心知识点:This

thisJavaScript 中非常重要且使用最广的一个关键字,它的值指向了一个对象的引用。这个引用的结果非常容易引起开发者的误判,所以必须对这个关键字刨根问底。

执行上下文:Execution Context

在深入了解 this 对象之前先介绍另一个概念:执行上下文。

没错,执行上下文与 this 在本质上是两个概念,或者说它们指代的范畴有差异,想要准确认识 this,就得先把它们区分开。

可以把执行上下文想象为一个容器,其中包含了一句句待执行的代码。代码在这个容器中有上下行两条路线,是由某一些特殊代码所触发(如函数),上行路线跳入了一个新的容器,开始在新容器中执行另一些代码,本容器中的后续代码被暂时中断;如果新容器中还有代码会触发上行路线,就继续往上增加新容器,并交出控制权,层层叠加,形成了一个从底往上形式的叠罗汉,这就是 JavaScript 运行时的执行上下文栈。

执行上下文这一抽象概念本身包含了更多有关 JavaScript 这门语言的内部机制,对于语言使用者来说是不透明的,其中与运行前的编译规则有很大关联,并被包含到整个程序运行前的初始化过程中,与词法作用域的变量解析规则相配合,将这些静态解析后的变量带入运行时的环境,所以它是程序运行时的关键内部组件或者说容器,而 JavaScript 将对执行上下文的引用提供给程序开发者的唯一入口就是 this,它得以访问被编译后带入到某个执行上下文运行环境中的变量。this 指代的其实只是内部抽象的执行上下文向用户所开放的那一部分,其实体是一个对象,绑定了许多编译后的变量。

以下是一段关于执行上下文精辟的总结:

An execution context is purely a specification mechanism and need not correspond to any particular artefact of an ECMAScript implementation. It is impossible for ECMAScript code to directly access or observe an execution context.

翻译:执行上下文纯粹是一种规范机制,它不需要与基于 ECMAScript 规范的任何特定扩展实现对应。ECMAScript 代码无法直接访问或观察执行上下文。

关于This对象:What’s This

我将官方文档和一些别的文章里的说明稍加梳理,可以从以下段落中较为清晰地看出 this 的本质:

First, know that all functions in JavaScript have properties, just as objects have properties. And when a function executes, it gets the this property—a variable with the value of the object that invokes the function where this is used.

The this keyword evaluates to the value of the ThisBinding of the current execution context.

The abstract operation GetThisEnvironment finds the Environment Record that currently supplies the binding of the keyword this

this is not assigned a value until an object invokes the function where this is defined.

翻译:

  • 首先,要知道 JavaScript 中所有的函数与对象一样都拥有属性。当一个函数执行时,它得到 this 属性——一个指向调用函数的对象的变量。
  • this 关键字计算为当前执行上下文的 ThisBinding 属性的值。
  • GetThisEnvironment 抽象运算查找当前提供 this 关键字的绑定的环境记录。
  • 在对象调用了定义了 this 的函数之前,this 不会被赋值。

由此可得出关于 this 的完全定义:this 是在程序运行时,通过语言内部抽象操作在执行上下文中动态计算得到的,指向调用使用了其的函数的对象的变量。

执行上下文 vs. This关键字:Execution Context vs. This Keyword

执行上下文和 this 关键字的关系与潜意识相对于意识的关系类似,执行上下文是冰山下深邃庞大而不可窥探的秘地,而 this 只将其一个小部分显露出来。由于 JavaScript 是面向对象的编程语言,所以执行上下文其实质相当于一个对象,this 指向了它向开发者开放了的一系列属性集合的对象,因而我把 this 叫做执行上下文的引用对象。

This因何而来:Why This

JavaScript 在编写初始借鉴了JAVAC 语言的特性,即便本质上不同,但还是把这个如同惯例般存在的 this 拿了过来。使用 this 的原因其实很简单:

首先,我们时常无法得知调用了函数的对象的名称,并且有时候根本就没有名称可以用来引用调用对象。这是一个迫切的原因,因为我们在开发时必定会遇到需要引用调用函数的对象的场景。

其次,避免重复指代,就像我们经常使用第三人称来指代前文的主体一样,作为程序员大家当然很乐意使用一个快捷方式来避免机械重复一些不必要的代码,这也是“语言”这一重要产品的特性。

最后,它提供给我们实现高级功能的可能性,我们可以通过 this 动态对于执行上下文的指代而实现程序的复用性和扩展。

This的判断规则:Rules of This

this 的根源进行深入探究的目的就是为了在开发中对自己所使用的 this 关键字指代的对象进行准确的判定,它就是一个变量,所以当我们使用它的时候,必须清晰地知道它的值到底是什么。

一般来说,我们可以通过确定是哪个对象拥有所调用的函数来确定其 this 的指向。这是由于 this 的绑定值是在函数调用的时候才赋予的,要看函数在哪个上下文对象中调用,但有时候这不是仅用肉眼就能观察出来的。

此外还要严肃声明一下,虽然在之前下定义的时候将 this 的概念明确地划分到了运行阶段,但由于它作为一个变量的特性,是可以改变引用值的,它的值的计算与词法规则还是息息相关,得将编译和运行时两个阶段结合起来,总结出关于判断 this 绑定值的基本原则。

this 关键字绑定的操作是在语言内核机制的运行时里执行的,由于无法去探索其内部,只能通过官方文档中给出的一系列描述程序来得知其如何判断,可以梳理出函数调用的内部过程中对 this 的绑定计算的依据:

前置知识 1: 内部机制创建执行上下文、初始化函数所属领域和创建相关环境记录

在函数被真正执行之前,内部机制会执行创建拥有函数的领域、创建执行上下文、移交当前执行上下文控制权、创建环境记录、环境记录对象参数的绑定等一系列操作,为程序运行做编译准备。在将函数推入执行栈顶层的时候,对其上下文的归属有以下的判断过程,此处与一个新的概念领域有关:

  • 如果领域中的属性 this 返回了一个对象,就将内部属性 thisValue 设置为以此对象为基础按照规格创建的 js 对象,否则 thisValue 绑定值为 undefined,表明领域的全局对象(本地全局对象)将设置为全局对象(程序全局对象)。

这里在新规范里出现的一个概念领域取代了之前版本中简单的作用域的概念,由于实现了模块化等其他新特性,所以作用域的概念可以相当于扩展成了现在的领域,它下属了其他几个环境记录,其中变量的绑定分别在不同环境记录中,这里就不做深入解释了。

领域中比较重要的属性是领域中的全局对象,这与程序运行时的全局对象的概念要加以区别,所以可以把领域中的全局对象看作是本地全局变量,其实也就是函数所属的上下文对象,它的值就是在刚才的以上的判断中确定的,如果没有这个前置对象,就会把全局对象设置为本地全局对象的值。

前置知识 2: 内部机制创建函数

内部机制在词法分析阶段会通过函数的定义方式向创建函数操作传入几种不同类型的函数类型:NormalArrowMethod,相对应的是普通函数、箭头函数、作为对象方法的函数。同时在这一步还传入指定代码严格模式的参数 strict。然后进行函数的初始化的。

方式 1: 内部机制初始化普通函数

内部机制在这一步会设置函数的一个重要属性 ThisMode 的值,它是决定 this 绑定值的依据,它的值是根据上一步传入的参数来判断的,依次执行一下三条判断分支:

  • 函数类型为 Arrow:将 ThisMode 赋值为 lexical ,这个值在计算 this 绑定时将按照词法作用域的规则来赋值,也就是说 this 的值与定义函数的词法作用域中的 this 相一致。
  • 代码模式为 strict :将 ThisMode 赋值 strict,按照这个值计算 this 绑定时只会将显式传入的上下文对象绑定给 this
  • 非以上两种条件:将 ThisMode 赋值 global,被设置为 global 之后,函数在运行阶段被调用时,this 的值就会指向全局对象。

方式 2: 内部机制创建对象方法函数

作为对象属性的方法是另外来计算 this 的,只有在作为对象方法被调用的函数,在内部创建函数时才会传入 Method 值。毫无疑问它将 this 指向了这个前置的对象。构造函数也是同理。

总结一下对一般使用到的函数的判断规则如下:

  • 箭头函数:无论调用位置,取它词法定义处的外层上下文中绑定的 this,没有中间本地对象存在时总是能够取到全局对象。
  • 严格模式:无论调用位置,只取显式给定的上下文绑定的 this,通过 call()apply()bind() 方法传入的第一参数,否则是 undefined
  • new 关键字调用的构造器函数:无论调用位置,this 必为在内部创建的新的实例对象
  • 显式绑定上下文对象的普通函数:无论调用位置,this 必为传入的上下文对象
  • 方法函数:属于隐式绑定,无论词法定义位置,实际情况视调用处而定:
    • 直接调用时:this 为前置上下文对象
    • 作为被引用值时:this 为调用时的上下文对象,在其他对象中引用 this 就是这个调用它的对象;被全局变量引用,this 就是全局对象。
  • 普通函数:无论词法定义位置,视调用处而定,其实质在内存都都是被作为引用值调用的,所以 this 都指向全局对象,严格模式规则优先。

另外关于事件造成的一些 this 误解可以参考The this keyword这篇文章。其实并不属于特殊规则,是由于各种事件监听定义方式本身造成的。

在实际开发中可以参考《You Don’t Know JS》里关于 this 的绑定规则和优先级的章节Nothing But Rules。在这套基础通用规则之外,箭头函数利用了另一套方式来判断 this 的绑定值,这篇文章里也有详尽的叙述。

参考文献:Reference