JavaScript中“this”的全面解析






4.92/5 (15投票s)
关键字“this”现在更容易理解了!
引言
变量作用域一直是每种编程语言的一部分。无论我们使用的是基于类的面向对象语言(例如 Java/C#)还是无类的面向对象语言,作用域都起着非常重要的作用。
作用域定义了什么?
最简单的说法,作用域定义了任何变量/数据的可见性。在 JavaScript 中,您可以将一段代码(函数/方法)定义为全局的,也可以将其保留在对象内并公开供公共使用。当涉及到 JavaScript 时,关键字 'this' 具有特殊的意义。其他语言也使用 'this' 关键字,但在 JavaScript 世界中尝试使用或理解它需要非常认真。
背景
JavaScript 不再仅仅是客户端语言。这意味着它可以用于编写客户端代码(在浏览器内)和服务器端代码(Node JS)。在本文中,我们将仅讨论当 JavaScript 用作客户端语言时,即当它在浏览器环境中运行时,'this' 关键字的行为。我将另写一篇文章来讨论 JavaScript 在 Node 等服务器端环境中使用时 'this' 的故事。
让我们从一些简单的例子开始,然后继续深入更复杂的场景,在这些场景中,如果我们不完全了解发生了什么,'this' 可能会变得很疯狂。
所以,首先
全局作用域中使用的 'this'
让我们看一下下面的代码
这是结果...
因此,当 'this' 不用在对象、函数或方法内部时,它就指向全局“Window”对象。浏览器中的一切都包含在一个 window 对象中。您可以将“window”对象视为 JavaScript 中所有对象的父对象。完整的 DOM(文档对象模型),包括您的 <html>、<head> 和 <body> 标签,都属于 document 对象的一部分,而 document 对象也是全局“Window”对象的子对象。您可以通过执行 window.document(或在这种情况下为 this.document,其中 'this' 指的是全局 window 对象)来检查这一点。
提醒您一下,在本文中,我们仅在浏览器环境中讨论 JavaScript,其中 'this' 指的是全局作用域中的 window 对象。本文不讨论服务器端 JavaScript。
在全局作用域中 'this' 的另一个示例是声明全局变量时。全局变量是在 JavaScript 中的对象、函数或方法之外声明的变量。例如,请看下面的代码
这是结果...
我们不必显式使用 'this' 关键字。您可以直接引用该变量。我们使用 'this' 关键字来引用变量,目的是为了证明当我们在全局作用域中声明变量时,该变量会自动添加到全局对象,即 Window 对象。这意味着 'employeeCount' 变量现在是 Window 对象的一个变量。证据?我在控制台中尝试查找...执行 window.employeeCount ,看看下面的结果...
[附加说明: 不建议在 JavaScript 的全局空间中有自己的变量。事实上,这是编写 JS 代码时可以做的最糟糕的事情之一。本文中的所有示例都仅用于演示目的。您可能想阅读有关“模块”模式的内容,以及如何在 JS 中避免在全局作用域中声明您的内容。]
[ 附加说明: 您可能注意到我在本文中写了“函数或方法”。为什么?因为函数和方法在 JavaScript 中是不同的东西,真的吗?是的,它们确实指的是不同的代码块。我们很快就会在本文中看到。]
无论如何,接下来
函数内部使用的 'this'
让我们在全局环境中声明一个函数,考虑下面的代码
因此,当我们最后调用 AddEmployee() 函数时,这是结果...
哦,天哪!您期望有什么不同的吗?如果您来自不同的 OO 语言,也许来自像 Java/C# 这样的基于类的 OO 语言,您可能会期望 'this' 关键字在上述情况下具有自己的含义。我也曾期望 'this' 在函数中使用时具有个人作用域,但这个家伙 'this' 在 JavaScript 中有点疯狂。那么发生了什么?'this' 仍然指向全局 window 对象。这次,我们向全局作用域(window 对象)添加了 2 个东西。您得猜猜!哦,是的,我知道您明白了... 1) employeeCount 变量和 2) AddEmployee() 函数。两者现在都是 window 对象的一部分,可以通过引用 window 对象来调用。证据?在这里...
对象字面量中使用的 'this'
在 JavaScript 中,我们使用字面量来存储值。其中,最常见的字面量是 JS 中的数组字面量和对象字面量。我们有变量来存储简单值,但当涉及到有一个变量包含几个其他变量,并且您可能还想有一些函数来定义该对象的函数/行为时,您会想使用对象字面量。
让我们看下面的例子
在上面的代码中,Employee 对象字面量有一个名为 'headCount' 的属性和一个名为 'AddEmployee' 的方法。这不是一个函数,而是被称为方法。当匿名函数中的一段代码被赋予对象字面量中的属性时,它被称为方法。我知道您明白了,这就是 JS 中函数和方法的区别。
调用 JavaScript 中的方法时,'this' 的值设置为调用该方法的对象。
在这个例子中,AddEmployee() 是通过 Employee 对象引用的,因此 AddEmployee() 方法中的 'this' 指的是 Employee 对象。由于 Employee 对象有一个 headCount 属性,所以显示了它。
如果我调用 Employee.AddEmployee() 方法多次,this.headCount 仍将引用对象字面量 Employee 中的同一个 headCount,并会继续递增它。我为了节省时间在控制台中做了这个,请在下面查看
[一些额外的冒险: 如果您真的想深入研究,请删除行 this.headCount++ 中的 'this' 关键字,看看会发生什么。您一定会发现发生了什么,但给您一个提示是,‘无论’会发生什么,因为函数将尝试在全局作用域中查找它。如果您尝试了,请在评论中告诉我!]
到目前为止一切顺利,让我们深入研究一些更复杂的场景,以了解 JavaScript 中的 'this' 有何作用。
构造函数中使用的 'this'
如我们所见,对象字面量是 JavaScript 中即时构建对象的一种方式。然而,如果我们想在 JavaScript 中创建多个相同类型的对象,我们就会使用构造函数。它有点像其他语言中的类,“有点”但又不完全一样。
如何使用构造函数创建对象?
2 步
a) 写一个简单的函数
b) 使用 'new' 关键字调用函数,并将引用存储在一个变量中
所以让我们看一个例子
猜猜怎么着!我们进入了一个不同的世界。使用 'new' 关键字调用函数会改变 'this' 的故事!!!耶!终于看到一些不同的东西了。那么发生了什么?当您使用 'new' 关键字调用函数(通常是为了创建对象)时,'this' 被设置为一个空对象。证据?请查看上面下面运行的结果
现在 'this' 不再指向全局 window 对象。'this' 现在有了自己的世界,它不必处理全局作用域。哦,我太兴奋了!!!您呢?
让我们在这个构造函数中添加一些属性和函数
当上面的代码运行时,并且我们使用 'new' 关键字调用构造函数时,除了 AddEmployee() 函数之外,所有内容都会运行,因为我们还没有调用它。接下来,我们通过引用 Employee 类型的 oEmp 对象来调用 AddEmployee() 函数。正如我之前所说,JavaScript 中的构造函数用于创建相似类型的对象。我们稍后会看到这一点。好的,那么上面代码的结果是什么?
AddEmployee() 方法中 'this' 的值再次由调用者决定。这里的调用者是 oEmp,它是 Employee 类型的一个对象,因此 'this.headCount' 指的是 Employee 构造函数中定义的 headCount。如果您仔细查看上面的结果,您可以看到 Employee 最初是一个空对象,在添加了一个属性和一个函数之后,Employee 对象不再是一个空对象。它包含我们在其中定义的属性和函数。我们可以根据需要多次使用 oEmp 对象的引用来调用 AddEmployee() 函数,为了节省时间,我再次在控制台中这样做了...
我们如何使用构造函数创建超过 1 个对象。
当我们运行第 23 行时,AddEmployee() 中的 'this.headCount' 属于 HumanResource 对象。类似地,在第 26 行,当调用 AddEmployee() 时,'this.headCount' 的值仅属于 Sales 对象。
[您可能想在控制台中尝试多次调用 HumanResource.AddEmployee() 和 Sales.AddEmployee() 并检查它们的计数是否分开。两者都有自己的 'this']
总之,在使用构造函数时跟踪 'this',您需要检查在该方法被调用时(您正在使用 'this' 关键字的方法)是哪个对象。
在构造函数的方法内部调用全局函数时使用的 'this'
到目前为止的旅程很顺利,不是吗?好的,现在有一些情况,您将在构造函数的方法内部调用存在于全局作用域中的函数。没明白我刚才说的话吗?看看下面的代码
我为您的理解对代码做了一些小的更改。在上面的代码中,我们定义了一个单独的 Log() 服务/函数,它定义在全局作用域中。每当用户添加新员工时,我们都想记录此操作。此外,我们想检查员工人数是否已超过我们的阈值。在这种情况下,我们需要给出消息“员工数量已超出。”。另外,这次我们将 headCount 初始化为 10,以创建这种情况下的一个示例。
好的,一切看起来都很好,但运行上面的代码会发生什么?让我们看看结果...
我们可以看到 headCount 超过 10,但 Log() 函数中的 if(this.headCount > 10) 条件失败。为什么?哦,是的,您想到了正确的事情。回想一下,全局作用域中定义的函数中的 'this' 指的是 window 对象。猜猜怎么着,Log() 在 window 对象中搜索 'headCount' 属性,但找不到它,this.headCount 返回 'undefined',而 'undefined' > 10 是一个假条件。
该怎么办?有出路吗?我们能告诉 Log() 方法 'this' 的含义吗?我们能改变全局作用域中定义的函数中 'this' 的含义吗?
是的!我们可以...通过在构造方法中调用函数时传递 'this' 本身来实现。看看我们如何在下面的代码中调用 Log() 方法
另外,看看我们是如何在 Log() 函数的定义中通过一个名为 'emp' 的局部变量来捕获传入的 'this' 的。还看看我们是如何在 if 语句及其内部语句中使用 'emp' 变量的。'emp' 保存了 Log() 函数被调用处 'this' 的引用,这意味着 'emp' 拥有 Employee 构造函数的所有变量和方法。
您可以使用 'emp' 以外的任何其他变量名代替 'emp',除了 'this' 关键字本身。
[附加说明: 您不能使用变量名 'this' 作为函数中的参数变量。'this' 是保留关键字,如果您尝试这样做,JavaScript 将会报错]
现在一切正常。证据?看看
当全局函数被设置为方法/事件处理程序时使用的 'this'
页面上有几种情况需要处理事件。其中最著名的是按钮的单击事件。我们通常在 DOM 完全加载时设置按钮的单击事件,通常在 window.onload 中或在使用 jQuery 时在 $(document).ready() 中。为简单起见,我没有使用任何 DOM 操作库,所以使用简单的 onload 事件处理程序,让我们看一个例子
假设,我们的 <body> 中有这个 HTML
而在 <head> 标签内有这个脚本
当 DOM (实际上当所有资源(如图片/脚本)也加载完成时)完全加载时,将调用 SetAction() 方法,您可能想阅读有关 onload 和 document.ready 之间区别的内容)。
在 SetAction() 函数中,我们为 ID 为 'btnAddTicket' 的按钮设置了处理程序。如果您仔细看,这里 AddTicket() 函数被用作一个方法。为什么?您怎么知道?因为 AddTicket() 的引用被赋给了一个属性,如果您还记得,当一个函数引用被赋给一个属性/变量时,它就被称为方法。
您可能想猜猜这段代码是否能正常运行,因为我们总是先看到错误的例子:D 这次正好相反。哦,是的!代码运行完美,每次点击按钮时,计数器都会递增,按钮的文本/值/标题会显示您已经添加了多少张票。让我们看看在我疯狂地按了 5 次按钮后的样子。我还将控制台捕获在屏幕内,请也看看。
正如我之前写的,“当您调用 JavaScript 中的方法时,'this' 的值设置为调用该方法的对象”。这里也是一样。 AddTicket() 知道 'this' 的含义,因为它是在按钮对象上调用的。
如果我想在调用处理程序之前做一些事情,也许做一些重要的任务,然后调用函数/事件处理程序(AddTicket())。请记住,我们的处理程序 AddTicket() 定义在全局作用域中。
好的,我们来做刚才说的事情,看看下面的代码
我们在 JavaScript 中这样做。我们现在有一个匿名函数被设置为按钮的点击事件,我们可以在调用 AddTicket() 函数之前在其中做任何我们想做的事情。
会发生什么?它奏效了吗?它看起来一样,但事实并非如此!!!!这里 AddTicket() 不再作为方法行事。它现在只是一个定义在全局作用域中的函数。它没有直接被设置为属性,而是被调用了!这就是区别。
您猜怎么着,我们又回到了起点,AddTicket() 中的朋友 'this' 现在又指向了全局 window 对象,也就是 JavaScript 中所有对象的父对象(在浏览器中运行时)。哦,天哪!!!如何摆脱这种情况?该怎么办?如何告诉 AddTicket() 函数仍然指向同一个 'this',即指向按钮对象。
我们可以做和之前完全一样的事情,我们可以将 'this' 关键字传递给 AddTicket() 函数,将其捕获为一个参数,然后根据需要使用该局部变量。
一切正常,我们又回到了正轨。
但是,作为一名软件工程师(而不仅仅是一名程序员),我们脑子里总会有一个想法“我们可以做得更好吗?有更好的方法吗?” 我们关心的是为什么每次都要传递 'this' 对象?随后,全局函数会问,为什么我需要明确地捕获我被调用的地方的作用域。
Call() 和 Apply() 来救援!
在 JavaScript 中有一些特殊的方法,您可以通过它们来改变任何作用域中 'this' 的全部含义。在本文中,我将不讨论 call() 和 apply() 之间的区别,让我们留到以后再说,但通过使用 call/apply,我们有 3 个好处,
1. 我们可以改变被调用函数中 'this' 的含义
2. 我们不必在被调用函数中显式捕获它。(是的,我们仍然需要从调用函数中显式地传递它)
3.被调用函数,即独立的全局函数,再次变得通用,我们不必更改 'this' 关键字为任何传递的变量(正如我们在上一个解决方案中所做的那样)
让我们来看看
一切再次正常工作。 您可以使用完全相同的方式使用 apply。
[额外内容: 您可能想在此处阅读有关 Call & Apply 的内容。
https://mdn.org.cn/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply
https://mdn.org.cn/en/JavaScript/Reference/Global_Objects/Function/call
]
另一个 'this' 的用途...一个流行的用途!
我通常会尝试用最简单的语言来写作。这个例子实际上侧重于 JavaScript 中的函数调用模式。为了简单起见,回想一下当一个函数被调用为函数和一个函数被调用为方法时的区别。我分享这个最后的例子是因为您更有可能在任何大规模/小规模的 JavaScript 应用程序中找到它,而且我不想让您回来对我说“嘿!你在文章中漏掉了 'this' 的这个用法。我不敢说我已经涵盖了地球上 'this' 的每一个用法,但确实涵盖了被广泛使用的。
考虑下面的例子
上面的例子可能看起来有点偏离主题,但让我试着解释一下。这是我能想到的最简单的例子。
在上面的代码中,我们首先定义了一个空的字面量对象,然后单独向它添加了一个方法(我们通常这样做,创建一个空字面量,然后稍后添加我们想要的任何内容)。当我们调用方法 Add() 时,它会调用另一个方法 Helper(),它是一个类似于 Add() 的内部函数。我想解释的是 'this' 在外部上下文和内部上下文(即在内部函数中)的含义。
结果是什么?这是...
哦,我的天!!!外部函数 Add() 的 'this' 看起来还可以,它指向 Employee 对象字面量,并显示它有一个名为 Add() 的函数。在这种情况下,内部函数发生了什么?它指向全局 window 对象。为什么?为什么会发生这种情况?我们需要一遍又一遍地回顾同一个故事才能清楚地理解 'this' 的用法。Add() 函数是一个方法,因为它被添加为 Employee 对象字面量的一个属性,因此 'this' 指的是 Employee 对象字面量本身。另一方面,内部函数 Helper() 是独立定义的,不是 Employee 对象字面量的一个属性,由于 Helper() 不是一个方法,Helper() 中的 'this' 不指向 Employee 对象字面量,而是指向全局 window 对象。
这种情况有解决办法吗?幸运的是,有!
我们首先在外部函数中将 'this' 的引用赋值给另一个变量。传统上/历史上,我们将这个变量命名为 'that',但您可以根据需要命名。'that' 这个词不是保留关键字,也没有意义。一旦我们有了新变量中 'this' 的引用,我们就可以使用这个新变量来引用与外部函数相同的 'this' 含义。请记住,内部函数的 'this' 仍然是相同的,这只是一个流行的解决方法来克服这种情况。
所以这是代码和控制台结果...
关注点
在 JavaScript 中跟踪 'this' 很重要。很容易陷入 JavaScript 代码看起来“还可以”但实际上并非如此的境地。绝大多数时候,这是因为 JavaScript 中这个特殊的 'this' 关键字。
我希望我能为您添加更多情况,以便您更好地理解 'this'。请在此处留下评论或在此处发送推文给我>>> @AnasFirdousi