我们知道 根据函数的调用方式 this
会设成不同的值,Javascript
提供了几种设置 this
值的方法:
- call()
- apply()
- bind()
call
和apply
作为方法在函数上被调用,会因为参数的传入方式不同而有所不同,bind
是返回新函数的方法.
call
call()
是一个直接调用到函数上的方法。我们传递给它一个单一的值,以设置为 this 的值,然后逐个传入该函数的任何参数,用逗号分隔。
请考虑以下函数 multiply(),它只会返回其两个参数的乘积:
function multiply(n1, n2) { |
让我们在控制台中调用它:
multiply(3, 4); // 12 |
这里没有任何惊喜!但现在,让我们使用 call()
方法来调用同一个函数:
multiply.call(window, 3, 4); // 12 |
我们得到了相同的结果!这是怎么发生的?我们首先直接调用 call()
方法到 multiply()
函数上。请注意,.call(window, 3, 4)
之前的 multiply
后面没有任何括号! call()
将会处理该调用 和 multiply()
函数的参数本身!
这一步完成后,我们传递将设置为 this
的值作为 call()
的第一个参数:window
。最后,我们逐个传递 multiply()
函数的参数,并用逗号分隔。
当 multiply.call(window, 3, 4)
; 执行后,该函数将以 this
的给定值被调用,我们所看到的结果就是 12。在严格模式之外,调用 multiply()
的这两种方式是等价的。
除了调用常规函数之外,我们如何调用附加到对象上的函数(即方法)呢?这是 call()
真正发挥强大功能的地方。使用 call()
来调用方法允许我们从对象中“借用”方法,然后将其用于另一个对象!请看以下对象 mockingbird
:
const mockingbird = { |
我们可以让 mockingbird
调用自己的 describe()
方法:
mockingbird.describe(); |
但是,借助 call()
,以下 pride
对象可以使用 mockingbird
的 describe()
方法:
const pride = { |
让我们来分析一下当 mockingbird.describe.call(pride)
; 被执行时,究竟发生了什么!首先,call()
方法被调用到 mockingbird.describe
(它指向一个函数)上。然后,this
的值被传递给 call()
方法:pride
。
由于 mockingbird
的 describe()
方法引用了 this.title
,我们需要访问 this 所指向的对象的 title
属性。但是,由于我们已经设置了自己的 this
的值,this.title
的值将会从 pride
对象中被访问!结果,mockingbird.describe.call(pride)
; 被执行,我们在控制台中看到 'Pride and Prejudice is a classic novel'
。
如果你打算在传递给它的第一个参数的作用域内调用一个函数,那么 call()
是非常有效的。同样,我们可以利用 apply()
方法达到相同的目的,尽管在参数传入的方式上有所不同。让我们来仔细看看!
apply
就像 call()
一样,apply()
在一个函数上被调用,不仅可以调用该函数,而且还可以为它关联一个特定的 this 值。但是,apply()
并不是逐个传递参数并用逗号分隔,而是将函数的参数放在一个_数组_中。请回想一下之前的 multiply()
函数:
function multiply(n1, n2) { |
我们使用了 call()
,并逐个传递参数:
multiply.call(window, 3, 4); |
然而,使用 apply()
,我们则是将 multiply()
函数的所有参数收集到一个数组中,然后再将这个数组传递给 apply()
:
太棒了!那么,使用 apply()
来调用对象的方法,又会怎样呢?请回想一下之前的 mockingbird
和 pride
对象:
const mockingbird = { |
之前,我们使用了 call()
来允许 pride
对象”借用” mockingbird
的 describe()
方法:
mockingbird.describe.call(pride); |
我们可以使用 apply() 来达到相同的结果!
mockingbird.describe.apply(pride); |
请注意,传递给 call()
和 apply()
的第一个参数是相同的(即绑定 this 值的对象)。由于 describe()
方法不接受任何参数,因此 mockingbird.describe.call(pride)
; 和 mockingbird.describe.apply(pride)
; 唯一的区别就是方法!这两种方法都会产生相同的结果。
偏向选择其中一种方法
call()
和 apply()
都会在其传入的第一个参数(即作为 this 值的对象)的作用域内调用一个函数。那么,你什么时候会偏向于选择 call()
,或偏向于选择 apply()
呢?
如果你事先并不知道函数所需要的参数个数,那么 call()
的使用可能会受到限制。在这种情况下,apply()
是一个更好的选择,因为它只接受一个参数数组,然后将其解包并传递给函数。请记住,解包可能会略微影响性能,但这种影响并不显著。
小练习
请考虑以下 dave 对象和 sayHello() 函数:
const dave = { |
假设你想将消息 'Hello, Dave. You're looking well today.'
输出到控制台。你应该编写哪个表达式来达到这个目的?
参考答案: sayHello.apply(dave, [‘Hello’]);
请考虑以下 Andrew 和 Richard 对象:
const Andrew = { |
当 Richard.introduce.call(Andrew)
; 被执行时,什么会被记录到控制台?
参考答案: 'Hello there! I'm Andrew.'
请考虑以下代码:
const andrew = { |
请编写一个表达式,使用 call() 方法产生以下消息:'I'm Andrew and my favorite programming language is JavaScript.'
参考答案: introduce.call(andrew,'JavaScript')
回调和 this
当涉及到回调函数时,this 的值有一些潜在的作用域问题,事情会变得比较棘手。接下来,让我们来看看这个问题。
function invokeTwice(cb) { |
首先,调用 growOneYear()
按预期运作,将 dog
对象的 age
属性的值从 5
更新为 6
:
dog.growOneYear(); |
但是,将 dog.growOneYear
(一个函数)作为参数传递给 invokeTwice()
则会产生不希望的结果:
invokeTwice(dog.growOneYear); |
这是什么原因呢?事实证明,invokeTwice() 确实会调用 growOneYear,但它是被调用为一个 函数而不是一个方法!让我们回顾一下先前的 this 网格:
Call Style | new |
method | function |
---|---|---|---|
this |
{} | object itself | global object |
Example | new Cat() | bailey.sayName() | introduce() |
如果使用
new
运算符来调用构造函数,this
的值将被设置为新创建的对象,如果在对象上调用方法,this
将被设置为该对象本身,如果简单地调用一个函数,this
将被设置为全局对象:window
使用匿名闭包来保存 this
还记得吗,简单地调用一个普通函数会将 this
的值设置为全局对象(即 window
)。我们如何解决这个问题呢?
解决这个问题的一种方式就是使用一个匿名闭包来遮蔽 dog
对象:
invokeTwice(function () { |
使用这种方式,调用 invokeTwice()
仍然会将 this
的值设置为 window
。但是,这对闭包没有影响;在匿名函数中,growOneYear()
方法仍然会被直接调用到 dog
对象上。因此,dog
的 age 属性的值会从 5 更新为 7。
由于这是一种十分常见的模式,因此 JavaScript 提供了另一种比较简洁的方式:bind()
方法。
使用 bind() 来保存 this
与 call()
和 apply()
类似,bind()
方法也允许用户直接为 this
定义一个值。bind()
也是一个在函数上调用的方法,但不同于 call()
或 apply()
,它们都会立即调用函数——bind()
会返回一个新的函数。当被调用时,该函数会将 this
设置为我们赋给它的值。
const dog = { |
因为bind()
会返回一个新的函数,将其保存在新的变量中,称之为 myGrow
const myGrow = dog.growOneYear.bind(dog); |
小练习
请考虑以下
driver
和car
对象:
const driver = { |
请使用 bind()
编写一个表达式,让我们可以从 driver
中”借用” displayName()
方法,以供 car
对象使用。
参考答案: driver.displayName.bind(car)
小结
JavaScript 提供了三种方法,让我们可以为一个给定的函数设置 this
的值:
call()
会调用该函数,逐个传入参数,并用逗号分隔。apply()
与call()
类似;它会照样调用该函数,但它会将参数作为一个数组传入。bind()
会返回一个新的函数,并将 this 绑定到一个特定对象,让我们可以按照函数的样式来调用它。
如需进一步研究,我们建议你查看 Kyle Simpson 有关 this 的你不了解 JS 系列,链接已在下方提供。
到目前为止,你已经看到函数、对象和 this 关键字是如何相互关联的。你也看到了 JavaScript 中的几乎所有东西都是一个对象!你知道吗,你甚至可以将对象建立在其他对象上!这是原型继承背后的主要思想。通过实现它,对象可以接受其他对象的属性。接下来,我们将探索这一切,以及更多知识!