背景
在 JavaScript 中,当执行代码块时,JavaScript 引擎会为每个代码块创建一个 执行上下文(Execution Context)。在每个执行上下文中,存在几个关键的属性,它们是执行 JavaScript 代码时的基础:
- 变量对象(VO): 即变量对象,执行阶段会变成活动对象(AO)。这个概念值得深入学习,因为它涉及变量的生命周期和作用域的变化,尤其是在函数调用和作用域链中。
- 作用域链(Scope Chain): 作用域链是为了变量查找提供机制。它保证了当前代码能够正确引用作用域内的变量。
- this: this 是 JavaScript 中最具争议的概念之一,它的值在不同的上下文中可能会有所不同,理解它的动态绑定机制对深入理解 JavaScript 是至关重要的。
对这三个概念的深入理解,可以帮助我们更好地调试 JavaScript 代码,并且优化性能。 然后官方ECMAScript 5.1规范地址:yanhaijing.com/es5/#115
解读
第8章我们摘取一段翻译:
ECMAScript 的类型分为:语言类型、规范类型。
-
语言类型:这部分是我们在 JavaScript 开发中直接接触到的类型,如 undefined, null, boolean, string, number, 和 object。
-
规范类型:规范类型不直接暴露给开发者,而是描述 JavaScript 引擎的底层行为逻辑,属于元数据(meta-values)。这些类型包括 Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, 和 Environment Record。
Reference
什么是Reference ?
8.7 章 The Reference Specification Type:
The Reference type is used to explain the behaviour of such operators as delete, typeof, and the assignment operators.
所以 Reference 类型就是用来解释诸如 delete、typeof 以及赋值等操作行为的。
抄袭尤雨溪大佬的话,就是:
"这里的 Reference 是一个 Specification Type,也就是 "只存在于规范里的抽象类型"。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中。"
我们要重点理解的就是 Reference 类型。它与 JavaScript 中 this 的绑定有着密切关系。Reference 类型被广泛应用于诸如 delete, typeof, 以及赋值操作符(=)等操作符的底层实现。 在 ECMAScript 中,Reference 类型 是为了描述变量、属性引用等操作的内部机制,它并不存在于实际的 JavaScript 代码中。简单来说,Reference 是一个 "抽象" 类型,它只在 JavaScript 引擎内部和 ECMAScript 规范中存在,用来更精确地描述一些语法操作。
Reference 的构成,由三个组成部分,分别是:
- base value:表示属性所在的对象,或者环境记录(Environment Record)。它的值可以是 undefined, object, boolean, string, number 或 environment record 其中的一种。
- referenced name:表示属性的名称,即变量或属性的名称。
- strict reference:这是一个标志,用来区分严格模式和非严格模式。
base value 就是属性所在的对象或者就是 EnvironmentRecord,它的值只可能是 undefined, an Object, a Boolean, a String, a Number, or an environment record 其中的一种。
referenced name 就是属性的名称。
举个例子:
js
var foo = 1;
// 对应的Reference是:
var fooReference = {
base: EnvironmentRecord,
name: 'foo',
strict: false
};
js
var foo = {
bar: function () {
return this;
}
};
foo.bar(); // foo
// bar对应的Reference是:
var BarReference = {
base: foo,
propertyName: 'bar',
strict: false
};
而且规范中还提供了获取 Reference 组成部分的方法,比如 GetBase 和 IsPropertyReference。
这两个方法很简单,简单看一看:
1.GetBase
GetBase(V). Returns the base value component of the reference V.
返回 reference 的 base value。
2.IsPropertyReference
IsPropertyReference(V). Returns true if either the base value is an object or HasPrimitiveBase(V) is true; otherwise returns false.
简单的理解:如果 base value 是一个对象,就返回true。
GetValue.
在 8.7.1 章规范中就讲了一个用于从 Reference 类型获取对应值的方法: GetValue。
简单模拟 GetValue 的使用:
js
var foo = 1;
var fooReference = {
base: EnvironmentRecord,
name: 'foo',
strict: false
};
GetValue(fooReference) // 1;
GetValue 返回对象属性真正的值,但是要注意:
调用 GetValue,返回的将是具体的值,而不再是一个 Reference
如何确定this的值
JavaScript 中 this 的值是动态的,根据调用的上下文而不同。ECMAScript 规范 11.2.3 中详细描述了如何在函数调用时确定 this 的值。大致步骤如下:
规范 11.2.3 Function Calls:
这里讲了当函数调用的时候,如何确定 this 的取值。
1.Let ref be the result of evaluating MemberExpression.
6.If Type(ref) is Reference, then
a.If IsPropertyReference(ref) is true, theni.Let thisValue be GetBase(ref).
b.Else, the base of ref is an Environment Recordi.Let thisValue be the result of calling the ImplicitThisValue concrete method of GetBase(ref).
7.Else, Type(ref) is not Reference.
a. Let thisValue be undefined.
1.计算 MemberExpression 的结果赋值给 ref
2.判断 ref 是不是一个 Reference 类型
2.1 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)
2.2 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)
2.3 如果 ref 不是 Reference,那么 this 的值为 undefined
具体分析
1、计算 MemberExpression 的结果赋值给 ref
什么是 MemberExpression?看规范 11.2 Left-Hand-Side Expressions:
MemberExpression :
- PrimaryExpression // 原始表达式 可以参见《JavaScript权威指南第四章》
- FunctionExpression // 函数定义表达式
- MemberExpression [ Expression ] // 属性访问表达式
- MemberExpression . IdentifierName // 属性访问表达式
- new MemberExpression Arguments // 对象创建表达式
举个例子:
js
function foo() {
console.log(this)
}
foo(); // MemberExpression 是 foo
function foo() {
return function() {
console.log(this)
}
}
foo()(); // MemberExpression 是 foo()
var foo = {
bar: function () {
return this;
}
}
foo.bar(); // MemberExpression 是 foo.bar
简单理解 MemberExpression 其实就是()左边的部分。
2、判断 ref 是不是一个 Reference 类型。
关键就在于看规范是如何处理各种 MemberExpression,返回的结果是不是一个Reference类型。
举最后一个例子:
js
var value = 1;
var foo = {
value: 2,
bar: function () {
return this.value;
}
}
//示例1
console.log(foo.bar());
//示例2
console.log((foo.bar)());
//示例3
console.log((foo.bar = foo.bar)());
//示例4
console.log((false || foo.bar)());
//示例5
console.log((foo.bar, foo.bar)());
foo.bar()
在示例 1 中,MemberExpression 计算的结果是 foo.bar,那么 foo.bar 是不是一个 Reference 呢?
查看规范 11.2.1 Property Accessors,这里展示了一个计算的过程,什么都不管了,就看最后一步:
Return a value of type Reference whose base value is baseValue and whose referenced name is propertyNameString, and whose strict mode flag is strict.
我们得知该表达式返回了一个 Reference 类型!
根据之前的内容,我们知道该值为:
js
var Reference = {
base: foo,
name: 'bar',
strict: false
};
然后按照之前的判断流程:
2.1 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)
该值是 Reference 类型,那么 IsPropertyReference(ref) 的结果是多少呢?
前面我们已经铺垫了 IsPropertyReference 方法,如果 base value 是一个对象,结果返回 true。
base value 为 foo,是一个对象,所以 IsPropertyReference(ref) 结果为 true。
这个时候我们就可以确定 this 的值了:
this = GetBase(ref)
GetBase 也已经铺垫了,获得 base value 值,这个例子中就是foo,所以 this 的值就是 foo ,示例1的结果就是 2!
唉呀妈呀,为了证明 this 指向foo,真是累死我了!但是知道了原理,剩下的就更快了。
(foo.bar)()
示例2:
console.log((foo.bar()())
foo.bar 被 () 包住,查看规范 11.1.6 The Grouping Operator
直接看结果部分:
Return the result of evaluating Expression. This may be of type Reference.
NOTE This algorithm does not apply GetValue to the result of evaluating Expression.
实际上 () 并没有对 MemberExpression 进行计算,所以其实跟示例 1 的结果是一样的
(foo.bar = foo.bar)()
看示例3,有赋值操作符,查看规范 11.13.1 Simple Assignment ( = ):
计算的第三步:
3.Let rval be GetValue(rref).
因为使用了 GetValue,所以返回的值不是 Reference 类型,
按照之前讲的判断逻辑:
2.3 如果 ref 不是Reference,那么 this 的值为 undefined
this 为 undefined,非严格模式下,this 的值为 undefined 的时候,其值会被隐式转换为全局对象。
(false || foo.bar)()
看示例4,逻辑与算法,查看规范 11.11 Binary Logical Operators:
计算第二步:
2.Let lval be GetValue(lref).
因为使用了 GetValue,所以返回的不是 Reference 类型,this 为 undefined
(foo.bar, foo.bar)()
看示例5,逗号操作符,查看规范11.14 Comma Operator ( , )
计算第二步:
2.Call GetValue(lref).
因为使用了 GetValue,所以返回的不是 Reference 类型,this 为 undefined
揭晓结果 所以最后一个例子的结果是:
js
var value = 1;
var foo = {
value: 2,
bar: function () {
return this.value;
}
}
//示例1
console.log(foo.bar()); // 2
//示例2
console.log((foo.bar)()); // 2
//示例3
console.log((foo.bar = foo.bar)()); // 1
//示例4
console.log((false || foo.bar)()); // 1
//示例5
console.log((foo.bar, foo.bar)()); // 1
补充
忘记了一个最最普通的情况:
js
function foo() {
console.log(this)
}
foo();
MemberExpression 是 foo,解析标识符,查看规范 10.3.1 Identifier Resolution,会返回一个 Reference 类型的值:
js
var fooReference = {
base: EnvironmentRecord,
name: 'foo',
strict: false
};
接下来进行判断:
2.1 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)
因为 base value 是 EnvironmentRecord,并不是一个 Object 类型,还记得前面讲过的 base value 的取值可能吗? 只可能是 undefined, an Object, a Boolean, a String, a Number, 和 an environment record 中的一种。
IsPropertyReference(ref) 的结果为 false,进入下个判断:
2.2 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)
base value 正是 Environment Record,所以会调用 ImplicitThisValue(ref)
查看规范 10.2.1.1.6,ImplicitThisValue 方法的介绍:该函数始终返回 undefined。
所以最后 this 的值就是 undefined。
更深层次的理解 this 的绑定
通过上面的分析,我们可以得出 this 的值绑定逻辑:
- 成员表达式(MemberExpression) 的计算结果会被存储为 Reference 类型。
- this 的值 由该 Reference 的 base value 确定,如果是对象,this 会指向该对象。 通过这个机制,JavaScript 能够实现灵活的 this 绑定,在不同的上下文中绑定不同的对象。