从ECMAScript规范中学习this
this的指向是js的难点之一,网上不同的文章对this的指向有着不同的解释,要不就是只给结果不给解释,要不就是解释描述模糊不清,因此要真正了解js中的this,还得去ECMAScript规范中学习。
this Keyword规范
在ES6中明确了this关键字的取值,在12.2.2中是这样描述的。
12.2.2 The this Keyword 12.2.2.1 Runtime Semantics: Evaluation PrimaryExpression : this Return ResolveThisBinding( ) .
也就是说this关键字的值取决于
ResolveThisBinding
方法。ResolveThisBinding
抽象操作
ResolveThisBinding
使用当前运行的执行上下文( the running execution context)的词法环境 (LexicalEnvironment)来确定关键字this。这会执行下面两步- let encRec = GetThisEnvironment( ).
- return envRec.GetThisBinding().
也就是说es6首先会调用 GetThisEnvironment ,获取到绑定this关键字的环境记录,然后在调用GetThisBinding获取到该环境记录绑定的this值,因此接下来需要知道抽象操作GetThisEnvironment是如何执行的。
原文:
The abstract operation ResolveThisBinding determines the binding of the keyword this using the LexicalEnvironment of the running execution context. ResolveThisBinding performs the following steps: Let envRec be GetThisEnvironment( ). Return envRec.GetThisBinding().
GetThisEnvironment
GetThisEnvironment
通过查找环境记录(Environment Record)来确定this关键字。这会执行以下步骤:
- 获取当前运行的执行上下文的词法环境,并赋值到lex
- 重复以下步骤
- 将lex的环境记录赋值到envRec
- 调用envRec.HasThisBinding()来确定该环境记录是否有绑定this
- 如果为true,则返回envRec
- 否则将lex指向当前词法环境的外层(outer)环境
这里要注意:全局环境是最外部的词法环境,也就是说第二个步骤不会一直重复下去,最终会以this绑定全局环境结束。
原文
The abstract operation GetThisEnvironment finds the Environment Record that currently supplies the binding of the keyword this. GetThisEnvironment performs the following steps: Let lex be the running execution context’s LexicalEnvironment. Repeat Let envRec be lex’s EnvironmentRecord. Let exists be envRec.HasThisBinding(). If exists is true, return envRec. Let outer be the value of lex’s outer environment reference. Let lex be outer.
词法环境(Lexical Environments)
词法环境是一种规范类型,用于根据ECMAScript代码的词法嵌套结构定义标识符与特定变量和函数的关联。词法环境由环境记录和外层词法环境的引用组成。通常,词法环境与ECMAScript代码的某些特定语法结构相关联,例如函数声明、块级声明或try…catch语句等,并且每次运行此类代码时都会创建一个新的词法环境。
环境记录记录着与其关联的词法环境范围内的所有被创建的标识符绑定。它被称为词法环境的环境记录(EnvironmentRecord)。换句话说就是词法环境定义了了标识符与具体变量、函数的关联关系,而环境记录则保存着这些标识符绑定。
全局环境
词法环境是嵌套结构,一个词法环境可以作为多个内部词法环境的外部词法环境。但是全局环境是没有外部环境的词汇环境。全局环境的外部环境引用为null。全局环境的环境记录可以预先填充标识符绑定,并包括一个关联的全局对象(例如浏览器中的window),其属性提供了一些全局环境的标识符绑定。此全局对象是全局环境的This绑定的值。在执行ECMAScript代码时,可以向全局对象添加其他属性,并修改初始属性。
模块环境
模块环境是一个词汇环境,包含模块顶级声明的绑定。它还包含由模块显式导入的绑定。模块环境的外部环境是全局环境。
函数环境
函数环境是对应于ECMAScript函数对象调用的词汇环境。函数环境可以建立新的this绑定。函数环境还捕获支持super方法调用所需的状态。
注意: 词法环境和环境记录值纯粹是规范机制,不需要对应于ECMAScript实现的任何特定构件。ECMAScript程序不可能直接访问或操作这些值。
环境记录(Environment Records)
ESCMAScript规范中使用了两种主要的环境记录值:声明性环境记录和对象环境记录。声明性环境记录用于定义ECMAScript语言语法元素(如FunctionDeclarations、VariableDeclarations和Catch子句)的效果,这些语法元素将标识符绑定与ECMAScript语言值直接关联。对象环境记录用于定义ECMAScript元素的效果,例如将标识符绑定与某些对象的属性相关联的WithStatement。全局环境记录和函数环境记录则是指具体被用于
script
内的全局声明和函数内顶级声明。环境记录是一个抽象类,包含三个子类:声明性环境记录、对象环境记录和全局环境记录。函数环境记录和模块环境记录是声明性环境记录的子类。也就是说环境记录总共有四种:函数环境记录、模块环境记录、对象环境记录和全局环境记录。
模块环境记录
模块环境记录保存着模块顶级声明和不可变的导入绑定。
模块环境记录的HasThisBinding方法总是返回true,并且GetThisBinding方法返回undefined。
全局环境记录
全局环境记录保存着所有顶级声明、全局对象的属性和所有内置的全局变量的绑定。
全局环境记录的HasThisBinding方法总是返回true,并且GetThisBinding方法返回全局环境记录绑定的全局对象
对象环境记录
每个对象环境记录都与它的绑定对象相关联。对象环境记录绑定与其绑定对象的属性名称直接对应的字符串标识符名称集合。无论[[Enumerable]]属性的设置如何,都会将自己的属性和继承的属性都包含在集合中。由于可以从对象中动态添加和删除属性,因此对象环境记录绑定的标识符集可能会发生更改,这是添加或删除属性的任何操作的副作用。由于这种副作用而创建的任何绑定都被视为可变绑定,即使相应属性的Writable属性的值为false。对象环境记录不存在不可变绑定。
对象环境记录的HasThisBinding方法总是返回false。
函数环境记录
函数环境记录是一种声明性环境记录,用于表示函数的顶级范围,如果函数不是箭头函数(ArrowFunction),则提供this绑定。如果函数不是ArrowFunction函数并引用super,则其函数环境记录还包含用于从函数内执行super方法调用的状态。
函数环境记录中有两个与this相关的内部属性和方法:
- [[thisValue]]: 函数调用时的this的值
- BindThisValue(): 设置[[thisValue]]并记录其已初始化。
函数调用
前面已经明确了函数环境记录中绑定了this,非箭头函数内this的值就是该函数环境记录中的
[[thisValue]]
,因此我们需要知道函数调用时[[thisValue]]
是如何确定的。ES6规范中函数调用的步骤如下:
- Let ref be the result of evaluating MemberExpression.
- Let func be GetValue(ref).
- ReturnIfAbrupt(func).
- If Type(ref) is Reference and IsPropertyReference(ref) is false and GetReferencedName(ref) is
"eval"
, then - If SameValue(func, %eval%) is true, then
- Let argList be ArgumentListEvaluation(Arguments).
- ReturnIfAbrupt(argList).
- If argList has no elements, return undefined.
- Let evalText be the first element of argList.
- If the source code matching this CallExpression is strict code, let strictCaller be true. Otherwise let strictCaller be false.
- Let evalRealm be the running execution context’s Realm.
- Return PerformEval(evalText, evalRealm, strictCaller, true). .
- If Type(ref) is Reference, then
- If IsPropertyReference(ref) is true, then
- Let thisValue be GetThisValue(ref).
- Else, the base of ref is an Environment Record
- Let refEnv be GetBase(ref).
- Let thisValue be refEnv.WithBaseObject().
- Let thisCall be this CallExpression.
- Let tailCall be IsInTailPosition(thisCall). (See 14.6.1)
- Return EvaluateDirectCall(func, thisValue, Arguments, tailCall).
我们省略其他步骤,只关注
thisValue
。主要在第5和第6步。首先我们来看ref是什么?ref是MemberExpression执行的结果。而MemberExpression是一个左侧表达式,它有以下几种形式:
MemberExpression[Yield] : PrimaryExpression[?Yield] MemberExpression[?Yield] [ Expression[In, ?Yield] ] MemberExpression[?Yield] . IdentifierName MemberExpression[?Yield] TemplateLiteral[?Yield] SuperProperty[?Yield] MetaProperty new MemberExpression[?Yield] Arguments[?Yield]
Reference 其实就是引用类型,ES6规范中是这样描述Reference的:
Reference引用由三个组件组成,即基值(base value)、引用名称(reference name)和布尔值严格引用标志。基值可以是undefined、对象、布尔值、字符串、symbol、数字或环境记录。如果基值是undefined则表示无法将引用解析为绑定。引用的名称是字符串或symbol。
接着是IsPropertyReference方法。定义如下:
IsPropertyReference(V). Returns true if either the base value is an object or HasPrimitiveBase(V) is true; otherwise returns false.
HasPrimitiveBase(V). Returns true if Type(base) is Boolean, String, Symbol, or Number.
也就是说,当引用类型的基值是一个对象,或者是Boolean, String, Symbol, or Number,则返回true,否则返回false。
而WithBaseObject方法的定义是:如果此环境记录与with语句关联,那么就返回with对象。否则,返回undefined。
最后是GetThisValue方法:
- Assert: IsPropertyReference(V) is true.
- If IsSuperReference(V), then
- Return the value of the thisValue component of the reference V.
- Return GetBase(V).
• IsSuperReference(V). Returns true if this reference has a thisValue component.
• GetBase(V). Returns the base value component of the reference V.
由此已经可以明确函数调用时this的确定步骤了:
- 声明ref = MemberExpression的返回结果
- 如果ref是引用类型
- 如果ref的基值是一个对象
- 如果引用类型的基值有thisValue,则this就是该thisValue
- 否则this就指向该基值
- 否则,ref的基值就是环境记录,this是undefined
- 如果ref不是引用类型,则this就是undefined
场景分析
在全局作用域中、模块顶级作用域中的this
console.log(this)
模块环境记录和全局环境记录中都有绑定this:
- 在全局作用域中的this会返回全局环境记录所绑定的全局对象,在浏览器中是window,在node中是global。
- 在模块顶级作用域中的this会直接返回undefined。
函数上下文中的this
普通函数中的this取值主要取决于函数调用情况,根据之前所讲的在函数调用时确定this的步骤,很容易就理解:
<script> // 左侧MemberExpression是一个对象,因此this就指向这个对象 const o = { fn() { console.log(this); }, }; o.fn(); // o // 通过包装对象调用函数也和通过普通对象调用方法一样 const b = new Boolean(); b.fn = function () { console.log(this); }; b.fn(); // Boolean // 函数在全局作用域中直接执行,运行时没有绑定thisValue,因此返回全局对象window function fn1() { console.log(this); } fn1(); // window // 函数的this与声明的位置无关,与调用方式有关 // fn2 相当于 window.fn2() const fn2 = o.fn; fn2(); // window /** * 类似于 * var temp = fn3() * temp() */ function fn3() { return function temp() { console.log(this); }; } fn3()(); // window /** * 类似于 * var o = fn4() * o.temp() */ function fn4() { var o = { temp() { console.log(this); }, }; return o; } fn4().temp(); // o </script> <script type="module"> // ref的基值是模块环境记录,this指向undefined function fn1() { console.log(this); } fn1(); </script>
原型链、setter和getter中的
this
指向的是调用这个方法的对象。Array.prototype.myCall = function(){ return this } const arr = [] console.log(arr.myCall() === arr); // true
箭头函数中的this
箭头函数的函数环境记录并不会绑定this,也就是说它将会按照GetThisEnvironment方法里的步骤逐层向外地查找外部环境记录中绑定的this。请看以下几个示例
const o = { fn1: () => { console.log(this); }, fn2() { const temp = () => { console.log(this); }; temp(); }, }; o.fn1(); o.fn2();
对于
o.fn1()
而言,fn1是箭头函数,因此函数环境记录中没有绑定this,因此会向外部环境记录查找绑定this,外部环境记录是对象o所关联的对象环境记录,对象环境记录HasThisBinding方法总是返回false,因此再向更外部的环境记录(也就是全局环境记录)查找,最终全局环境记录返回其绑定的全局对象作为this的值。o.fn2()
与o.fn1()
类似,区别是temp函数环境记录外部环境记录是一个(非箭头函数的)函数环境记录,其有绑定this指向对象o
,因此temp箭头函数的this的值就是对象o
注意: 箭头函数中的this取决于词法环境,也就是说与箭头函数定义的位置有关。
显式绑定this
通过
call
、apply
或bind
显示绑定this来调用函数,事实上是调用了函数内部方法[[Call]]
,它最终会通过函数环境记录中的BindThisValue
方法手动绑定this的值。不过需要注意的是这里要区分严格模式和非严格模式
- 严格模式下,传入的第一个参数就是this的值
- 非严格模式下
- 如果传入的第一个参数是null或者undefined,那么this指向全局对象
- 如果传入的第一个参数是对象,那么this就是这个对象
- 如果传入的第一个参数是booelan、string、number等,那么this会是它们的包装器对象。
造成这个现象的原因是严格模式下this可以是任意类型的值,但是非严格模式下this必须是一个对象。
function fn(){ console.log(this) } fn.call({}) // {} fn.apply(123) // Number对象
这里还要注意为箭头函数显式绑定this的情况,前面说过箭头函数的函数环境记录中没有绑定this,所以无关严格模式还是非严格模式,显式绑定this对箭头函数总是无效的,和直接调用效果相同。
var f = ()=>{ console.log(this); } f() // window var f = ()=>{ "use strict"; console.log(this); } f() // window
严格模式
在非严格模式下this总是是一个对象,但是在严格模式下this可以是任意值。非严格模式下的这种自动转化为对象的过程不仅是一种性能上的损耗,同时在浏览器中暴露出全局对象也会成为安全隐患,因为全局对象提供了访问那些所谓安全的 JavaScript 环境必须限制的功能的途径。所以对于一个开启严格模式的函数,指定的
this
不再被封装为对象,而且如果没有指定this
的话它值是undefined
"use strict"; function fun() { return this; } console.log(fun() === undefined); console.log(fun.call(2) === 2); console.log(fun.apply(null) === null); console.log(fun.call(undefined) === undefined); console.log(fun.bind(true)() === true); // 全为true
和非严格模式不同,如果最终没有找到thisValue,非严格吗模式下返回全局对象,而严格模式则返回undefined。
function fn(){ "use strict" console.log(this); } fn() // 函数调用时没有绑定thisValue,因此是undefined function fn(){ "use strict" console.log(this); } fn() // 函数调用时没有绑定thisValue,因此返回全局环境的thisValue,也就是window
闭包中的this
function fn(){ console.log(this); // o function f(){ console.log(this) // window } f() } const o = {} o.fn = fn o.fn()
造成这种现象的原因是因为闭包中中的匿名函数具有全局性,上面的代码相当于:
var temp = function() { console.log(this); // o function f(){ console.log(this) // window } f() } const o = {} o.fn = fn o.fn() function fn(){ console.log(this); // o var f = temp f() } const o = {} o.fn = fn o.fn()