导语
在 深入理解:ECMAScript 3中的执行上下文 中,我们曾对 this
的指向进行了简单的介绍:
如果当前函数作为对象方法调用,或使用 bind call apply 等方法调用,则引擎会将对应的调用者信息( this )存入当前执行上下文中。否则,调用者信息将默认地被设置为全局对象( globalThis )。
因此,实践的大多数情况下,我们可以将 this 简单地理解为调用者。然而,观察以下示例:
var value = 1; // var声明的变量将被挂载在全局对象上
let foo = {
value: 2,
bar: function () {
return this.value;
}
}
console.log((foo.bar, foo.bar)()); // 1
按照我们先前的逻辑,我们仍然是在 foo
对象中调用 bar
。于是, this.value
应该与 foo.value
相等,输出的应该是 2 而非 1 呀!
为了弄清楚这个问题,我们有必要深入理解 ECMAScript 规范中有关 this
的内容。
规范类型:Reference
在深入理解:ECMAScript 6中的执行上下文中,我们提到了只存在于语言规范层面的“规范方法”,实际上,语言规范层面不仅存在“规范方法”,还存在“规范类型”。像我们先前的博文中提到的 Lexical Environment 以及 Environment Record ,都属于规范类型(尽管我们当时称之为“规范对象”)。
同样地,Reference也是规范类型中的一员。在代码分析阶段,它的存在使得 this
能够获取到相对应的值。
因此,现在让我们先从规范类型Reference谈起。
何为Reference?
Reference被用于解释诸如 delete、typeof、赋值运算符以及super关键字的行为,它通常由三部分组成:
- base value
- referenced name
- strict reference
让我们从官方的规范中简单地了解一下以上三个组成部分:
base:
The base value is either undefined, an Object, a Boolean, a String, a Symbol, a Number, or an Environment Record
因此,简单来说,base value就是属性所在的对象或Environment Record。
referenced name:
The referenced name is a String or Symbol value.
因此,简单来说,referenced name就是属性的名称或属性对应的Symbol。
例如对于下面的代码:
let foo = 1;
我们有:
fooReference = {
base: EnvironmentRecord,
name: 'foo',
strict: false
};
而对于下面的代码:
let foo = {
bar: function(){
return this;
}
};
我们则有:
barReference = {
base: foo,
name: 'bar',
strict: false
};
规范方法
同样在深入理解:ECMAScript 6中的执行上下文中,我们曾提到:在非箭头函数环境中, this
的值是由规范方法完成绑定的。因此,下面让我们来了解与 this
绑定相关的规范方法。
GetBase()
我们已经知道, base
是Reference的组成部分之一,而在规范层面,我们可以通过 GetBase()
方法来获取Referece中的 base
。
IsPropertyReference()
在代码分析阶段的许多环节中,都会调用这个规范方法。如果Reference中的 base
不为对象(例如,为环境记录项(Environment Record)),则返回 false 。
GetValue()
当我们调用 GetValue()
时,会首先调用类型判断方法。当类型判断结果不为Reference时,则直接返回该“对象”;否则便调用 IsPropertyReference()
方法,根据该方法的返回值,决定返回该Reference中 base
内部绑定值的方式(因此,返回的不是 base ,而是从 base 中获取到的值)。
例如,对于以下代码:
let foo = 1;
我们有:
fooReference = {
base: EnvironmentRecord,
name: 'foo',
strict: false
};
GetValue(fooReference); // 1
让我们模拟一下该示例中GetValue()的运作机制:
可知,
fooReference
为Reference因此,调用
IsPropertyReference()
可知,该Reference的
base
为 EnvironmentRecord 而非对象因此,
IsPropertyReference()
返回false
随后,基于返回值
false
,GetValue()
根据该Reference中的name
,在base
所指向的 EnvironmentRecord 中获取对应的绑定值
因此,此处 GetValue(fooReference)
获取到的正是 foo
在 EnvironmentRecord
中的绑定值 1
,亦即返回值不再是一个 Reference 。
确定this的值!
ES6 规范 中,函数调用一节中,关于确定this值的内容如下:
Let ref be the result of evaluating MemberExpression.
If Type(ref) is Reference, then
a. If IsPropertyReference(ref) is true, then
-> i. Let thisValue be GetThisValue(ref).
b. Else, the base of ref is an Environment Record
-> i. Let refEnv be GetBase(ref).
-> ii. Let thisValue be refEnv.WithBaseObject().
Else Type(ref) is not Reference,
-> Let thisValue be undefined.
中文翻译如下:
将 MemberExpression 的计算结果赋值给 ref
如果 ref 是一个 Reference ,那么
a. 如果 IsPropertyReference(ref) 返回 true,那么
-> i. 将GetThisValue(ref) 的返回值赋值给 thisValue
(在该情况下,GetThisValue(ref) 与 GetBase(ref) 等价)
b. 否则,ref的 base 一定是环境记录项(Environment Record)
-> i. 将 GetBase(ref) 的返回值赋值给 refEnv
-> ii. 将 refEnv.WithBaseObject() 的返回值赋值给 thisValue
(除非使用了 with 语句,否则 WithBaseObject() 始终返回 undefined )
否则, ref 不是一个 Reference,那么
-> 将 undefined 赋值给 thisValue
以上步骤中的绝大多数方法我们都在 规范方法 中进行了介绍,但在对具体的代码进行分析之前,我们还需要了解步骤 1 中的 MemberExpression 是什么。
MemberExpression
步骤 1 提到,函数调用后,会将 MemberExpression 的计算结果赋值给 ref 。查阅 ES 6规范 ,我们发现, MemberExpression 拥有如下的分类:
表 1
类别 | 示例 |
---|---|
PrimaryExpression | (稍后介绍) |
MemberExpression [ Expression ] | foo[bar] |
MemberExpression . IdentifierName | foo.bar |
MemberExpression TemplateLiteral | tagFunction`string text ${expression}` |
super [ Expression ] | super[foo] |
super . IdentifierName | super.foo |
new . target | new.target |
new MemberExpression Arguments | new Object() |
对于PrimaryExpression,它的值如下:
表 2
类别 | 含义 |
---|---|
this | this 关键字 |
IdentifierReference | 可简单理解为词法环境中的变量绑定名 |
Literal | 数字字面量、字符串字面量等 |
ArrayLiteral | 诸如 [1, 2, 3] 的字面量 |
ObjectLiteral | 诸如 { name: 'Proca' } 的字面量 |
FunctionExpression | 诸如 function print(name){console.log(name);} 的表达式 |
ClassExpression | 诸如 class Student { getname() { ... } } 的表达式 |
GeneratorExpression | 诸如 function generator ( start = 0, end = Infinity, step = 1 ) { GeneratorBody* } 的表达式 |
RegularExpressionLiteral | 正则表达式字面量 |
TemplateLiteral | 诸如 `string text ${expression}` 的模版字面量 |
CoverParenthesizedExpressionAndArrowParameterList | 可简单理解为被一对括号括起来的表达式 |
需要注意的是,表 1 中对 MemberExpression 使用了递归定义,例如, foo[bar]
属于 MemberExpression [ Expression ] ,但同时也属于 MemberExpression,因此,foo[bar][name]
依然属于 MemberExpression [ Expression ] 。
现在,让我们通过实例来巩固以上内容。
function foo() {
return this;
}
foo(); // 此处,foo 为 MemberExpression
function foo() {
return function() {
return this;
}
}
foo()(); // 此处 foo() 为 MemberExpression
let foo = {
bar: function () {
return this;
}
}
foo.bar(); // 此处,foo.bar 为 MemberExpression
让我们正式开始吧!
在上面,我们已经介绍了深入了解this所需的预备知识,也了解了确认this指向的一般步骤。接下来,为了更好地将这些知识运用于实践中,我们将对具体代码进行分析。
例 1
先从最简单最基本的情况开始:
function foo() {
return this;
}
foo();
让我们一步步地跟随规范中的步骤:
- 将 MemberExpression 的计算结果赋值给 ref
正如我们上面提到的,这里的 MemberExpression 正是 foo
,它属于PrimaryExpression 。而对于如何计算该类 MemberExpression ,规范中有相应的规定:
Return a value of type Reference whose base value is envRec, whose referenced name is name, and whose strict reference flag is strict.
因此,此时的 ref 内容如下:
ref = {
base: EnvironmentRecord,
name: 'foo',
strict: false
}
- 如果 ref 是一个 Reference ,那么
由以上规范可知,该 ref 属于 Reference ,因此进入下一步。
a. 如果 IsPropertyReference(ref) 返回 true,那么
我们知道,IsPropertyReference(ref) 只有在 ref 的 base 不为 EnvironmentRecord 时才返回true,而此时的 base 恰好就是 EnvironmentRecord,因此,IsPropertyReference(ref) 返回 false。
b. 否则,ref的 base 一定是环境记录项(Environment Record)
太酷了!ref 的 base 确确实实为环境记录项。
继续进入下一步。
i. 将 GetBase(ref) 的返回值赋值给 refEnv
ii. 将 refEnv.WithBaseObject() 的返回值赋值给 thisValue
因此,this的值就是 refEnv.WithBaseObject() 的返回值。而这个简单的示例显然没有使用 with 语句,因此,返回值就是 undefined 。
亦即 this 的值为 undefined
。
例 2
var value = 1;
var foo = {
value: 2,
bar: function () {
return this.value;
}
}
foo.bar();
我们还是按照步骤来:
- 将 MemberExpression 的计算结果赋值给 ref
在这里,MemberExpression 是 foo.bar
,对于这类 MemberExpression ,计算其结果的规定如下:
Return a value of type Reference whose base value is bv and whose referenced name is propertyKey, and whose strict reference flag is strict.
因此,此时的 ref 内容如下:
ref = {
base: foo,
name: 'bar',
strict: false
}
- 如果 ref 是一个 Reference ,那么
由规范,我们也可以知道该 ref 也是一个 Reference,因此进入下一步。
a. 如果 IsPropertyReference(ref) 返回 true,那么
这次 ref 的 base 不为 EnvironmentRecord,因此,返回值为 true,继续进入下一步。
i. 将GetThisValue(ref) 的返回值赋值给 thisValue
根据前面的介绍,此时 GetThisValue(ref) 与 GetBase(ref) 等价,而该 ref 的 base 正为对象 foo
。
于是,this 的值就是对象 foo
。
例 3
var value = 1;
var foo = {
value: 2,
bar: function () {
return this.value;
}
}
(foo.bar)();
例 3 与前面例 2 的区别在于,最后的 foo.bar
被括号括了起来,这会产生什么不同的效果呢?查阅规范后发现,对于括号括起来的 MemberExpression:
Return the result of evaluating Expression. This may be of type Reference.
This algorithm does not apply GetValue to the result of evaluating Expression.
因此,括号并不会对 MemberExpress 返回的 Reference 进行计算,因此,例 3 的效果实际上与 例 2 相同。
亦即 this 的值都为对象 foo
。
例 4
var value = 1;
var foo = {
value: 2,
bar: function () {
return this.value;
}
}
(foo.bar = foo.bar)();
还是按照步骤来!
- 将 MemberExpression 的计算结果赋值给 ref
这里 MemberExpression 是 (foo.bar = foo.bar),由例 3 我们可知,这样的括号对于分析 this 的值没有影响。那么根据规范,foo.bar = foo.bar 的计算结果如何得到呢?
If LeftHandSideExpression is neither an ObjectLiteral nor an ArrayLiteral
Let rval be GetValue(rref)
Return rval
因此,最后的计算结果其实是 GetValue(rref) 的返回值。由 规范方法 中的介绍,可知 GetValue(rref) 的返回值一定不为 Reference 。
因此,进入下一步。
- 否则, ref 不是一个 Reference,那么
将 undefined 赋值给 thisValue
因此,此处 this 的值为 undefined
。
例 5
var value = 1;
var foo = {
value: 2,
bar: function () {
return this.value;
}
}
(false || foo.bar)()
继续按照步骤来!
- 将 MemberExpression 的计算结果赋值给 ref
这里 MemberExpression 是 (false || foo.bar),同理,我们只对 false || foo.bar 进行计算。
对于逻辑运算符( || ),当左值为false时,由规范:
Let rref be the result of evaluating BitwiseORExpression(注:该例中的 BitwiseORExpression 指 foo.bar )
Return GetValue(rref).
因此,最后的计算结果也是 GetValue(rref) 的返回值,因此返回值一定不为 Reference
- 否则, ref 不是一个 Reference,那么
将 undefined 赋值给 thisValue
则同理,this 为 undefined
。
例 6
现在,让我们回到最开始提出的问题上:
var value = 1; // var声明的变量将被挂载在全局对象上
let foo = {
value: 2,
bar: function () {
return this.value;
}
}
console.log((foo.bar, foo.bar)()); // 1
具体的步骤不再赘述。对于逗号运算符,根据规范,在没有中断的情况下:
Let rref be the result of evaluating AssignmentExpression
Return GetValue(rref)
因此,最后 ref 的值依然不是 Reference,于是 this 的值便为 undefined。
需要注明的是:对于以上所有样例,在非严格模式下且 this 的值为 undefined 时,this 会最终指向全局对象。
总结:
现在,你已经掌握了确定 this 指向的必备规范类型、规范方法,还了解了确定 this 指向的通用步骤。在实践中,只要确认好计算 MemberExpression 所得的结果, this 的指向便自然浮现而出。