理解JavaScript的核心知识点:This
this
是 JavaScript
中非常重要且使用最广的一个关键字,它的值指向了一个对象的引用。这个引用的结果非常容易引起开发者的误判,所以必须对这个关键字刨根问底。
执行上下文: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
的本质:
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
在编写初始借鉴了JAVA
和 C
语言的特性,即便本质上不同,但还是把这个如同惯例般存在的 this
拿了过来。使用 this
的原因其实很简单:
首先,我们时常无法得知调用了函数的对象的名称,并且有时候根本就没有名称可以用来引用调用对象。这是一个迫切的原因,因为我们在开发时必定会遇到需要引用调用函数的对象的场景。
其次,避免重复指代,就像我们经常使用第三人称来指代前文的主体一样,作为程序员大家当然很乐意使用一个快捷方式来避免机械重复一些不必要的代码,这也是“语言”这一重要产品的特性。
最后,它提供给我们实现高级功能的可能性,我们可以通过 this
动态对于执行上下文的指代而实现程序的复用性和扩展。
This的判断规则:Rules of This
对 this
的根源进行深入探究的目的就是为了在开发中对自己所使用的 this
关键字指代的对象进行准确的判定,它就是一个变量,所以当我们使用它的时候,必须清晰地知道它的值到底是什么。
一般来说,我们可以通过确定是哪个对象拥有所调用的函数来确定其 this
的指向。这是由于 this
的绑定值是在函数调用的时候才赋予的,要看函数在哪个上下文对象中调用,但有时候这不是仅用肉眼就能观察出来的。
此外还要严肃声明一下,虽然在之前下定义的时候将 this
的概念明确地划分到了运行阶段,但由于它作为一个变量的特性,是可以改变引用值的,它的值的计算与词法规则还是息息相关,得将编译和运行时两个阶段结合起来,总结出关于判断 this
绑定值的基本原则。
this
关键字绑定的操作是在语言内核机制的运行时里执行的,由于无法去探索其内部,只能通过官方文档中给出的一系列描述程序来得知其如何判断,可以梳理出函数调用的内部过程中对 this
的绑定计算的依据:
前置知识 1: 内部机制创建执行上下文、初始化函数所属领域和创建相关环境记录
在函数被真正执行之前,内部机制会执行创建拥有函数的领域、创建执行上下文、移交当前执行上下文控制权、创建环境记录、环境记录对象参数的绑定等一系列操作,为程序运行做编译准备。在将函数推入执行栈顶层的时候,对其上下文的归属有以下的判断过程,此处与一个新的概念领域有关:
- 如果领域中的属性
this
返回了一个对象,就将内部属性thisValue
设置为以此对象为基础按照规格创建的js
对象,否则thisValue
绑定值为undefined
,表明领域的全局对象(本地全局对象)将设置为全局对象(程序全局对象)。
这里在新规范里出现的一个概念领域取代了之前版本中简单的作用域的概念,由于实现了模块化等其他新特性,所以作用域的概念可以相当于扩展成了现在的领域,它下属了其他几个环境记录,其中变量的绑定分别在不同环境记录中,这里就不做深入解释了。
领域中比较重要的属性是领域中的全局对象,这与程序运行时的全局对象的概念要加以区别,所以可以把领域中的全局对象看作是本地全局变量,其实也就是函数所属的上下文对象,它的值就是在刚才的以上的判断中确定的,如果没有这个前置对象,就会把全局对象设置为本地全局对象的值。
前置知识 2: 内部机制创建函数
内部机制在词法分析阶段会通过函数的定义方式向创建函数操作传入几种不同类型的函数类型:Normal
、Arrow
、Method
,相对应的是普通函数、箭头函数、作为对象方法的函数。同时在这一步还传入指定代码严格模式的参数 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
The ECMAScript 9.0 Standard
The ECMAScript 5.1 Standard
- MDN web docs: this
You Don't Know JS: this & object prototypes
- Understanding the "this" keyword in JavaScript
- The this keyword
- StackOverflow: How does the “this” keyword work?
- Scope In JavaScript
- Understand JavaScript’s “this” With Clarity, and Master It
- JavaScript 的 this 原理