JavaScript——深入了解“this”关键字

2025-06-07

JavaScript——深入了解“this”关键字

最初发布在我的个人博客debuggr.io

在本文中,我们将学习如何识别和辨别this在给定上下文中指的是什么,并探讨引擎考虑哪些规则和条件来确定this关键词的引用。

您还可以在我的博客debuggr.io上阅读本文和其他文章

挑战

JavaScript 中最具挑战性的概念之一是this关键字,可能是因为它与其他语言有很大不同,或者是因为确定其值的规则并不那么明确。

让我们引用MDN中的一段话

在大多数情况下,this 的值由函数的调用方式(运行时绑定)决定。它无法在执行期间通过赋值来设置,并且每次调用函数时它都可能不同……

这确实很有挑战性。一方面,它说this是在运行时确定的——即动态绑定;但另一方面,它又说In most cases...,这意味着它可以是静态绑定的。为什么某个东西既是静态的又是动态的?我们如何在给定的上下文中确定它是哪一种?这正是我们现在要探究的!

什么是静态?

让我们看一个 JavaScript 中静态内容的例子,比如“局部变量环境”——通常称为范围。

每次调用函数时,都会创建一个新的执行上下文并将其推送到调用栈的顶部(我们的应用程序启动时,已经有一个默认的执行上下文,通常称为全局上下文)。
每个执行上下文都包含一个“局部变量环境”,通常称为局部作用域(在全局执行上下文中称为全局作用域)。

给出以下代码片段:

function foo(){
  var message = 'Hello!';
  console.log(message);
}
foo()

只需查看foo的声明,我们就知道它message属于哪个作用域——foo函数执行上下文的局部作用域。因为var语句声明了一个函数作用域的变量

另一个例子:

function foo(){
  var message = 'Hello';
  {
    let message = 'there!'
    console.log(message) // there!
  }
  console.log(message) // Hello
}

foo()

请注意,在块内部我们得到的结果与在块外部得到的结果不同,这是因为let语句声明了一个块范围局部变量

我们只需观察函数的减速过程就能知道会发生什么,因为 JavaScript 的作用域是静态确定的(词法),或者说是在“设计时”确定的。
无论我们在何处以及如何运行该函数,它的局部作用域都不会改变。
换句话说,我们可以说变量的作用域取决于变量的声明位置

什么是动态?

如果静态的意思是“某物被声明的地方”,那么我们可能会说动态的意思是“某物如何运行”。

让我们想象一下 JavaScript 中的作用域是动态的:
注意,这不是真正的语法⚠️

function foo(){
  // not a real syntax!!! ⚠️
  let message = if(foo in myObj) "Hello" else "There"
  console.log(message)
}

let myObj = {
  foo
}; 

myObj.foo() // Hello
foo() // There

如你所见,与静态作用域示例相比,我们现在无法message仅通过查看 的声明来确定 的最终值foo,我们需要查看它在何处以及如何被调用。这是因为message变量的值是在执行 时foo根据一组条件确定的。
这看起来可能很奇怪,但当我们处理上下文时,这与事实相差无几。this每次运行函数时,JavaScript 引擎都会进行一些检查,并有条件地设置 的引用this

有一些规则,顺序很重要
你知道吗,让我们把它们写出来,就像我们自己写引擎一样:
注意,这不是真正的语法⚠️

function foo(){
  // not real syntax!!! ⚠️
  if(foo is ArrowFunction) doNothing;
  else if(foo called with new) this = {};
  else if(
    foo called with apply || 
    foo called with call  ||
    foo called with bind  ||
  ) this = thisArg
  else if(foo called within an object) this = thatObject
  else if(strictMode){
    this = undefined
  } else{
    // default binding, last resort
    this = window;
    // or global in node
  }

  console.log(this); // who knows? we need to see where and how it runs
}

看起来有点繁琐和复杂,也许这个流程图可以提供更好的可视化效果:

此流程图

正如您所见,我们可以将流程分为两部分:

  • 静态绑定 - 箭头函数
  • 动态绑定-其余条件

让我们来看一下:

  1. 它是一个箭头函数吗? - 如果相关的执行上下文是由箭头函数创建的,那么什么也不做,含义this将由包装执行上下文设置。
  2. 该函数是否用 调用new -
    当使用关键字调用函数时,new引擎将为我们做一些事情:

    • 创建一个新对象并设置this引用它。
    • 将该对象__proto__(在规范[[Prototype]]中调用)引用到函数的对象prototype
    • 返回新创建的对象(this)。

    因此,为了确定它是什么,我们知道它将是一个只需通过使用关键字this调用函数即可自动创建的新对象。new

  3. call该函数是否使用/apply或?调用bind -
    然后设置this为作为第一个参数传递的任何内容。

  4. 该函数是否作为对象方法调用-
    然后设置this为点或方括号左侧的对象。

  5. strict mode吗? -
    那么thisundefined

  6. 默认情况下-
    this将引用全局/窗口。

