你对“这个”了解多少?
“这个”是什么?
用最简单的术语来说,JavaScript 关键字指this
的是它在运行时所属的对象,取决于它的调用站点(调用它的地方)。
然而,要理解它在任何给定上下文中指的是什么,需要对一些相关概念有更深入的了解,本文将对此进行介绍。
首先,this
根据访问位置,可以具有以下值:
-
默认情况下:
this
指的是global
对象。 -
在函数内部:
this
指的是global
对象。strict
但在模式下,this
将是undefined
。 -
方法内部:
this
指的是所有者对象。(方法是属于对象内部的函数。换句话说,它是对象的属性。) -
在事件中:
this
指触发事件的元素。 -
在立即调用函数表达式 (IIFE) 中:
this
指向global
对象。在strict
模式下,this
将是undefined
,就像全局上下文中的任何其他函数一样。 -
在 Fat-Arrow 函数内部:当使用粗箭头
()=>
定义函数时,它不会为其创建新值this
,而是继续引用函数外部所引用的同一对象。
本文希望让您了解如何将这些值分配给this
,以及如何利用这些知识来满足我们的要求。
调用站点和调用堆栈
正如上一节所讨论的,我们知道这是为每个函数调用进行的运行时绑定,这完全取决于它被调用的具体位置。
代码中调用相关函数的位置称为调用点。理解确定调用点对于理解在执行的任何给定点 this 会绑定到什么位置至关重要。
虽然查找调用站点通常就像定位函数调用位置一样简单,但由于某些编码模式可能会掩盖它,因此它可能并不总是那么清楚。
因此,考虑调用堆栈非常重要,调用堆栈是让我们进入我们所关注的当前执行阶段的函数堆栈。
让我们举一个简单的例子来说明如何确定调用堆栈和调用站点。
// Example 1. Call-sites and Call-stacks | |
function thunderbolt(){ | |
debugger; | |
console.log("Using Thunderbolt!"); | |
} | |
function attack(){ | |
console.log("Attacking!"); | |
thunderbolt(); // <- Call-site for thunderbolt() | |
} | |
function choosePikachu(){ | |
console.log("Pikachu, I choose you!"); | |
attack(); // <- Call-site for attack() | |
} | |
choosePikachu(); // <- Call-site for choosePikachu() | |
// Execution starts when choosePikachu() is called. | |
// Call stack : 0) choosePikachu | |
// choosePikachu() calls attack() | |
// Call stack : 0) choosePikachu, 1) attack | |
// attack() calls thunderbolt() | |
// Call stack : 0) choosePikachu, 1) attack, 2) thunderbolt |
// Example 1. Call-sites and Call-stacks | |
function thunderbolt(){ | |
debugger; | |
console.log("Using Thunderbolt!"); | |
} | |
function attack(){ | |
console.log("Attacking!"); | |
thunderbolt(); // <- Call-site for thunderbolt() | |
} | |
function choosePikachu(){ | |
console.log("Pikachu, I choose you!"); | |
attack(); // <- Call-site for attack() | |
} | |
choosePikachu(); // <- Call-site for choosePikachu() | |
// Execution starts when choosePikachu() is called. | |
// Call stack : 0) choosePikachu | |
// choosePikachu() calls attack() | |
// Call stack : 0) choosePikachu, 1) attack | |
// attack() calls thunderbolt() | |
// Call stack : 0) choosePikachu, 1) attack, 2) thunderbolt |
通过按顺序跟踪函数调用链,您可以确定调用堆栈和调用站点。
* 确定调用堆栈的技巧
debugger
利用任何现代浏览器的开发工具提供的内置 JS 。
在执行任何 JS 代码时,都可以使用关键字 , 设置断点debugger
,以在浏览器中的该点停止执行。
假设我们在thunderbolt()
被调用时添加一个断点。
调试器在自定义断点处停止执行,可以在右侧查看该点的函数调用堆栈。
在上图中,我们可以看到,执行在我们提到debugger
关键字的位置停止了,一旦thunderbolt()
被调用。此时,我们将不会观察到其后的任何代码执行(在本例中,debugger
只有日志)。thunderbolt()
我们现在主要感兴趣的点是右侧清晰显示的调用堆栈(anonymous)
,与我们在上面的示例中确定的相同。堆栈底部指的是对的初始全局调用choosePikachu()
。
“this” 的绑定规则
现在我们了解了什么是调用站点和调用堆栈,我们可以了解调用站点如何确定在执行期间将保留什么。
有四条通用规则适用。首先,让我们分别理解它们,然后,当多个规则适用于调用点时,它们的优先顺序也需要注意。
1. 默认绑定
当其他规则均不适用时,这是默认的“全部捕获”规则。它来自最常见的函数调用情况,即独立函数调用。
我们来看下面的例子。
ultraBall
在范围内声明的变量与在同名对象global
上声明属性相同。global
在 内部getPokemon()
,对 this 的引用默认指向对象。因此,我们会看到正在记录global
的值。this.ultraBall
但是,如果strict
模式在全局或内部有效,则不允许getPokemon
该对象进行默认绑定。在这种情况下,我们将看到错误。global
TypeError : 'this' is 'undefined'
2.隐式绑定
如果调用站点具有上下文对象(如果通过拥有或包含对象调用函数,作为其属性),则适用隐式绑定。
该规则指出,当函数引用有一个上下文对象时,该对象应该用于其方法调用的this
绑定。
让我们看几个例子来说明可能出现的不同情况。
由于对象pikachu
是this
调用的getBaseSpeed
,this.baseSpeed
因此与同义pikachu.baseSpeed
。
让我们看另一个例子,看看对象属性引用链的顶层或最后一层如何对隐式this
绑定的调用站点有影响。
我们可以看到,baseSpeed
值仍然是90
。这是因为 的调用getBaseSpeed
绑定到了它的直接调用者 ,pikachu
后者充当了它的this
绑定。在这个上下文中,baseSpeedvalue
是90
。
让我们看更多示例来展示隐式绑定可能看起来出乎意料的常见情况。
在这个例子中,如果将 赋值给另一个变量,我们就失去了 的隐式this
绑定。现在, for引用的是对象(默认绑定生效)。因此, for 调用将是。pikachu
pikachu.getBaseSpeed
baseSpeedFunction
baseSpeedFunction
this
global
this.baseSpeed
50
现在,一种更常见且不太明显的隐式绑定失效的情况是当我们传递回调函数时。请考虑以下示例:
再次,在回调函数 executor 内部executeFunction
,我们实际上是在传递一个对 的引用pikachu.getBaseSpeedfunction
。执行时,this
将再次绑定到对象(如果启用了 模式global
,则抛出),而不是。TypeError
strict
pikachu
函数回调失去绑定的情况很常见this
。另一种意外情况是,当我们将回调传递给的函数故意更改this
调用的 时,可能会出现这种情况。例如,流行的 JavaScript 库中的事件处理程序this
通常会修改,使其指向DOM element
触发事件的 。
你实际上无法控制回调函数引用的执行方式。目前为止,你无法控制调用方分配你想要的绑定。这时,显式绑定就派上用场了。
3. 显式绑定
为了解决隐式绑定造成的意外丢失this
,我们可以将的值明确设置this
为函数调用的给定对象。
有几种内置方法可以帮助我们实现显式绑定,例如:
bind() 方法
bind()
是属性的一种方法Function.prototype
。这意味着bind()
它可以被每个函数使用。
该bind()
方法创建一个新函数,当调用该函数时,其 this 关键字设置为提供的值,并且在调用新函数时,在提供的任何参数之前有一个给定的参数序列。
换句话说,返回一个新函数,该函数被硬编码以使用指定的上下文设置bind()
来调用原始函数。this
call() 和 apply() 方法
call()
和apply()
也是Function.prototype
属性的方法,用法类似但略有不同。
该call()
方法调用具有给定值和单独提供的参数的函数this
。
而该apply()
方法调用具有给定值的函数this
,并以数组(或类似数组的对象)的形式提供参数。
Pokémon
通过Pokémon.call()
或的显式绑定调用Pokémon.apply()
允许我们强制其this
成为this
函数PokémonExtension
。
另外,上述示例中值得注意的一点是,所有 的实例都PokémonExtension
将各自绑定this
到其中 的执行Pokémon
。这种显式绑定也称为硬绑定。
4. 新的绑定
在 JavaScript 中,实际上没有“构造函数”这样的东西,而是函数的构造调用。
当一个函数在其前面被调用时new
(也称为构造函数调用),以下操作会自动完成。
-
一个全新的物体凭空被创造出来(也就是构造出来)。
-
新构造的对象是
[[Prototype]]
-linked 的。(超出本文讨论范围) -
新构造的对象被设置为该函数调用的 this 绑定。
-
除非函数返回其自己的替代对象,否则新调用的函数调用将自动返回新构造的对象。
所有约束规则均有效
应该清楚的是,默认绑定是四个规则中优先级最低的规则。
让我们比较一下隐式绑定、显式绑定和新绑定。
隐式与显式
正如我们所见,with的显式绑定优先于其自身的隐式绑定,第二种情况也是如此。firstAttempt.catchPokémon
secondAttempt
因此,显式绑定的优先级高于隐式绑定。
隐式与新式
因此,新绑定比隐式绑定更具先例。
明确还是新颖?
new
和call
或apply
不能一起使用,所以var fourthAttempt = new catchPokémon.call(firstAttempt);
不允许直接用显式绑定来测试新绑定。但是,我们仍然可以使用硬绑定来测试两者的优先级。
attemptBinder
与 严格绑定firstAttempt
,但new attemptBinder(“Steelix”)
并没有像我们预期的那样变为 ,而是保留firstAttempt.name
了。"Steelix"
"Onix"
相反,对 的硬绑定调用attemptBinder("Steelix")
可以被 覆盖new
。由于new
应用了 ,我们得到了新创建的对象,我们将其命名为secondAttempt
,并且我们看到secondAttempt.name
确实具有值"Steelix"
。
因此,将使用新创建的 this ,而不是之前指定的this硬绑定。实际上,new
能够覆盖硬绑定。
这种行为的主要原因是创建一个本质上忽略 this硬绑定的函数,并预设部分或全部函数参数。
最后,确定“这个”
我们可以按照优先顺序总结从函数调用的调用站点确定这一点的规则。
它们在这里:
-
函数是否使用 调用
new
?如果是,则这是新构造的对象(新绑定)。例如,var attempt = new catchPokémon("Pidgey");
-
函数是否使用
call
或 调用apply
,即使隐藏在bind
硬绑定中?如果是,则这是显式指定的对象(显式绑定)。例如,var attempt = catchPokémon.call("Pidgeotto");
-
函数调用时是否带有上下文(也称为拥有对象或包含对象)?如果是,
this
则该上下文对象是隐式绑定。例如,var attempt = firstAttempt.catchPokémon("Pidgeot");
-
否则,默认为
global
对象,或undefined
处于strict
模式(默认绑定)。
概括
确定正在执行的函数的 this 绑定需要找到该函数的直接调用站点。
检查后,可以按照优先顺序将四条规则应用于调用站点。
-
用
new
? 调用时使用新构造的对象。 -
用
call
或apply
或bind
?调用使用指定的对象。 -
调用时是否使用了拥有该调用的上下文对象?请使用该上下文对象。
-
默认值:
undefined
在strict
模式下,global
否则为对象。
致谢
-
官方文档:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this
-
你不知道 JS:this 和对象原型,作者 Kyle Simpson。