前言
在第一节中,我们介绍了可执行代码以及执行上下文的相关知识,对于每个执行上下文,都有三个重要属性:
- 变量对象(
Variable object, VO
) - 作用域、作用域链(
Scope chain
) this
指向问题
这节重点介绍this问题,这也就面试中高频遇到的。
注:全文硬核烧脑,不建议碎片化时间阅读。
一、类型
在JavaScript中,说到类型,你们第一反应可能是基础数据类型和引用数据类型,但这节说的类型并非如此。类型又分为ECMAScript 语言类型
与 规范类型
。
ECMAScript语言类型
ECMAScript 语言类型
是 ECMAScript 程序员使用 ECMAScript 语言直接操作的值对应的类型。ECMAScript 语言类型包括未定义 (Undefined)
、 空值 (Null)
、 布尔值(Boolean)
、字符串 (String)
、数值 (Number)
、对象 (Object)
。
规范类型
规范类型
是描述 ECMAScript 语言构造与 ECMAScript 语言类型语意的算法所用的元值对应的类型。规范类型包括 引用 、 列表 、 完结 、 属性描述符 、 属性标志符 、 词法环境(Lexical Environment)、 环境纪录(Environment Record)
。规范类型的值是不一定对应 ECMAScript 实现里任何实体的虚拟对象。规范类型可用来描述 ECMAScript 表达式运算的中途结果,但是这些值不能存成对象的变量或是 ECMAScript 语言变量的值。
如果不能理解,没关系,只需要知道JavaScript中还存在一种用于规范类型,它的作用是用于描述语言的底层行为逻辑,并不真实存在于js代码中。
简单解释一下上面的规范类型:
词法环境
词法环境 是一个用于定义特定变量和函数标识符在 ECMAScript 代码的词法嵌套结构上关联关系的规范类型。一个词法环境由一个环境记录项 和可能为空的外部词法环境引用构成。
环境纪录项
环境记录项记录了在它的关联词法环境域内创建的标识符绑定情形。
分两类:声明式环境记录项 和 对象式环境记录项 。声明式环境记录项用于定义那些将 标识符 与语言值直接绑定的 ECMA 脚本语法元素,例如 函数定义 , 变量定义 以及 Catch 语句。对象式环境记录项用于定义那些将 标识符 与具体对象的属性绑定的 ECMA 脚本元素,例如 程序 以及 With 表达式 。
通俗的理解就是:在创建执行上下文的时候,JS需要初始化函数声明、变量声明、函数形参定义,决定内部函数或者代码块的作用域链情况,不同类型的代码会记录在不同的变量对象(环境记录项)中。
这节重点要讲解的是引用(Reference)
,它跟我们要介绍的this的指向有很大的关联。
二、引用-Reference
1、什么是Reference?
引用ECMAscript中的原话是:
The Reference type is used to explain the behaviour of such operators as
delete
,typeof
, and the assignment operators.
翻译过来是 Reference
类型就是用来解释诸如 delete、typeof
以及赋值等操作行为的。
2、Reference具体包含哪些内容?
A Reference is a resolved name binding. A Reference consists of three components, the base value, the referenced name and the Boolean valued strict reference flag. The base value is either undefined , an Object, a Boolean, a String, a Number, or an environment record (10.2.1). A base value of undefined indicates that the reference could not be resolved to a binding. The referenced name is a String.
官方这段话描述的是Reference的构成,其中有三个部分组成,如下:
- 基值 (
base value
) - 引用名称(
referenced name
) - 布尔值 严格引用 (
strict reference
) 标志
base value
值是 undefined、Object、Boolean、 String、Number、environment record 中的任意一种。
referenced name
就是属性名称。
strict reference
用于表示当前环境类型,即是否为严格环境。
举例说明:
js
// 举例一
var yc = 123;
// 对应的Reference为
var ycReference = {
base: EnvironmentRecord,
name: 'yc',
strict: false
}
// 举例二
var yc = {
fn: function() {
return this;
}
}
yc.fn();
//对应fn的Reference为
var fnReference = {
base: yc,
name: 'fn',
strict: false
}
问题来了,规范内部如何确认base
和name
的值是什么?
接下来我们就要说到GetBase(V)
和IsPropertyReference(V)
。
GetBase
GetBase(V). Returns the base value component of the reference V.
返回reference的base value
。
IsPropertyReference
IsPropertyReference(V). Returns true if either the base value is an object or HasPrimitiveBase(V) is true; otherwise returns false.
简单理解为:如果base value
是一个对象,就返回true
。
3、GetValue
GetValue
是在规范Reference 8.7.1
中提供了一个用于在Reference类型中获取对应值的方法。它的作用在于为一些表达式或者操作符的计算过程提供运算支持。
具体来看下面例子:
js
// 举例一
var yc = 123;
// 对应的Reference为
var ycReference = {
base: EnvironmentRecord,
name: 'yc'
}
// 非js或者浏览器提供的方法,故不能在实际中进行计算
GetValue(ycReference) // 123
如上,它返回了Reference
类型的值,属于真实的值。
结论:调用GetValue
方法返回具体的值,而不再是一个Reference
。 谨记!这个结论会在后面的计算中使用非常频繁!
4、推算this的指向
上面一大篇幅在讲Reference
类型相关的概念,可能听下来你还是没法理解具体的作用以及它跟this
的指向到底存在什么样的关联。下面进入高能阶段~
直接上栗子🌰:
js
'use strict'
function foo() {
console.log(this);
}
foo();
我们一般在分析this指向的时候,通常会拿函数调用的方式来说明,比如上面的例子,我们从经验上一眼可以看清楚this指向的是undefined
。
再来一个栗子🌰:
js
var name = '小明';
var yc = {
name: '云场',
fn: function() {
console.log(this.name)
}
}
// 示例一
yc.fn();
// 示例二
(false || yc.fn)();
// 示例三
(0, yc.fn)();
这时候,你还能快速准确的说出this的指向情况吗?自测一下~
想要搞清楚这个问题,首先我们先来看下规范中对于函数调用是如何描述的。
The production CallExpression : MemberExpression Arguments is evaluated as follows:
Let ref be the result of evaluating MemberExpression.
Let func be GetValue(ref).
Let argList be the result of evaluating Arguments , producing an internal list of argument values (see 11.2.4).
If IsCallable(func ) is false , throw a TypeError exception.
If Type(ref ) is Reference, then
If IsPropertyReference(ref ) is true, then
- Let thisValue be GetBase(ref).
Else, the base of ref is an Environment Record
- Let thisValue be the result of calling the ImplicitThisValue concrete method of GetBase(ref).
Else, Type(ref ) is not Reference.
- Let thisValue be undefined.
上面一串描述的是关于函数调用结果和表达式判断的概念,1,2,3,4,5表示的是函数调用结果的内部判断,1,6,7表示的是函数表达式的内部判断。表达式判断步骤如下:
- 计算
MemberExpression
的结果并复制给ref
- 判断
ref
是不是一个Reference
类型- 如果
ref
是一个Reference
类型,并且IsPropertyReference(ref)
是true
,那么this
值为GetBase(ref)
返回值 - 如果
ref
是一个Reference
类型,并且base value
是Environment Record
,那么this
值为ImplicitThisValue(ref)
返回值 - 如果
ref
不是一个Reference
类型,那么this
值为undefined
- 如果
那么问题来了,第一步中
MemberExpression
是什么东西?
我们通过规范11.2 Left-Hand-Side Expressions
看到MemberExpression
定义如下:
js
// 成员表达式
MemberExpression :
// 原始表达式
PrimaryExpression
// 函数定义表达式
FunctionExpression
// 属性访问表达式
MemberExpression [ Expression ]
// 属性访问表达式
MemberExpression . IdentifierName
// new 创建对象表达式
new MemberExpression Arguments
拿上面的例子来说明一下:
js
// 示例一
yc.fn(); // yc.fn 是 MemberExpression
// 示例二
(false || yc.fn)(); // (false || yc.fn) 是 MemberExpression
// 示例三
(0, yc.fn)(); // (0, yc.fn) 是 MemberExpression
简单理解就是:括号() 左侧部分便是MemberExpression
。
如何判断ref
是不是一个Reference
类型?
判断一个
ref
是不是一个Reference
,关键要看规范是如何处理不同类型的MemberExpression
。
拿上面的例子进行说明:
js
'use strict'
var name = '小明';
var yc = {
name: '云场'
fn: function() {
console.log(this.name)
}
}
// 示例一
yc.fn();
// 示例二
(false || yc.fn)();
// 示例三
(0, yc.fn)();
// 示例四
function fn(){
console.log(this);
}
fn();
示例一:yc.fn()
首先,我们知道这个MemberExpression
为yc.fn
,并且yc.fn
为属性访问表达式Property Accessors
,看规范11.2.1。
- 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
类型。
根据上面的判断步骤,那IsPropertyReference(ref)
返回的结果是什么?
还记得我们一开始说过IsPropertyReference
是如何计算的吗?如果base value
是一个对象,就返回true
。
通过上面的内容,我们知道yc.fn
的Reference
的值为:
js
var name = '小明';
var yc = {
name: '云场'
fn: function() {
console.log(this.name)
}
}
//对应fn的Reference为
var fnReference = {
base: yc,
name: 'fn',
strict: false
}
如上,base value
的值为yc,是一个对象,那么IsPropertyReference(ref)
返回true
。那么这时候this
的值为GetBase(ref)
。
js
this = GetBase(ref)
通过前面的内容,我们说过GetBase(ref)
返回的就是base value
,即为yc
,那么this = yc
。yc.fn
返回云场
。
cao,终于验证了this是指向了yc的,真费劲~,不过按照这个步骤,接下来的表达式就很好验证了~
示例二:(false || yc.fn)()
以此类推,MemberExpression
为(false || yc.fn)
,一个逻辑运算符,我们来看规范11.11 Binary Logical Operators
。
该运算符最终会返回一个GetValue(ref)
,根据第二节的第三小节,调用GetValue
方法返回具体的值,而不再是一个Reference
。
根据判断步骤:
如果
ref
不是一个Reference
类型,那么this
值为undefined
所以,严格模式下函数调用会报错,否则返回小明
。
示例三:(0, yc.fn)()
(0, yc.fn)
为逗号操作符,查看规范11.14 Comma Operator
。
明显,也是跟示例二一样,最后this
值为undefined
。严格模式下函数调用会报错,否则返回小明
。
示例四:fn()
最普通、常见的函数调用方式,MemberExpression
为fn
,即解析标识符(函数名、变量名、属性名等),根据规范10.3.1 Identifier Resolution 。
The result of evaluating an identifier is always a value of type Reference with its referenced name component equal to the Identifier String.
该标识符表达式总是返回Reference
类型。
js
var fnReference = {
base: EnvironmentRecord,
name: 'fn'
};
根据判断步骤2.1:
如果
ref
是一个Reference
类型,并且IsPropertyReference(ref)
是true
,那么this
值为GetBase(ref)
返回值
那么IsPropertyReference(ref)
的结果是什么?
根据前面的内容,如果base value
是一个对象,IsPropertyReference(ref)
就返回true
,然而fnReference
的base value
是EnvironmentRecord
,不是一个JavaScript对象类型,所以结果为false
。
根据判断步骤2.2:
如果
ref
是一个Reference
类型,并且base value
是Environment Record
,那么this
值为ImplicitThisValue(ref)
返回值
那么this
为ImplicitThisValue(ref)
,而ImplicitThisValue(ref)
又是什么东西?
根据规范10.2.1.1.6 ImplicitThisValue
Declarative Environment Records always return undefined as their ImplicitThisValue.
这玩意总是返回一个undefined
。所以,fn()
中this
的最终值为undefined
。-_-
最终结果
js
var name = '小明';
var yc = {
name: '云场',
fn: function() {
console.log(this.name)
}
}
// 示例一
var aa = yc.fn.bind(yc);
aa(); // 云场
// 示例二
(false || yc.fn)(); // 小明
// 示例三
(0, yc.fn)(); // 小明
// 示例四
function fn(){
console.log(this.name);
}
fn(); // 小明
三、总结
综上,看似简单、普通的 this
指向问题,从ECMAScript
实现上去理解却是复杂的,虽然从这个角度去分析比较复杂或者难以理解,一旦理解实现原理后,会给一个全新的视角去看待this
指向问题。
小试牛刀
上面示例四中,fn()
执行返回全局对象中的name
(非严格模式),那改为call
进行调用呢?尝试从规范的角度去解释~
js
function fn(){
console.log(this.name);
}
fn.call({name: '蛇皮'});