测验

衡量我们理解的最好方法是测试自己,所以让我们做一个测验。在新选项卡上打开流程图,并从上到下浏览每个问题(答案如下所示):

尝试回答将会打印到控制台上的内容。

问题 #1

function logThis(){
  console.log(this);
}

const myObj = {
  logThis
}

myObj.logThis()

问题 #2

function logThis(){
  console.log(this);
}

const myObj = {
  foo: function(){
    logThis();
  }
}

myObj.foo()

问题 #3

const logThis = () => {
  console.log(this);
}

const myObj = {
  foo: logThis
}

myObj.foo()

问题 #4

function logThis() {
  console.log(this);
}

const myObj = { name: "sag1v" }

logThis.apply(myObj)

问题 #5

const logThis = () => {
  console.log(this);
}

const myObj = { name: "sag1v" }

logThis.apply(myObj)

问题 #6

function logThis(){
  console.log(this);
}

const someObj = new logThis()

问题 #7

function logThis(){
  'use strict'
  console.log(this);
}

function myFunc(){
  logThis();
}

const someObj = new myFunc()

问题 #8

function logThis(){
  console.log(this);
}

class myClass {
  logThat(){
    logThis()
  }
}

const myClassInstance = new myClass()
myClassInstance.logThat()

问题 #9

function logThis(){
  console.log(this);
}

class myClass {
  logThat(){
    logThis.call(this)
  }
}

const myClassInstance = new myClass()
myClassInstance.logThat()

问题 #10

class myClass {
  logThis = () => {
    console.log(this);
  }
}

const myObj = { name: 'sagiv' };

const myClassInstance = new myClass()
myClassInstance.logThis.call(myObj)

附加题

问题 #11

function logThis() {
  console.log(this);
}

const btn = document.getElementById('btn');
btn.addEventListener('click', logThis);

问题 #12

const logThis = () => {
  console.log(this);
}

const btn = document.getElementById('btn');
btn.addEventListener('click', logThis);

答案

答案 #1

function logThis(){
  console.log(this);
}

const myObj = {
  logThis
}

myObj.logThis()

结果 - myObj
说明:

  • logThis箭头函数吗? - 不是。
  • 被叫logThisnew?- 没有。
  • 是否logThis通过 call/apply/bind 调用?- 没有。
  • logThis称为对象方法吗?——是的,myObj留给点去吧。

答案 #2

function logThis(){
  console.log(this);
}

const myObj = {
  foo: function(){
    logThis();
  }
}

myObj.foo()

结果 - window
说明:

  • logThis箭头函数吗? - 不是。
  • 被叫logThisnew?- 没有。
  • 是否logThis通过 call/apply/bind 调用?- 没有。
  • logThis称为对象方法吗? - 没有。
  • 开着吗strict mode? - 没有。
  • 默认情况 - window(或全局)。

答案 #3

const logThis = () => {
  console.log(this);
}

const myObj = {
  foo: logThis
}

myObj.foo()

结果 - window
说明:

  • logThis箭头函数吗?——是的,无论this包装上下文中如何设置。在这种情况下,包装上下文是“全局执行上下文”,它内部this引用的是 window/全局对象。

答案 #4

function logThis() {
  console.log(this);
}

const myObj = { name: "sag1v" }

logThis.apply(myObj)

结果 - myObj
说明:

  • logThis箭头函数吗? - 不是。
  • 被叫logThisnew?- 没有。
  • logThis通过 call / apply / bind 调用的吗?- 是的,无论传入哪个第一个参数 -myObj在这种情况下。

答案 #5

const logThis = () => {
  console.log(this);
}

const myObj = { name: "sag1v" }

logThis.apply(myObj)

结果 - window
说明:

  • logThis箭头函数吗?——是的,无论this包装上下文中如何设置。在这种情况下,包装上下文是“全局执行上下文”,它内部this引用的是 window/全局对象。

答案 #6

function logThis(){
  console.log(this);
}

const someObj = new logThis()

结果 - 由 创建的对象logThis
说明:

  • logThis箭头函数吗? - 不是。
  • logThis调用了new吗?-是的,那么this函数内部就是一个自动创建的对象。

答案 #7

function logThis(){
  'use strict'
  console.log(this);
}

function myFunc(){
  logThis();
}

