Javascript 使用 .call() 和 .apply() 编写更好的代码

2021-09-17 14:55:00 浏览数 (2113)

Javascript函数对象的原型公开了两个有价值的方法,分别是call()和apply()。下面,我们就一起来看看到如何在代码中有效地使用这两种方法。

首先,让我们了解每种方法的作用。

.call()

作用

call()函数用于通过​为其提供的​this的上下文来调用函数。它允许我们通过在特定函数内显式提供用于​this​事件的对象来调用函数。

为了更好地了解为什么存在这种方法,请考虑以下示例:

function sayHello() {
    console.log(`Hello, ${this.name}`);
}

sayHello(); // Hello, undefined

如你所见,​this​函数内部指的是全局作用域。在上面的代码中,​sayHello​函数试图在全局范围内查找名为​name​变量。由于不存在这样的变量,它打印出​undefined​. 如果我们定义了一个在全局范围内调用的​name​变量,该函数将按预期工作,如下所示:

const name = 'archeun';
function sayHello() {
    console.log(`Hello, ${this.name}`);
}

sayHello(); // Hello, archeun
如果我们严格在上面的代码中使用了模式,它实际上会抛出一个运行时错误,因为​this​将是未定义的。

这里的缺点是​sayHello​函数假定​this​变量的范围,我们无法控制它。根据我们执行它的词法范围,该函数的行为会有所不同。这时候​call()​方法派上用场了。如你所知,它允许我们显式注入我们需要用于​this​函数内部变量的对象:

考虑下面的例子:

const name = 'archeun';
function sayHello() {
    console.log(`Hello, ${this.name}`);
}

sayHello(); // Hello, archeun

const visitor = {
    name: 'My lord!'
}

/**
 * The first parameter of the call method is,
 * the object to be used for the `this` context inside the function.
 * So when the `sayHello` function encounters `this.name`, it now knows
 * to refer to the `name` key of the `visitor` object we passed
 * to the `call` method.
 */
sayHello.call(visitor); // Hello, My lord!

/**
 * Here we do not provide the `this` context.
 * This is identical to calling sayHello().
 * The function will assume the global scope for `this`.
 */
sayHello.call(); // Hello, archeun

除了​this​作为方法的第一个参数传递的上下文之外,​call()​还接受被调用函数的参数。在第一个参数之后,我们传递给​call()​方法的所有其他参数都将作为参数传递给被调用函数。

function sayHello(greetingPrefix) {
    console.log(`${greetingPrefix}, ${this.name}`);
}

const visitor = {
    name: 'My lord!'
}

/**
 * Here `Hello` will be passed as the argument to the
 * `greetingPrefix` parameter of the `sayHello` function
 */
sayHello.call(visitor, 'Hello'); // Hello, My lord!

/**
 * Here `Howdy` will be passed as the argument to the
 * `greetingPrefix` parameter of the `sayHello` function
 */
sayHello.call(visitor, 'Howdy'); // Howdy, My lord!

使用方法

1. 可重用的上下文无关函数

我们可以编写一个函数并在不同的​this​上下文中调用它:

function sayHello(greetingPrefix) {
    console.log(`${greetingPrefix}, ${this.name}`);
}

const member = {
    name: 'Well-known member'
}

const guest = {
    name: 'Random guest'
}

/**
 * `sayHello` function will refer to the `member` object
 * whenever it encouneters `this`
 */
sayHello.call(member, 'Hello'); // Hello, Well-known member

/**
 * `sayHello` function will refer to the `guest` object
 * whenever it encouneters `this`
 */
sayHello.call(guest, 'Howdy'); // Howdy, Random guest

如你所见,如果使用得当,这会提高代码的可重用性和可维护性。

2. 构造函数链

我们可以使用​call()​方法来链接通过函数创建的对象的构造函数。使用该函数创建对象时,函数可以采用另一个函数作为其构造函数。如下例所示,​Dog​和​Fish​都调用​Animal​函数来初始化它们的公共属性,即​name​和​noOfLegs​:

function Animal(name, noOfLegs) {
    this.name = name;
    this.noOfLegs = noOfLegs;
}

function Dog(name, noOfLegs) {
    // Reuse Animal function as the Dog constructor
    Animal.call(this, name, noOfLegs);
    this.category = 'mammals';
}

function Fish(name, noOfLegs) {
    // Reuse Animal function as the Fish constructor
    Animal.call(this, name, noOfLegs);
    this.category = 'fish';
}

const tiny = new Dog('Tiny', 4);
const marcus = new Fish('Marcus', 0);

console.log(tiny); // {name: "Tiny", noOfLegs: 4, category: "mammals"}
console.log(marcus); // {name: "Marcus", noOfLegs: 0, category: "fish"}

