深入理解JS:执行上下文中的this(一)
目录
- 执行上下文与执行上下文栈
- this
- 全局环境
- 函数环境
- 总结
- 参考
1.执行上下文与执行上下文栈
(1)什么是执行上下文?
在 JavaScript 代码运行时,解释执行全局代码、调用函数或使用 eval 函数执行一个字符串表达式都会创建并进入一个新的执行环境,而这个执行环境被称之为执行上下文。因此执行上下文有三类:全局执行上下文、函数执行上下文、eval 函数执行上下文。
执行上下文可以理解为一个抽象的对象,如下图:
Variable object:变量对象,用于存储被定义在执行上下文中的变量 (variables) 和函数声明 (function declarations) 。
Scope chain:作用域链,是一个对象列表 (list of objects) ,用以检索上下文代码中出现的标识符 (identifiers) 。
thisValue:this 指针,是一个与执行上下文相关的特殊对象,也被称之为上下文对象。
(2)什么是执行上下文栈?
在全局代码中调用函数,或函数中调用函数(如递归)等,都会涉及到在一个执行上下文中创建另一个新的执行上下文,并且等待这个新的上下文执行完毕,才会返回之前的执行上下文接着继续执行,而这样的调用方式就形成了执行上下文栈。
示例代码:
function A() {
console.log('function A')
B()
}
function B() {
console.log('function B')
C()
}
function C() {
console.log('function C')
}
A()
上述示例代码,当执行到函数 C时,此时的执行上下文栈如下图:
2.this
首先需要清楚,this 是执行上下文的一个属性,而不是某个变量对象的属性,是一个与执行上下文相关的特殊对象。由于在开发中不推荐或应尽量避免使用 eval 函数,所以在这里我们主要讨论全局执行上下文(全局环境)和函数执行上下文(函数环境)中的 this。
(1)全局环境
无论是否在严格模式下,在全局环境中(在任何函数体外部的代码),this 始终指向全局对象(在浏览器中即 window)。
示例代码(浏览器中):
console.log(this === window) // true
a = 1;
console.log(window.a) // 1
console.log(this.a === window.a) // true
this.b = "test"
console.log(window.b) // test
console.log(b) //test
(2)函数环境
在大多数情况下,函数的调用方式决定了 this 的值。 this 是不能够在执行期间被赋值修改的,并且在每次函数被调用时其 this 可能不同(通过 apply 或 call 方法显示设置 this 等)。
另外,ES5 引入了 bind 方法来设置函数的 this 值,而不用考虑函数如何被调用的。ES6 引入了支持 this 词法解析的箭头函数(它在闭合的执行环境内设置 this 的值)。
接下来我们主要分析:函数的调用方式是如何决定 this 的值?(对于 bind 方法以及箭头函数将留于下一篇文章进行详细分析)
要弄明白这个问题,我们来看看 EcmaScript 5.1标准的规定,了解一下 函数调用 的规范:
11.2.3 函数调用
产生式 CallExpression : MemberExpression Arguments 按照下面的过程执行 :
- 令 ref 为解释执行 MemberExpression 的结果 .
- 令 func 为 GetValue(ref).
- 令 argList 为解释执行 Arguments 的结果 , 产生参数值们的内部列表 (see 11.2.4).
- 如果 Type(func) is not Object ,抛出一个 TypeError 异常 .
- 如果 IsCallable(func) is false ,抛出一个 TypeError 异常 .
- 如果 Type(ref) 为 Reference,那么 如果 IsPropertyReference(ref) 为 true,那么 令 thisValue 为 GetBase(ref). 否则 , ref 的基值是一个环境记录项 , 令 thisValue 为 GetBase(ref).ImplicitThisValue().
- 否则 , 假如 Type(ref) 不是 Reference. 令 thisValue 为 undefined.
- 返回调用 func 的 [[Call]] 内置方法的结果 , 传入 thisValue 作为 this 值和列表 argList 作为参数列表
产生式 CallExpression : CallExpression Arguments以完全相同的方式执行,除了第1步执行的是其中的CallExpression。
简单解析:
第1步,令 ref 为 MemberExpression 解释执行的结果。
在 11.2 左值表达式 中有提到,MemberExpression 可以是以下五种表达式中的任意一种:
- PrimaryExpression // 原始表达式
- FunctionExpression // 函数定义表达式
- MemberExpression [ Expression ] // 属性访问表达式
- MemberExpression . IdentifierName // 属性访问表达式
- new MemberExpression Arguments // 对象创建表达式
简单理解 MemberExpression 就是调用一个函数的()
左侧的部分。
第2~5步,获取调用函数的参数列表以及检测所调用的函数是否合法,否则抛出相应异常(GetValue的作用是获取 Reference 类型具体的值)。
第6、7步,就是决定函数调用的 this 的值的关键步骤,翻译一下,如同下面的伪代码:
var thisValue = getThisValue(ref)
function getThisValue(ref) {
// 判断 ref 的类型是否是 Reference,如果不是,直接返回 undefined
if(Type(ref) !== Reference) return undefined
// 是否是 Object, Boolean, String, Number
if(IsPropertyReference(ref)) {
return GetBase(ref)
} else {
// 是一个环境记录项(Environment record),调用其 ImplicitThisValue 方法
return GetBase(ref).ImplicitThisValue()
}
}
关于 GetBase 和 IsPropertyReference 方法:
- GetBase(V), 返回引用值 V 的基值 (Reference 的基值 base,详见下面提到的 Reference 的组成)。
- HasPrimitiveBase(V), 如果基值是 Boolean, String, Number,那么返回 true。
- IsPropertyReference(V), 如果基值是个对象或 HasPrimitiveBase(V) 是 true,那么返回 true;否则返回 false。
而对于 ImplicitThisValue 方法,其属于环境记录项(Environment record)的方法。而环境记录项分为两种:
-
声明式环境记录项:每个声明式环境记录项都与一个包含变量和(或)函数声明的 ECMA 脚本的程序作用域相关联。声明式环境记录项用于绑定作用域内定义的一系列标识符。其 ImplicitThisValue 永远返回 undefined。
-
对象式环境记录项:每一个对象式环境记录项都有一个关联的对象,这个对象被称作 绑定对象 。对象式环境记录项直接将一系列标识符与其绑定对象的属性名称建立一一对应关系。其 ImplicitThisValue 通常返回 undefined,除非其 provideThis 标识的值为 true。具体如下:
- 令 envRec 为函数调用时对应的声明式环境记录项。
- 如果 envRec 的 provideThis 标识的值为 true,返回 envRec 的绑定对象。
- 否则返回 undefined。
对象式环境记录项可以通过配置的方式,将其绑定对象合为函数调用时的隐式 this 对象的值。这一功能用于规范 With 表达式(12.10 章 )引入的绑定行为。该行为通过对象式环境记录项中布尔类型的 provideThis 值控制,默认情况下,provideThis 的值为 false。(只有使用了 with 表达式,才会将 provideThis 标识的值为 true)
而上面提到了两种新的类型: 引用规范类型 (Reference)与 环境记录项(Environment record)都是属于ECMAScript 的规范类型,相当于 meta-values,是用来用算法描述 ECMAScript 语言结构和 ECMAScript 语言类型的。
而与规范类型相对于的就是语言类型:就是开发者直接使用的类型,即Undefined, Null, Boolean, String, Number, 和 Object。(ECMAScript的类型分为语言类型和规范类型)
从上面的伪代码中可以看到 thisValue 的值与 ref 是否是引用规范类型(Reference)有直接关联,即调用一个函数时,其()
左侧的部分的解释执行的结果的类型是不是 Reference 类型,将直接影响 thisValue 的值。
EcmaScript 5.1标准中的 Reference 的规范:
8.7 引用规范类型 (Reference)
Reference 类型是用来说明 delete,typeof,赋值运算符这些运算符的行为。
一个 Reference 是个已解决的命名绑定。其由三部分组成, 基值 (base) , 引用名称(referenced name) 和布尔值 严格引用 (strict reference) 标志。
基值是 undefined, Object, Boolean, String, Number, Environment record 中的任意一个。基值是 undefined 表示此引用可以不解决一个绑定。引用名称是一个字符串。严格引用标志表示是否在严格模式下解释执行的代码。
而引用规范类型(Reference)会被用在标识符解析中,标识符执行的结果总是一个 Reference 类型的值。
EcmaScript 5.1标准中的 标识符解析 的规范:
10.3.1 标识符解析
标识符解析是指使用正在运行的执行环境中的词法环境,通过一个 标识符 获得其对应的绑定的过程。在 ECMA 脚本代码执行过程中,PrimaryExpression : Identifier 这一语法产生式将按以下算法进行解释执行:
- 令 env 为正在运行的执行环境的 词法环境 。
- 如果正在解释执行的语法产生式处在 严格模式下的代码 中,则仅 strict 的值为 true,否则令 strict 的值为 false。
- 以 env,Identifier 和 strict 为参数,调用 GetIdentifierReference 函数,并返回调用的结果。
解释执行一个标识符得到的结果必定是 Reference 类型的对象,且其引用名属性的值与 Identifier 字符串相等。
而 GetIdentifierReference 函数就是返回一个 Reference 类型的对象,类似如下对象:
var valueOfReferenceType = {
base: <base object>, // Identifier 所处的环境(Environment Record)或者 Identifier 属性所属的对象
propertyName: <property name>, // 与 Identifier 字符串相等
strict: <boolean>
};
因此,我们可以来看一些相关的示例代码。
第一组:非严格模式和严格模式的全局函数
function foo() {
console.log(this)
}
function bar() {
'use strict'
console.log(this)
}
foo() // global
bar() // undefined
// foo 标识符对应的 Reference
var fooReference = {
base: EnvironmentRecord,
propertyName: 'foo',
strict: false
}
// bar 标识符对应的 Reference
var barReference = {
base: EnvironmentRecord,
propertyName: 'bar',
strict: true
}
上述代码中,对于 fooReference,根据函数调用规范可知其 this = getThisValue(fooReference) = GetBase(fooReference).ImplicitThisValue() = undefined
,而 barReference 也是一样。
但为什么 foo()
输出的是 global 全局对象而不是 undefined 呢?这是因为在非严格模式下, 当 this 的值为 undefined 时,会被隐式转换为全局对象。而在严格模式下,指定的 this 不再被封装为对象。
第二组:对象的属性访问
var foo = {
bar: function () {
console.log(this)
}
}
foo.bar() // foo
// foo 的 bar 属性对应的 Reference
var barReference = {
base: foo,
propertyName: 'bar',
strict: false
}
上述代码中,对于 barReference,根据函数调用规范可知 this = getThisValue(barReference) = GetBase(barReference) = foo
在 foo.bar()
中,MemberExpression 计算的结果是 foo.bar,为什么它是一个 Reference 类型呢?
EcmaScript 5.1标准中的 属性访问 的规范:
11.2.1 属性访问
- 返回一个 Reference 类型的值,其基值为 baseValue 且其引用名为 propertyNameString, 严格模式标记为 strict.
这里只引用了最后一步,属性访问最终返回的值是一个 Reference 类型。
第三组:非 Reference 类型的函数调用
首先,需要简单了解一下 GetValue 方法,其作用是获取 Reference 类型具体的值,返回结果不再是一个 Reference。例如:
var foo = 1
// foo 标识符对应的 Reference
var fooReference = {
base: EnvironmentRecord,
propertyName: 'foo',
strict: false
}
GetValue(fooReference) // 1
示例代码:
value = 1
var foo = {
value: 2,
bar: function () {
console.log(this.value)
}
};
foo.bar(); // 2
(foo.bar)(); // 2
(false || foo.bar)(); // 1
(foo.bar = foo.bar)(); // 1
(foo.bar, foo.bar)(); // 1
在上述示例代码中:
- 对于
(foo.bar)
,foo.bar 被 () 包住,使用了分组运算符,查看规范 11.1.6 分组操作符,可知分组表达式不会调用 GetValue 方法, 所以(foo.bar)
仍旧是一个 Reference 类型,因此 this 为 Reference 类型的 base 对象,即 foo。 - 对于
(false || foo.bar)
,有逻辑与算法,查看规范 11.11 二元逻辑运算符,可知二元逻辑运算符调用了 GetValue 方法,所以false || foo.bar
不再是一个 Reference 类型,因此 this 为 undefined,非严格模式下,被隐式转化为 global 对象。 - 对于
(foo.bar = foo.bar)
,有赋值运算符,查看规范 11.13.1 简单赋值,可知简单赋值调用了 GetValue 方法,所以foo.bar = foo.bar
不再是一个 Reference 类型,因此 this 为 undefined,非严格模式下,被隐式转化为 global 对象。 - 对于
(foo.bar, foo.bar)
,有逗号运算符,查看规范 11.14 逗号运算符,可知逗号运算符调用了 GetValue 方法,所以foo.bar, foo.bar
不再是一个 Reference 类型,因此 this 为 undefined,非严格模式下,被隐式转化为 global 对象。
3.总结
- 在全局环境(全局执行上下文)中(在任何函数体外部的代码),this 始终指向全局对象
- 在函数环境(函数执行上下文)中,绝大多数情况,函数的调用方式决定了 this 的值,这与调用函数的
()
左侧的部分 MemberExpression 的解释执行的结果的类型是不是 Reference 类型直接关联。
4.参考
this 关键字 - JavaScript | MDN - Mozilla
深入理解JavaScript系列(10):JavaScript核心(晋级高手必读篇)
深入理解JavaScript系列(13):This? Yes,this!