一尘不染

“this”关键字如何工作?

javascript

我注意到this网站上似乎没有明确解释关键字是什么以及它是如何在 JavaScript 中正确(和错误地)使用的。

我目睹了它的一些非常奇怪的行为,并且无法理解它为什么会发生。

如何this工作以及何时应该使用它?


阅读 213

收藏
2021-10-16

共1个答案

一尘不染

this是 JavaScript 中的关键字,是执行上下文的属性。它的主要用途是在函数和构造函数中。

我建议先阅读Mike West的文章JavaScript 中的作用域存档)。这是对thisJavaScript 中作用域链和作用域链概念的出色而友好的介绍。的规则this非常简单(如果您坚持最佳实践)。

this规范中的技术说明

ECMAScript标准定义this经由抽象操作(缩写AOResolveThisBinding

[AO] ResolveThisBinding […]this使用正在运行的执行上下文的 LexicalEnvironment 来确定关键字的绑定。[脚步]:

  1. envRecGetThisEnvironment ()。
  2. 返回 ?envRec .GetThisBinding()。

全局环境记录模块环境记录函数环境记录都有自己的 GetThisBinding 方法。

所述GetThisEnvironment AO找到当前正在运行的执行上下文的LexicalEnvironment和查找最接近方兴未艾环境记录(通过迭代地访问他们的[[OuterEnv]]特性),其具有这种结合(即HasThisBinding返回)。此过程以三种环境记录类型之一结束。

的值this通常取决于代码是否处于严格模式

GetThisBinding 的返回值反映了this当前执行上下文的值,因此无论何时建立新的执行上下文,都会this解析为一个不同的值。这也可能在修改当前执行上下文时发生。以下小节列出了可能发生这种情况的五种情况。

您可以将代码示例放在AST 资源管理器中以遵循规范详细信息。

1. 脚本中的全局执行上下文

这是在顶层评估的脚本代码,例如直接在 a 中<script>

<script>
// Global context
console.log(this); // Logs global object.

setTimeout(function(){
  console.log("Not global context");
});
</script>

当在脚本的初始全局执行上下文中时,评估this会导致GetThisBinding采取以下步骤:

全局环境记录envRec […] [这样做]的 GetThisBinding 具体方法:

  1. 返回envRec .[[GlobalThisValue]]。

全局环境记录的 [[GlobalThisValue]] 属性始终设置为主机定义的全局对象,可通过globalThiswindow在 Web 上,global在 Node.js 上;MDN上的文档)访问该对象。按照InitializeHostDefinedRealm的步骤了解 [[GlobalThisValue]] 属性是如何形成的。

2.模块中的全局执行上下文

模块已在 ECMAScript 2015 中引入。

这适用于模块,例如直接在 a 内部时<script type="module">,而不是简单的<script>.

在模块的初始全局执行上下文中,求值this会导致GetThisBinding采取以下步骤:

模块环境记录的 GetThisBinding 具体方法 […] [这样做]:

  1. 返回undefined

在模块中,的值this总是undefined在全局上下文中。模块隐式处于严格模式

3. 输入评估

有两种eval调用:直接调用和间接调用。这种区别自 ECMAScript 第 5 版起就存在。

  • 直接eval调用通常看起来像eval(……);(eval)(…… );(或((eval))(……);等)。1仅当调用表达式适合窄模式时才是直接的。2
  • 间接eval调用涉及eval以任何其他方式调用函数引用。这可能是eval?.()(, eval)()window.eval()eval.call(,)等。考虑const aliasEval1 = eval; window.aliasEval2 = eval;,它也将是aliasEval1()aliasEval2()。另外,给定const originalEval = eval; window.eval = (x) => originalEval(x);,调用eval()也将是间接的。

请参阅chuckj 对) JavaScript 中的](1, eval)(‘this’) vs eval(‘this’)”的回答Dmitry Soshnikov 的 ECMA-262-5 的详细信息——第 2 章:严格模式已存档),了解何时可能使用间接eval()调用。

