我们知道 根据函数的调用方式 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 中的几乎所有东西都是一个对象!你知道吗,你甚至可以将对象建立在其他对象上!这是原型继承背后的主要思想。通过实现它,对象可以接受其他对象的属性。接下来,我们将探索这一切,以及更多知识!