设置自己的this

我们知道 根据函数的调用方式 this 会设成不同的值,Javascript 提供了几种设置 this值的方法:

  • call()
  • apply()
  • bind()

callapply 作为方法在函数上被调用,会因为参数的传入方式不同而有所不同,bind 是返回新函数的方法.

call


call() 是一个直接调用到函数上的方法。我们传递给它一个单一的值,以设置为 this 的值,然后逐个传入该函数的任何参数,用逗号分隔。

请考虑以下函数 multiply(),它只会返回其两个参数的乘积:

function multiply(n1, n2) {
return 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 = {
title: 'To Kill a Mockingbird',
describe: function () {
console.log(`${this.title} is a classic novel`);
}
};

我们可以让 mockingbird 调用自己的 describe() 方法:

mockingbird.describe();

// 'To Kill a Mockingbird is a classic novel'

但是,借助 call(),以下 pride 对象可以使用 mockingbirddescribe() 方法:

const pride = {
title: 'Pride and Prejudice'
};

mockingbird.describe.call(pride);
// 'Pride and Prejudice is a classic novel'

让我们来分析一下当 mockingbird.describe.call(pride); 被执行时,究竟发生了什么!首先,call() 方法被调用到 mockingbird.describe(它指向一个函数)上。然后,this 的值被传递给 call() 方法:pride

由于 mockingbirddescribe() 方法引用了 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) {
return n1 * n2;
}

我们使用了 call(),并逐个传递参数:

multiply.call(window, 3, 4);

// 12

然而,使用 apply(),我们则是将 multiply() 函数的所有参数收集到一个数组中,然后再将这个数组传递给 apply()

太棒了!那么,使用 apply() 来调用对象的方法,又会怎样呢?请回想一下之前的 mockingbirdpride 对象:

const mockingbird = {
title: 'To Kill a Mockingbird',
describe: function () {
console.log(`${this.title} is a classic novel`);
}
};


const pride = {
title: 'Pride and Prejudice'
};

之前,我们使用了 call() 来允许 pride 对象”借用” mockingbirddescribe() 方法:

mockingbird.describe.call(pride);

// 'Pride and Prejudice is a classic novel'

我们可以使用 apply() 来达到相同的结果!

mockingbird.describe.apply(pride);

// 'Pride and Prejudice is a classic novel'

请注意,传递给 call()apply() 的第一个参数是相同的(即绑定 this 值的对象)。由于 describe() 方法不接受任何参数,因此 mockingbird.describe.call(pride); 和 mockingbird.describe.apply(pride); 唯一的区别就是方法!这两种方法都会产生相同的结果。

偏向选择其中一种方法


call()apply() 都会在其传入的第一个参数(即作为 this 值的对象)的作用域内调用一个函数。那么,你什么时候会偏向于选择 call(),或偏向于选择 apply() 呢?

如果你事先并不知道函数所需要的参数个数,那么 call() 的使用可能会受到限制。在这种情况下,apply() 是一个更好的选择,因为它只接受一个参数数组,然后将其解包并传递给函数。请记住,解包可能会略微影响性能,但这种影响并不显著。

小练习

请考虑以下 dave 对象和 sayHello() 函数:

const dave = {
name: 'Dave'
};

function sayHello(message) {
console.log(`${message}, ${this.name}. You're looking well today.`);
}

假设你想将消息 'Hello, Dave. You're looking well today.' 输出到控制台。你应该编写哪个表达式来达到这个目的?

参考答案: sayHello.apply(dave, [‘Hello’]);

请考虑以下 Andrew 和 Richard 对象:

const Andrew = {
name: 'Andrew',
introduce: function () {
console.log(`Hi, my name is ${this.name}!`);
}
};


const Richard = {
name: 'Richard',
introduce: function () {
console.log(`Hello there! I'm ${this.name}.`);
}
};

Richard.introduce.call(Andrew); 被执行时,什么会被记录到控制台?

参考答案: 'Hello there! I'm Andrew.'

请考虑以下代码:

const andrew = {
name: 'Andrew'
};

function introduce(language) {
console.log(`I'm ${this.name} and my favorite programming language is ${language}.`);
}

请编写一个表达式,使用 call() 方法产生以下消息:
'I'm Andrew and my favorite programming language is JavaScript.'

参考答案: introduce.call(andrew,'JavaScript')

回调和 this


当涉及到回调函数时,this 的值有一些潜在的作用域问题,事情会变得比较棘手。接下来,让我们来看看这个问题。

function invokeTwice(cb) {
cb();
cb();
}

const dog = {
age: 5,
growOneYear: function () {
this.age += 1;
}
};

首先,调用 growOneYear() 按预期运作,将 dog 对象的 age 属性的值从 5 更新为 6

dog.growOneYear();
// undefined

dog.age;
// 6

但是,将 dog.growOneYear(一个函数)作为参数传递给 invokeTwice() 则会产生不希望的结果:

invokeTwice(dog.growOneYear);
// undefined

dog.age;
// 6

这是什么原因呢?事实证明,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 () { 
dog.growOneYear();
});

dog.age
// 7

使用这种方式,调用 invokeTwice() 仍然会将 this 的值设置为 window。但是,这对闭包没有影响;在匿名函数中,growOneYear() 方法仍然会被直接调用到 dog 对象上。因此,dog 的 age 属性的值会从 5 更新为 7。

由于这是一种十分常见的模式,因此 JavaScript 提供了另一种比较简洁的方式:bind() 方法。

使用 bind() 来保存 this


call()apply() 类似,bind() 方法也允许用户直接为 this 定义一个值。bind() 也是一个在函数上调用的方法,但不同于 call()apply(),它们都会立即调用函数——bind() 会返回一个新的函数。当被调用时,该函数会将 this 设置为我们赋给它的值。

const dog = {
age: 5,
growOneYear: function () {
this.age += 1;
}
}

function invokeTwice(cb) {
cb();
cb();
}

invokeTwice(dog.growOneYear);
// undefined

因为bind() 会返回一个新的函数,将其保存在新的变量中,称之为 myGrow

const myGrow = dog.growOneYear.bind(dog);
// undefined

invokeTwice(myGrow);
// undefined

dog.age;
// 7

小练习

请考虑以下 drivercar 对象:

const driver = {
name: 'Danica',
displayName: function () {
console.log(`Name: ${this.name}`);
}
};

const car = {
name: 'Fusion'
};

请使用 bind() 编写一个表达式,让我们可以从 driver 中”借用” displayName() 方法,以供 car 对象使用。

参考答案: driver.displayName.bind(car)

小结


JavaScript 提供了三种方法,让我们可以为一个给定的函数设置 this 的值:

  • call() 会调用该函数,逐个传入参数,并用逗号分隔。
  • apply()call() 类似;它会照样调用该函数,但它会将参数作为一个数组传入。
  • bind() 会返回一个新的函数,并将 this 绑定到一个特定对象,让我们可以按照函数的样式来调用它。
    如需进一步研究,我们建议你查看 Kyle Simpson 有关 this 的你不了解 JS 系列,链接已在下方提供。

到目前为止,你已经看到函数、对象和 this 关键字是如何相互关联的。你也看到了 JavaScript 中的几乎所有东西都是一个对象!你知道吗,你甚至可以将对象建立在其他对象上!这是原型继承背后的主要思想。通过实现它,对象可以接受其他对象的属性。接下来,我们将探索这一切,以及更多知识!

延伸


-------------本文结束 感谢您的阅读-------------
0%