PerformEval执行eval代码。它创建一个新的声明性环境记录作为其 LexicalEnvironment,这是GetThisEnvironment 从中获取this值的地方。

然后,如果this出现在eval代码中,环境记录的GetThisBinding方法发现GetThisEnvironment被称为其返回值。

并且创建的声明性环境记录取决于eval调用是直接的还是间接的:

意思是:

  • 在直接 eval 中,this值不会改变;它取自称为eval.
  • 在间接 eval 中,this值是全局对象 ( globalThis)。

怎么样new Function — new Function类似于eval,但它不会立即调用代码;它创建了一个函数。一装订任何地方都不会在这里适用,除非当函数被调用,如在下一小节解释其正常工作。

4. 输入功能

调用函数时会出现输入函数代码。

调用函数有四类语法。

EvaluateCall

AO是这三个执行:

3

EvaluateNew

3

实际的函数调用发生在Call AO 处,调用时使用根据上下文确定的thisValue;此参数在与调用相关的一长串调用中传递。Call调用函数的[[Call]]内部槽。这将调用PrepareForOrdinaryCall,其中创建了一个新的函数环境记录

功能环境记录是一种声明环境记录被用于表示功能的顶层范围和,如果函数不是ArrowFunction,提供了一种this结合。如果函数不是ArrowFunction函数并引用super,则其函数环境记录还包含用于super从函数内部执行方法调用的状态。

另外,在一个函数 Environment Record 中有 [[ThisValue]] 字段:

这是this用于此函数调用的值。

NewFunctionEnvironment通话也设置功能环境的[[ThisBindingStatus]]属性。

[[Call]]还调用OrdinaryCallBindThis,其中适当的thisArgument是基于以下因素确定的:

  • 原始参考,
  • 函数的类型,以及
  • 代码是否处于严格模式

一旦确定,对新创建的函数 Environment Record的BindThisValue方法的最终调用实际上将 [[ThisValue]] 字段设置为thisArgument

最后,这个字段是函数 Environment Record 的 GetThisBinding AOthis从以下位置获取值的地方:

函数 Environment Record envRec […] [这样做]的 GetThisBinding 具体方法:

[…]
\3. 返回envRec .[[ThisValue]]。

同样,值的确切确定方式取决于许多因素;这只是一个总体概述。有了这个技术背景,让我们检查所有具体的例子。

箭头函数

当评估箭头函数时,函数对象的 [[ThisMode]] 内部槽在OrdinaryFunctionCreate 中设置为“词法”

OrdinaryCallBindThis,它接受一个函数F

  1. thisModeF .[[ThisMode]]。
  2. 如果thisMode词法的,则返回 NormalCompletion( undefined)。[…]

这只是意味着将跳过绑定this的算法的其余部分。箭头函数不绑定自己的this值。

那么,this箭头函数内部是什么?回顾ResolveThisBindingGetThisEnvironmentHasThisBinding 方法显式返回false

HasThisBinding 函数的具体方法 Environment Record envRec […] [这样做]:

  1. 如果envRec .[[ThisBindingStatus]] 是lexical,则返回false;否则,返回true

因此,外部环境被迭代地查找。该过程将在具有this绑定的三个环境之一中结束。

