this 是 JavaScript 中的关键字,是执行上下文的属性。它的主要用途是在函数和构造函数中。
this 的规则非常简单(如果您坚持最佳做法)。
规范中this的技术说明
ECMAScript standard 通过抽象操作定义this(缩写AO)ResolveThisBinding:
[AO] ResolveThisBinding [...] 使用 running execution context 的 LexicalEnvironment 确定关键字 this 的绑定。 【步骤】:
- 让 envRec 为 GetThisEnvironment()。
- 返回 ? envRec.GetThisBinding().
Global Environment Records、module Environment Records 和 function Environment Records 都有自己的 GetThisBinding 方法。
GetThisEnvironment AO 找到当前running execution context 的 LexicalEnvironment 并找到最近的上升环境记录(通过迭代访问它们的 [[OuterEnv]] 属性),它具有 this 绑定(即HasThisBinding 返回 true)。此过程以三种环境记录类型之一结束。
this 的值通常取决于代码是否在strict mode 中。
GetThisBinding 的返回值反映了当前执行上下文的this 的值,因此每当建立新的执行上下文时,this 都会解析为不同的值。当当前执行上下文被修改时,也会发生这种情况。以下小节列出了可能发生这种情况的五种情况。
您可以将代码示例放在AST explorer 中以跟随规范详细信息。
1。脚本中的全局执行上下文
这是在顶层评估的脚本代码,例如直接在<script>:
<script>
// Global context
console.log(this); // Logs global object.
setTimeout(function(){
console.log("Not global context");
});
</script>
在脚本的初始全局执行上下文中,评估 this 会导致 GetThisBinding 采取以下步骤:
全局环境记录的GetThisBinding具体方法envRec […] [这样做]:
- 返回envRec。[[GlobalThisValue]]。
全局环境记录的 [[GlobalThisValue]] 属性始终设置为主机定义的global object,可通过globalThis 访问(Web 上为window,Node.js 上为global;@ 987654337@)。按照InitializeHostDefinedRealm的步骤来了解[[GlobalThisValue]]属性是怎么来的。
ECMAScript 2015 中引入了模块。
这适用于模块,例如当直接在<script type="module"> 中时,而不是简单的<script>。
在模块的初始全局执行上下文中,评估 this 会导致 GetThisBinding 采取以下步骤:
模块环境记录的 GetThisBinding 具体方法 […] [这样做]:
- 返回未定义。
在模块中,this 的值在全局上下文中始终为 undefined。模块隐含在strict mode 中。
eval 调用有两种:direct 和 indirect。这种区别从 ECMAScript 第 5 版开始就存在了。
- 直接的
eval 调用通常看起来像eval(…); 或(eval)(…);(或((eval))(…); 等)。1仅当调用表达式适合窄模式时才直接。2
- 间接
eval 调用涉及以任何其他方式调用函数引用eval。可能是eval?.(…),(…, eval)(…),window.eval(…),eval.call(…,…),等等。它也将是aliasEval1(…)、aliasEval2(…)。另外,给定const originalEval = eval; window.eval = (x) => originalEval(x);,调用eval(…) 也是间接的。
请参阅chuckj’s answer to “(1, eval)('this') vs eval('this') in JavaScript?” 和Dmitry Soshnikov’s ECMA-262-5 in detail – Chapter 2: Strict Mode (archived),了解何时可以使用间接eval() 调用。
PerformEval 执行eval 代码。它创建了一个新的declarative Environment Record 作为它的 LexicalEnvironment,GetThisEnvironment 从中获取this 的值。
然后,如果this出现在eval代码中,则调用GetThisEnvironment找到的Environment Record的GetThisBinding方法并返回其值。
而创建的declarative Environment Record 取决于eval 调用是直接还是间接:
这意味着:
- 在直接评估中,
this 值不会改变;它取自名为 eval 的词法范围。
- 在间接评估中,
this 值是全局对象 (globalThis)。
new Function 呢? — new Function 与eval 类似,但不会立即调用代码;它创建了一个函数。 this 绑定不适用于此处的任何地方,除非调用该函数,该函数正常工作,如下一小节所述。
在调用函数时输入函数代码。
调用函数的语法有四种。
实际的函数调用发生在Call AO,调用时使用了一个根据上下文确定的thisValue;此参数在与调用相关的一长串调用中传递。 Call 调用函数的[[Call]] 内部槽。这会调用PrepareForOrdinaryCall,其中会创建一个新的function Environment Record:
函数环境记录是一个声明性环境记录,用于表示函数的顶级范围,如果函数不是ArrowFunction,则提供this 绑定。如果函数不是 ArrowFunction 函数并引用 super,则其函数 Environment Record 还包含用于从函数内部执行 super 方法调用的状态。
另外,函数Environment Record中还有[[ThisValue]]字段:
这是用于此函数调用的this 值。
NewFunctionEnvironment 调用还设置了函数环境的 [[ThisBindingStatus]] 属性。
[[Call]] 也调用OrdinaryCallBindThis,其中合适的thisArgument 是基于:
一旦确定,最终调用新创建的函数 Environment Record 的 BindThisValue 方法实际上会将 [[ThisValue]] 字段设置为 thisArgument。
最后,这个字段是 function Environment Record’s GetThisBinding AO 获取 this 值的地方:
一个函数Environment Record的GetThisBinding具体方法envRec […] [这样做]:
[…]
3. 返回 envRec.[[ThisValue]].
同样,this 值的确定方式取决于许多因素;这只是一个总体概述。有了这个技术背景,让我们来看看所有的具体例子。
当评估arrow function 时,函数对象的[[ThisMode]] 内部槽在OrdinaryFunctionCreate 中设置为“lexical”。
在OrdinaryCallBindThis,它接受一个函数F:
- 让 thisMode 为 F.[[ThisMode]].
- 如果 thisMode 是 lexical,则返回 NormalCompletion(
undefined)。
[…]
这只是意味着绑定 this 的算法的其余部分被跳过。箭头函数不绑定自己的 this 值。
那么,箭头函数中的this 是什么?回顾ResolveThisBinding和GetThisEnvironment,HasThisBinding method explicitly returns false。
一个函数Environment Record的HasThisBinding具体方法envRec […] [这样做]:
- 如果 envRec.[[ThisBindingStatus]] 是 lexical,则返回 false;否则,返回 true。
因此,外部环境被迭代地查找。该过程将在具有 this 绑定的三个环境之一中结束。
这只是意味着,在箭头函数体中,this 来自箭头函数的词法范围,或者换句话说(来自Arrow function vs function declaration / expressions: Are they equivalent / exchangeable?):
箭头函数没有自己的this […] 绑定。相反,[this identifier is] 像任何其他变量一样在词法范围内解析。这意味着在箭头函数内部,this [指] 在箭头函数被定义 的环境中的[this 的值](即“外部”箭头函数)。
在普通函数(function、methods)中,this由函数的调用方式决定。
这就是这些“语法变体”派上用场的地方。
考虑这个对象包含一个函数:
const refObj = {
func: function(){
console.log(this);
}
};
或者:
const refObj = {
func(){
console.log(this);
}
};
在以下任何函数调用中,func 中的 this 值将是 refObj。1
refObj.func()
refObj["func"]()
refObj?.func()
refObj.func?.()
refObj.func``
如果被调用的函数在语法上是基础对象的属性,那么这个基础将是调用的“引用”,在通常情况下,它将是this 的值。上面链接的评估步骤对此进行了解释;例如,在refObj.func()(或refObj["func"]())中,CallMemberExpression 是整个表达式refObj.func(),它由MemberExpression refObj.func 和Arguments @987654536 组成@。
而且,refObj.func 和 refObj 分别扮演三个角色:
- 它们都是表达式,
- 它们都是参考,并且
- 它们都是价值观。
refObj.func 作为 value 是可调用函数对象;对应的reference用于确定this绑定。
可选链接和标记模板示例的工作方式非常相似:基本上,引用是?.()、`` 或() 之前的所有内容。
EvaluateCall 在语法上使用该引用的IsPropertyReference 来确定它是否是对象的属性。它试图获取引用的 [[Base]] 属性(例如,refObj,应用于refObj.func;或foo.bar,应用于foo.bar.baz)。如果写成属性,那么GetThisValue会得到这个[[Base]]属性,并将其用作this值。
注意:Getters / Setters 的工作方式与方法相同,关于 this。简单属性不会影响执行上下文,例如这里,this 在全局范围内:
const o = {
a: 1,
b: this.a, // Is `globalThis.a`.
[this.a]: 2 // Refers to `globalThis.a`.
};
没有基本引用、严格模式和with 的调用
没有基引用的调用通常是一个不作为属性调用的函数。例如:
func(); // As opposed to `refObj.func();`.
passing or assigning methods 或使用 comma operator 时也会发生这种情况。这就是参考记录和价值之间的区别所在。
注意函数j:按照规范,你会注意到j只能返回函数对象(Value)本身,不能返回Reference Record。因此,基本引用 refObj 丢失。
const g = (f) => f(); // No base ref.
const h = refObj.func;
const j = () => refObj.func;
g(refObj.func);
h(); // No base ref.
j()(); // No base ref.
(0, refObj.func)(); // Another common pattern to remove the base ref.
EvaluateCall 调用 Call 时,此处的 thisValue 为 undefined。这在 OrdinaryCallBindThis 中有所不同(F:函数对象;thisArgument:传递给 Call 的 thisValue):
- 让 thisMode 为 F.[[ThisMode]].
[…]
- 如果 thisMode 为 strict,则让 thisValue 为 thisArgument。
- 否则,
- 如果 thisArgument 为 undefined 或 null,则
- 让 globalEnv 为 calleeRealm.[[GlobalEnv]].
- […]
- 让 thisValue 为 globalEnv。[[GlobalThisValue]]。
- 否则,
- 让thisValue成为! ToObject(thisArgument)。
- 注意:ToObject 产生包装对象 […]。
[…]
注意:步骤 5 将 this 的实际值设置为在严格模式下提供的 thisArgument — undefined 在这种情况下。在“草率模式”中,未定义或空的 thisArgument 会导致 this 成为全局 this 值。
如果IsPropertyReference 返回false,则EvaluateCall 采取以下步骤:
- 让 refEnv 为 ref.[[Base]].
- 断言:refEnv 是环境记录。
- 让 thisValue 为 refEnv.WithBaseObject()。
这是一个未定义的 thisValue 可能来自的地方:refEnv.WithBaseObject() 总是 undefined,except 在with 语句中。在这种情况下,thisValue 将是绑定对象。
还有Symbol.unscopables(Docs on MDN)来控制with的绑定行为。
总结一下,到目前为止:
function f1(){
console.log(this);
}
function f2(){
console.log(this);
}
function f3(){
console.log(this);
}
const o = {
f1,
f2,
[Symbol.unscopables]: {
f2: true
}
};
f1(); // Logs `globalThis`.
with(o){
f1(); // Logs `o`.
f2(); // `f2` is unscopable, so this logs `globalThis`.
f3(); // `f3` is not on `o`, so this logs `globalThis`.
}
和:
"use strict";
function f(){
console.log(this);
}
f(); // Logs `undefined`.
// `with` statements are not allowed in strict-mode code.
请注意,在评估 this 时,在哪里定义普通函数并不重要。
OrdinaryCallBindThis 的第 5 步与第 6.2 步(规范中的 6.b)相结合的另一个结果是,原始 this 值被强制转换为对象only 处于“草率”模式。
为了检查这一点,让我们介绍 this 值的另一个来源:覆盖 this 绑定的三个方法:4
Function.prototype.apply(thisArg, argArray)
-
Function.prototype. {call, bind} (thisArg, ...args)
.bind 创建一个绑定函数,其 this 绑定设置为 thisArg 并且不能再次更改。 .call 和 .apply 立即调用该函数,并将 this 绑定设置为 thisArg。
.call 和 .apply 使用指定的 thisArg 直接映射到 Call。 .bind 用BoundFunctionCreate 创建一个绑定函数。它们有它们自己的[[Call]] method,它会查找函数对象的 [[BoundThis]] 内部槽。
设置自定义this值的示例:
function f(){
console.log(this);
}
const myObj = {},
g = f.bind(myObj),
h = (m) => m();
// All of these log `myObj`.
g();
f.bind(myObj)();
f.call(myObj);
h(g);
对于对象,在严格模式和非严格模式下是一样的。
现在,尝试提供一个原始值:
function f(){
console.log(this);
}
const myString = "s",
g = f.bind(myString);
g(); // Logs `String { "s" }`.
f.call(myString); // Logs `String { "s" }`.
在非严格模式下,原语被强制转换为它们的对象包装形式。它与调用Object("s") 或new String("s") 时得到的对象相同。在严格模式下,您可以使用原语:
"use strict";
function f(){
console.log(this);
}
const myString = "s",
g = f.bind(myString);
g(); // Logs `"s"`.
f.call(myString); // Logs `"s"`.
图书馆利用这些方法,例如jQuery 将 this 设置为此处选择的 DOM 元素:
$("button").click(function(){
console.log(this); // Logs the clicked button.
});
当使用new 运算符将函数作为构造函数调用时,EvaluateNew 调用Construct,后者调用[[Construct]] method。如果函数是基本构造函数(即不是class extends…{…}),它会将 thisArgument 设置为从构造函数的原型创建的新对象。在构造函数中在this 上设置的属性最终会出现在生成的实例对象上。 this 是隐式返回的,除非您显式返回您自己的非原始值。
class 是一种创建构造函数的新方法,在 ECMAScript 2015 中引入。
function Old(a){
this.p = a;
}
const o = new Old(1);
console.log(o); // Logs `Old { p: 1 }`.
class New{
constructor(a){
this.p = a;
}
}
const n = new New(1);
console.log(n); // Logs `New { p: 1 }`.
类定义隐含在strict mode:
class A{
m1(){
return this;
}
m2(){
const m1 = this.m1;
console.log(m1());
}
}
new A().m2(); // Logs `undefined`.
new 的行为例外是 class extends…{…},如上所述。派生类在调用时不会立即设置它们的 this 值;只有通过一系列super 调用(在没有自己的constructor 的情况下隐式发生)到达基类时,它们才会这样做。不允许在调用super 之前使用this。
调用super 使用调用的词法范围(函数环境记录)的this 值调用超级构造函数。 GetThisValue 对 super 调用有一个特殊规则。它使用BindThisValue 将this 设置为该环境记录。
class DerivedNew extends New{
constructor(a, a2){
// Using `this` before `super` results in a ReferenceError.
super(a);
this.p2 = a2;
}
}
const n2 = new DerivedNew(1, 2);
console.log(n2); // Logs `DerivedNew { p: 1, p2: 2 }`.
5。评估类字段
ECMAScript 2022 中引入了实例字段和静态字段。
当评估 class 时,将执行 ClassDefinitionEvaluation,修改 running execution context。对于每个ClassElement:
- 如果一个字段是静态的,那么
this 指的是类本身,
- 如果字段不是静态的,则
this 指的是实例。
将私有字段(例如 #x)和方法添加到 PrivateEnvironment。
Static blocks 当前是TC39 stage 3 proposal。静态块与静态字段和方法的工作方式相同:其中的this 指的是类本身。
请注意,在方法和 getter/setter 中,this 的工作方式与普通函数属性中的一样。
class Demo{
a = this;
b(){
return this;
}
static c = this;
static d(){
return this;
}
// Getters, setters, private modifiers are also possible.
}
const demo = new Demo;
console.log(demo.a, demo.b()); // Both log `demo`.
console.log(Demo.c, Demo.d()); // Both log `Demo`.
1:(o.f)() 等价于o.f(); (f)() 等价于 f()。这在this 2ality article (archived) 中有解释。具体见how a ParenthesizedExpression is evaluated。
2:它必须是一个MemberExpression,不能是一个属性,必须有一个正好是“eval”的[[ReferencedName]],并且必须是 %eval% 内在对象。
3:只要规范说“让引用是评估 X的结果”,那么X 是您需要为其找到评估步骤的一些表达式。例如,评估 MemberExpression 或 CallExpression 是 these algorithms 之一的结果。其中一些导致Reference Record。
4:还有其他几种本地和宿主方法允许提供 this 值,特别是接受 thisArg 作为他们的第二个参数。任何人都可以创建自己的方法来更改 this,例如 (func, thisArg) => func.bind(thisArg)、(func, thisArg) => func.call(thisArg) 等。与往常一样,MDN 提供了很好的文档。
只是为了好玩,用一些例子来测试你的理解
对于每个代码 sn-p,回答问题:“标记行处this 的值是多少?为什么?”。
要显示答案,请单击灰色框。
-
if(true){
console.log(this); // What is `this` here?
}
globalThis。标记的行在初始全局执行上下文中进行评估。
-
const obj = {};
function myFun(){
return { // What is `this` here?
"is obj": this === obj,
"is globalThis": this === globalThis
};
}
obj.method = myFun;
console.log(obj.method());
obj。当将函数作为对象的属性调用时,调用时会将 this 绑定设置为引用obj.method 的base,即obj。
-
const obj = {
myMethod: function(){
return { // What is `this` here?
"is obj": this === obj,
"is globalThis": this === globalThis
};
}
},
myFun = obj.myMethod;
console.log(myFun());
globalThis。由于函数值myFun / obj.myMethod 不是从对象调用的,因此作为属性,this 绑定将为globalThis。
这与 Python 不同,在 Python 中访问方法 (obj.myMethod) 会创建一个 bound method object。
-
const obj = {
myFun: () => ({ // What is `this` here?
"is obj": this === obj,
"is globalThis": this === globalThis
})
};
console.log(obj.myFun());
globalThis。箭头函数不会创建自己的 this 绑定。词法作用域与初始全局作用域相同,所以this 是globalThis。
-
function myFun(){
console.log(this); // What is `this` here?
}
const obj = {
myMethod: function(){
eval("myFun()");
}
};
obj.myMethod();
globalThis。在评估直接 eval 调用时,this 是 obj。但是,在 eval 代码中,myFun 不会从对象中调用,因此 this 绑定设置为全局对象。
-
function myFun() {
// What is `this` here?
return {
"is obj": this === obj,
"is globalThis": this === globalThis
};
}
const obj = {};
console.log(myFun.call(obj));
obj。 myFun.call(obj); 行调用了特殊的内置函数 Function.prototype.call,它接受 thisArg 作为第一个参数。
-
class MyCls{
arrow = () => ({ // What is `this` here?
"is MyCls": this === MyCls,
"is globalThis": this === globalThis,
"is instance": this instanceof MyCls
});
}
console.log(new MyCls().arrow());
这是MyCls 的实例。箭头函数不会更改 this 绑定,因此它来自词法范围。因此,这与上面提到的类字段完全相同,例如a = this;。尝试将其更改为static arrow。你得到了你期望的结果吗?