Skip to main content

深入理解:this的指向

· 约16分钟
Proca

导语

深入理解: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关键字的行为,它通常由三部分组成:

  1. base value
  2. referenced name
  3. 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()

我们已经知道, baseReference的组成部分之一,而在规范层面,我们可以通过 GetBase() 方法来获取Referece中的 base

IsPropertyReference()

在代码分析阶段的许多环节中,都会调用这个规范方法。如果Reference中的 base 不为对象(例如,为环境记录项(Environment Record)),则返回 false 。

GetValue()

当我们调用 GetValue() 时,会首先调用类型判断方法。当类型判断结果不为Reference时,则直接返回该“对象”;否则便调用 IsPropertyReference() 方法,根据该方法的返回值,决定返回该Referencebase 内部绑定值的方式(因此,返回的不是 base ,而是从 base 中获取到的值)。

例如,对于以下代码:

let foo = 1;

我们有:

fooReference = {
base: EnvironmentRecord,
name: 'foo',
strict: false
};

GetValue(fooReference); // 1

让我们模拟一下该示例中GetValue()的运作机制:

  1. 可知,fooReferenceReference

  2. 因此,调用 IsPropertyReference()

  3. 可知,该Referencebase 为 EnvironmentRecord 而非对象

  4. 因此,IsPropertyReference() 返回 false

  5. 随后,基于返回值 falseGetValue() 根据该Reference中的 name ,在 base 所指向的 EnvironmentRecord 中获取对应的绑定值

因此,此处 GetValue(fooReference) 获取到的正是 fooEnvironmentRecord 中的绑定值 1 ,亦即返回值不再是一个 Reference

确定this的值!

ES6 规范 中,函数调用一节中,关于确定this值的内容如下:

  1. Let ref be the result of evaluating MemberExpression.

  2. 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().

  3. Else Type(ref) is not Reference,

    -> Let thisValue be undefined.

中文翻译如下:

  1. MemberExpression 的计算结果赋值给 ref

  2. 如果 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 )

  3. 否则, ref 不是一个 Reference,那么

    -> 将 undefined 赋值给 thisValue

以上步骤中的绝大多数方法我们都在 规范方法 中进行了介绍,但在对具体的代码进行分析之前,我们还需要了解步骤 1 中的 MemberExpression 是什么。

MemberExpression

步骤 1 提到,函数调用后,会将 MemberExpression 的计算结果赋值给 ref 。查阅 ES 6规范 ,我们发现, MemberExpression 拥有如下的分类:

表 1

类别示例
PrimaryExpression(稍后介绍)
MemberExpression [ Expression ]foo[bar]
MemberExpression . IdentifierNamefoo.bar
MemberExpression TemplateLiteraltagFunction`string text ${expression}`
super [ Expression ]super[foo]
super . IdentifierNamesuper.foo
new . targetnew.target
new MemberExpression Argumentsnew Object()

对于PrimaryExpression,它的值如下:

表 2

类别含义
thisthis 关键字
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();

让我们一步步地跟随规范中的步骤:

  1. 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
}
  1. 如果 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();

我们还是按照步骤来:

  1. 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
}
  1. 如果 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)();

还是按照步骤来!

  1. 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 。

因此,进入下一步。

  1. 否则, ref 不是一个 Reference,那么

将 undefined 赋值给 thisValue

因此,此处 this 的值为 undefined

例 5

var value = 1;

var foo = {
value: 2,
bar: function () {
return this.value;
}
}

(false || foo.bar)()

继续按照步骤来!

  1. 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

  1. 否则, 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 的指向便自然浮现而出。