这只是意味着,在箭头函数体中,this来自箭头函数的词法范围,或者换句话说(来自[箭头函数与函数声明/表达式:它们是否等效/可交换?):

箭头函数没有自己的this[…] 绑定。相反,[这个标识符]像任何其他变量一样在词法范围内解析。这意味着在箭头函数内部,this[引用] 到定义this箭头函数的环境中的[值] (即箭头函数的“外部”)。

功能属性

在正常功能(function方法),this来确定由所述函数是如何被调用

这就是这些“语法变体”派上用场的地方。

考虑这个包含函数的对象:

const refObj = {
    func: function(){
      console.log(this);
    }
  };

或者:

const refObj = {
    func(){
      console.log(this);
    }
  };

在以下任何函数调用中,this里面的值func都是refObj. 1

  • refObj.func()
  • refObj["func"]()
  • refObj?.func()
  • refObj.func?.()
  • `refObj.func```

如果被调用的函数在语法上是一个基对象的属性,那么这个基将是调用的“引用”,在通常情况下,它是 的值this。上面链接的评估步骤对此进行了解释;例如,在refObj.func()(或refObj["func"]())中,CallMemberExpression是整个表达式refObj.func(),它由MemberExpression refObj.funcArguments 组成 ()

而且,refObj.funcrefObj扮演三个角色,每个角色:

  • 它们都是表达,
  • 它们都是参考,并且
  • 他们都是价值观。

refObj.func作为是可调用的函数对象;相应的参考用于确定this绑定。

可选链接和标记模板示例的工作方式非常相似:基本上,引用是在?.()、 之前```或 之前的所有内容()`。

EvaluateCall使用该引用的IsPropertyReference在语法上确定它是否是对象的属性。它试图获取引用的 [[Base]] 属性(例如refObj,当应用于refObj.func; 或foo.bar应用于 时foo.bar.baz)。如果将其写为属性,则GetThisValue将获取此 [[Base]] 属性并将其用作值。

注意:关于.getter/setter 的工作方式与方法相同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();`.

传递或分配方法或使用逗号运算符时也会发生这种情况。这是参考记录和值之间的差异相关的地方。

const g = (f) => f(); // No base ref.
const h = refObj.func;

g(refObj.func);
h(); // No base ref.
(0, refObj.func)(); // Another common pattern to remove the base ref.

EvaluateCall电话呼叫thisValue不确定这里。这在OrdinaryCallBindThisF:函数对象;thisArgument:传递给CallthisValue)中有所不同:

  1. thisModeF .[[ThisMode]]。

[…]

  1. 如果thisMode严格的,则让thisValuethisArgument

  2. 别的,

  3. 如果

    thisArgument

    undefined

    null

    ,则

    1. globalEnvcalleeRealm .[[GlobalEnv]]。
    2. […]
    3. thisValueglobalEnv .[[GlobalThisValue]]。
  4. 别的,

    1. thisValue成为!ToObject (thisArgument)。
    2. 注意:ToObject生成包装对象 […]。

[…]

注意:第 5 步在严格模式下将 的实际值设置this为提供的thisArgument - undefined在这种情况下。在“草率模式”中,未定义或空的thisArgument导致this成为全局this值。

如果IsPropertyReference返回false,则EvaluateCall采取以下步骤:

  1. refEnvref .[[Base]]。
  2. 断言:refEnv是环境记录。
  3. thisValuerefEnv .WithBaseObject()。

这就是未定义的thisValue可能来自:refEnvWithBaseObject()总是不确定的除了with声明。在这种情况下,thisValue将是绑定对象。

还有Symbol.unscopablesMDN 上的文档)来控制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它并不重要*,其中*一个正常的函数定义

.call, .apply, .bind, thisArg和原语

OrdinaryCallBindThis的第 5 步与第 6.2 步(规范中的 6.b)相结合的另一个结果是,在“草率”模式下将原始this值强制转换为对象。

为了检查这一点,让我们为this值引入另一个来源:覆盖this绑定的三个方法:4

  • Function.prototype.apply(thisArg, argArray)
  • Function.prototype.{ call, bind}(thisArg, ...args)

.bind创建一个绑定函数,其this绑定设置为thisArg并且不能再次更改。.call.apply立即调用该函数,并将this绑定设置为thisArg

.call`并使用指定的*thisArg*`.apply`直接映射到[Call](https://tc39.es/ecma262/#sec-call)。使用[BoundFunctionCreate](https://tc39.es/ecma262/#sec-boundfunctioncreate)创建绑定函数。它们有*自己的*[[[Call\]] 方法](https://tc39.es/ecma262/#sec-bound-function-exotic-objects-call-thisargument-argumentslist),用于查找函数对象的 [[BoundThis]] 内部插槽。`.bind

设置自定义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")or时获得的对象类型相同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

当使用new运算符将函数作为构造函数调用时,EvaluateNew调用Construct,后者调用[[Construct]] 方法。如果函数是一个基类的构造(即,不是class extends{}),它设置thisArgument从构造函数的原型创建新的对象。this在构造函数中设置的属性将最终出现在生成的实例对象上。this隐式返回,除非您显式返回您自己的非原始值。

Aclass是一种创建构造函数的新方法,在 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 }`.

类定义隐含在严格模式中

class A{  m1(){    return this;  }  m2(){    const m1 = this.m1;        console.log(m1());  }}new A().m2(); // Logs `undefined`.

super

行为的例外newclass extends{},如上所述。派生类不会在调用时立即设置它们的this值;他们只在super调用后这样做(在没有自己的情况下隐式发生constructor)。使用this呼叫之前super是不允许的。

调用使用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评估 a 时,执行ClassDefinitionEvaluation,修改正在运行的执行上下文。对于每个ClassElement

  • 如果字段是静态的,则this指的是类本身,
  • 如果字段不是静态的,则this引用实例。

私有字段(例如#x)和方法被添加到 PrivateEnvironment。

静态块目前是TC39 第 3 阶段的提案。静态块的工作方式与静态字段和方法相同: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()。这在这篇 2ality 文章存档)中进行了解释。特别是查看ParenthesizedExpression如何计算的

2:它必须是MemberExpression,不能是属性,必须有一个 [[ReferencedName]] 正好是“eval”,并且必须是 %eval% 内在对象。

3:每当规范说“让ref成为对X求值的结果。”,那么X是一些您需要查找求值步骤的表达式。例如,评估MemberExpressionCallExpression是这些算法之一的结果。其中一些导致参考记录

4:也有允许提供一些其他本地和主机方法这个值,特别是Array.prototype.mapArray.prototype.forEach等接受一个thisArg作为他们的第二个参数。任何人都可以做自己的方法来改变this一样(func, thisArg) => func.bind(thisArg)(func, thisArg) => func.call(thisArg)等像往常一样,MDN提供了巨大的文档。


只是为了好玩,用一些例子测试你的理解

对于每个代码片段,回答问题:this标记行的值是多少?为什么?” .

要显示答案,请单击灰色框。

  1. js if(true){ console.log(this); // What is `this` here? }

``

```js
const obj = {};

function myFun(){
return { // What is this here?
“is obj”: this === obj,
“is globalThis”: this === globalThis
};
}

obj.method = myFun;

console.log(obj.method());

```

Run code snippet

Expand snippet

``````

``js const obj = { myMethod: function(){ return { // What isthis` here?
“is obj”: this === obj,
“is globalThis”: this === globalThis
};
}
},
myFun = obj.myMethod;

console.log(myFun());

```

Run code snippet

Expand snippet

``````````

``js const obj = { myFun: () => ({ // What isthis` here?
“is obj”: this === obj,
“is globalThis”: this === globalThis
})
};

console.log(obj.myFun());

```

Run code snippet

Expand snippet

``````

  1. ``js function myFun(){ console.log(this); // What isthis` here?
    }

const obj = {
myMethod: function(){
eval(“myFun()”);
}
};

obj.myMethod();
```

````````

``js function myFun() { // What isthis` here?
return {
“is obj”: this === obj,
“is globalThis”: this === globalThis
};
}

const obj = {};

console.log(myFun.call(obj));

```

Run code snippet

Expand snippet

````````

``js class MyCls{ arrow = () => ({ // What isthis` here?
“is MyCls”: this === MyCls,
“is globalThis”: this === globalThis,
“is instance”: this instanceof MyCls
});
}

console.log(new MyCls().arrow());

```

2021-10-16