这也是代码重用的一种变体。这种模式还使我们能够用其他语言编写接近 OOP 原则的代码。

3. 使用对象调用匿名函数

匿名函数继承调用它们的词法作用域。我们可以使用​call()​方法将​this​作用域显式注入匿名函数。考虑下面的例子:

const animals = [
    { type: 'Dog', name: 'Tiny', sound: 'Bow wow' },
    { type: 'Duck', name: 'Marcus', sound: 'Quack' }
];

for (let i = 0; i < animals.length; i++) {
    /**
     * This anonymous function now has access to each animal object
     * through `this`.
     */
    (function (i) {
        this.makeSound = function () {
            console.log(`${this.name} says ${this.sound}!`);
        }
        this.makeSound();
    }).call(animals[i], i);
}

// Tiny says Bow wow!
// Marcus says Quack!

在这里,我们不必实现一个专门的函数来将​makeSound​方法附加到每个动物对象上。这使我们无法编写和命名一次性使用的实用程序函数。

这些是我们可以有效地使用​call()​方法使我们的代码干净、可重用和可维护的几种方法。

.apply()

作用

apply()​在功能方面几乎与​call()​方法相同。唯一的区别是它接受一个类似数组的对象作为它的第二个参数。

/**
 * After `this` context argument
 * `call` accepts a list of individual arguments.
 * Therefore, if `args` is an array, we can use the
 * `ES6` spread operator to pass individual elements
 * as the list of arguments
 */
func.call(context, ...args);

/**
 * After `this` context argument
 * `apply` accepts a single array-like object
 * as its second argument.
 */
func.apply(context, args); 

除了​apply()如何处理被调用方参数外,该功能与​call()​方法相同。但是,由于这种差异,我们可以将其用于不同于​call()​的用例。

使用方法

1. 连接(追加)数组

Array.prototype.push​函数可用于将元素推送到数组的末尾。例如:

const numbers = [1, 2, 3, 4];
numbers.push('a', 'b', 'c'); // push elements on by one

console.log(numbers); // [1, 2, 3, 4, "a", "b", "c"]

如果你想将一个数组的所有元素推送到另一个数组,该怎么办?像下面这样:

const numbers = [1, 2, 3, 4];
const letters = ['a', 'b', 'c'];

numbers.push(letters);

console.log(numbers); // [1, 2, 3, 4, ["a", "b", "c"]]

这并不是我们想要的。它将整个字母数组作为单个元素附加到数字数组。我们本可以使用​concat()​方法,但它将创建数组的副本并返回它。我们也不需要。我们还可以在字母数组上循环并单独推送每个元素。但还有一种更优雅的方式:

const numbers = [1, 2, 3, 4];
const letters = ['a', 'b', 'c'];

numbers.push.apply(numbers, letters);

console.log(numbers); // [1, 2, 3, 4, "a", "b", "c"]

如果我们有特权使用ES6扩展运算符,我们可以通过这样做来实现这一点,

const numbers = [1, 2, 3, 4];
const letters = ['a', 'b', 'c'];

numbers.push(...letters);

console.log(numbers); // [1, 2, 3, 4, "a", "b", "c"]

2.apply()与接受参数列表的内置函数一起使用

对于任何接受参数列表的函数,例如​Math.max​我们可以有效地使用 ​apply​。考虑以下。

如果你想找出一组数字的最小值和最大值,下面是老派的做法:

let min = +Infinity;
let max = -Infinity;
const numbers = [4, 5, 1, 2, 8, 3, 4, 6, 3];

for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] > max) {
    max = numbers[i];
  }
  if (numbers[i] < min) {
    min = numbers[i];
  }
}

console.log(`Min: ${min}, Max: ${max}`); // Min: 1, Max: 8

我们可以​apply()​以更优雅的方式实现相同的效果,如下所示:

const numbers = [4, 5, 1, 2, 8, 3, 4, 6, 3];

min = Math.min.apply(null, numbers);
max = Math.max.apply(null, numbers);

console.log(`Min: ${min}, Max: ${max}`); // Min: 1, Max: 8

与前一种情况相同,如果我们可以使用ES6扩展运算符,我们可以通过执行以下操作来实现相同的效果:

const numbers = [4, 5, 1, 2, 8, 3, 4, 6, 3];

min = Math.min(...numbers);
max = Math.max(...numbers);

console.log(`Min: ${min}, Max: ${max}`); // Min: 1, Max: 8

到现在为止,你可能对​call()​和​apply()​方法的功能和有效用法有了更好的了解。仔细使用这两种方法将有助于编写更好看的可重用代码。