const someObj = new myFunc()

结果 - undefined
说明:

  • logThis箭头函数吗? - 不是。
  • 被叫logThisnew?- 没有。
  • 是否logThis通过 call/apply/bind 调用?- 没有。
  • logThis称为对象方法吗? - 没有。
  • 开着吗strict mode? -this是的undefined

答案 #8

function logThis(){
  console.log(this);
}

class myClass {
  logThat(){
    logThis()
  }
}

const myClassInstance = new myClass()
myClassInstance.logThat()

结果 - window
说明:

  • logThis箭头函数吗? - 不是。
  • 被叫logThisnew?- 没有。
  • 是否logThis通过 call/apply/bind 调用?- 没有。
  • logThis称为对象方法吗? - 没有。
  • 开着吗strict mode? - 没有。
  • 默认情况 - window(或全局)。

答案 #9

function logThis(){
  console.log(this);
}

class myClass {
  logThat(){
    logThis.call(this)
  }
}

const myClassInstance = new myClass()
myClassInstance.logThat()

结果 - 由 创建的对象myClass
说明:

  • logThis箭头函数吗? - 不是。
  • 被叫logThisnew?- 没有。
  • logThis通过 call / apply / bind 调用的吗?——是的,无论第一个参数传入什么。好的,但是我们传递了this! ,它在执行上下文this中指的是什么logThat?让我们检查一下:
    • logThat箭头函数吗? - 不是。
    • 被叫logThatnew?- 没有。
    • 是否logThat通过 call/apply/bind 调用?- 没有。
    • logThat称为对象方法吗? - 是的,this对象留给了点 -myClass在这种情况下,里面是自动创建的对象。

答案 #10

class myClass {
  logThis = () => {
    console.log(this);
  }
}

const myObj = { name: 'sagiv' };

const myClassInstance = new myClass()
myClassInstance.logThis.call(myObj)

结果 - 由 创建的对象myClass
说明:

  • 这是logThis箭头函数吗?——是的,在本例中this,它指向包装上下文设置的任何内容myClass。让我们检查一下this包装上下文中它指向的内容:
    • myClass箭头函数吗? - 不是。
    • myClass称为new? - 是的,this指的是新创建的对象(实例)。

请注意,我们正在使用类字段,这是目前处于第 3 阶段的提案

答案 #11

function logThis() {
  console.log(this);
}

const btn = document.getElementById('btn');
btn.addEventListener('click', logThis);

结果 -btn元素。
解释:
这是一个棘手的问题,因为我们从未讨论过附加到DOM元素的事件处理程序。您可以将附加到DOM元素的事件处理程序视为元素对象(在本例中是对象)内部的方法btn。我们可以将其视为我们执行过的操作btn.click(),甚至…… btn.logThis()。请注意,这并非幕后运作的确切过程,但这种对处理程序调用的可视化可以帮助我们形成关于……设置的“心理模型” this。您可以在MDN
上阅读更多相关信息。

现在让我们来看看这个流程:

  • logThis箭头函数吗? - 不是。
  • 被叫logThisnew?- 没有。
  • 是否logThis通过 call/apply/bind 调用?- 没有。
  • logThis称为对象方法吗?——是的(有点),在我们的例子中btn是留给点的。

答案 #12

const logThis = () => {
  console.log(this);
}

const btn = document.getElementById('btn');
btn.addEventListener('click', logThis);

结果 - window.
解释

  • logThis箭头函数吗?——是的,无论this包装上下文中如何设置。在这种情况下,包装上下文是“全局执行上下文”,它内部this引用的是 window/全局对象。

总结

我们现在知道的赋值this可以是动态的,也可以是静态的(词汇的)。

  • 箭头函数将使其静态化,甚至根本不会改变this。这意味着我们需要了解this在包装执行上下文中设置的内容。
  • 普通函数将动态地进行,这意味着它取决于函数的调用方式。

现在看起来可能有点吓人,很复杂,你可能会想怎么记住流程图。其实你不需要,你可以保存或打印这个流程图,甚至可以自己制作一个。每次你需要知道this代码中引用了什么,只需看一下它,然后开始理解其中的条件即可。放心,随着时间的推移,你需要看这个流程图的次数会越来越少。

我希望它能提供信息并有所帮助,如果您有任何进一步的说明或更正,请随时发表评论或在推特上给我发私信(@sag1v)。

您可以在我的博客debuggr.io上阅读我的更多文章

文章来源:https://dev.to/sag1v/javascript-the-this-key-word-in-depth-4pkm
PREV
React 性能优化
NEXT
使用 Laravel 服务存储库模式实现 CRUD