Javascript call()、apply()、bind() 方法及其 polyfill

在本文中,我将解释 callapplybind 方法以及如何编写它们的 polyfill。这三个 polyfillJavaScript 面试中非常常见的问题。

让我们从一个例子开始,了解这些方法的必要性,然后我们将讨论它们的实现。

看看下面的代码,我们有一个 person 对象和一个 printName 方法。

1
2
3
4
5
6
7
8
let person = {
firstName: "William",
lastName: "wang",
};

let printName = function (country) {
console.log(this.firstName + " " + this.lastName + " from " + country);
};

如你所见,person 对象是非常通用的对象,我们可以拥有多个具有不同值的相同类型对象。在这里,我在 printName 方法中使用了这个关键字。如果你不熟悉它。别担心。我们稍后会介绍它。

现在,我们要为每个 person 对象执行一个 printName 方法。

我们该怎么做?

第一个选项是,我们将 printName 方法添加到每个对象并调用它,如下所示。

1
2
3
4
5
6
7
8
9
10
11
let person = {
firstName: "William",
lastName: "wang",
printName: function (country) {
console.log(this.firstName + " " + this.lastName + " from " + country);
},
};

person.printName("China");

// Output: "William wang from China";

如果你看到上面的代码,你会发现我们为每个对象重复了 printName 方法。这似乎不是一个好的做法。这就是我在第一个代码块中将 printName 方法定义为单独方法的原因。

现在,怎么办?

Javascript 为我们提供了三种方法来处理这种情况,而无需重复代码。

  1. call(object,arguments) - 如果有传递的对象以及传递的参数,则调用该函数
  2. apply(object,[arguments]) - 如果有传递的对象以及传递的参数数组,则调用该函数
  3. bind(object,arguments) - 返回一个引用传递的对象和参数的新函数

让我们首先从方法开始。

call 方法

call 方法通过将要执行该方法的对象作为第一个参数来调用函数,并接受可在该方法中传递的参数,例如 printName 方法中的 country。

它等于 person.printName(“China”)。在 printName 方法中,此关键字指的是 person 对象。因为这总是指调用该方法的 . 的左侧。你可以查看此链接以了解有关此关键字的更多信息。

1
2
3
4
5
6
7
8
9
10
11
12
let person = {
firstName: "William",
lastName: "wang",
};

let printName = function (country) {
console.log(this.firstName + " " + this.lastName + " from " + country);
};

printName.call(person, "China");

// Output: "William wang from China"

在这里,我们将调用方法附加到 printName 函数,其中 printName 方法被 person 对象调用,因此它从中获取 firstName 和 lastName 的值,并将“China”作为 country 参数的参数,并产生上述输出。

apply 方法

apply 方法与 call 方法非常相似,但唯一的区别是 call 方法将参数作为逗号分隔的值,而 apply 方法则采用参数数组。如果我们编写上述代码的 apply 方法实现。它将是这样的。

1
2
3
4
5
6
7
8
9
10
11
12
let person = {
firstName: "William",
lastName: "wang",
};

let printName = function (country) {
console.log(this.firstName + " " + this.lastName + " from " + country);
};

printName.apply(person, ["China"]);

// Output: "William wang from China"

在这里,我们用 apply 方法替换了 call 并将参数传递到数组中。

让我们转到最后一种方法。

bind 方法

bind 方法与 call 方法类似,但唯一的区别是 call 方法调用函数,而 bind 方法返回一个可以稍后调用的新函数。让我们实现 bind 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
let person = {
firstName: "William",
lastName: "wang",
};

let printName = function (country) {
console.log(this.firstName + " " + this.lastName + " from " + country);
};

let newPrintName = printName.bind(person, "China");
newPrintName();

// Output: "William wang from China"

如果你看到上面的代码,我们已将 bind 方法附加到 printName 并将其存储到名为 newPrintName 的新变量中。

现在,我们可以在代码中随时调用 newPrintName,它将产生相同的输出。

polyfill 实现

现在,让我们为所有三种方法编写 polyfill。在多姿多彩的 JavaScript 世界,polyfill 如同一座架在浏览器兼容性鸿沟之上的桥梁,让新生的 Web 特性得以在陈旧浏览器中生根发芽。

我将使用 Javascript 原型继承来编写 pollyfill。使 pollyfill 可用于所有函数。

如果你查看所有三种方法。它们应用于函数,因此我们将我们的 polyfill 方法添加到函数原型。

你可以在此处阅读有关原型的信息。

让我们从调用方法 polyfill 开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let person = {
firstName: "William",
lastName: "wang",
};

let printName = function (country) {
console.log(this.firstName + " " + this.lastName + " from " + country);
};

Function.prototype.mycall = function (obj, ...args) {
let sym = Symbol();
obj[sym] = this;
let res = obj[sym](...args);
delete obj[sym];
return res;
};

/*
Note: Applying mycall method to printName function so this
will be equal to printName inside mycall function as
printName is on the left side of the '.'
*/

printName.mycall(person, "China");

// Output: "William wang from China"

在这里,我使用 Function.prototype 使 mycall 方法可用于所有函数并为其分配一个新函数。

与调用方法相同,mycall 方法将要在其上调用该方法的对象作为第一个参数,然后将其余参数传递给函数。

现在,让我们了解该函数的内部实现。

在 mycall 函数内部,我们创建了一个符号 sym,以在传递的对象上创建一个唯一属性,以防止现有属性被覆盖。

现在,在传递的对象上添加 sym 属性并为其分配 this 关键字。它引用 printName 函数。

在下一行中,我们通过传递剩余的参数来调用该函数并将其响应存储在一个新变量 res 中。此后,我们从传递的对象中删除新创建的属性 sym,因为它不存在于该函数之外的对象上,然后我们返回对象的响应。

所以,我们最终创建了第一个 polyfill,结果相同。

让我们跳到应用方法 polyfill

正如我们所见,applycall 方法非常相似,只是它接受的是参数数组,而不是以逗号分隔的参数列表。因此,apply 的实现与 call 方法相同,只有细微的差别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let person = {
firstName: "William",
lastName: "wang",
};

let printName = function (country) {
console.log(this.firstName + " " + this.lastName + " from " + country);
};

Function.prototype.myapply = function (obj, ...args) {
let sym = Symbol();
obj[sym] = this;
let res = obj[sym](...args[0]);
delete obj[sym];
return res;
};

printName.myapply(person, ["China"]);

// Output: "William wang from China"

如果你看到上面的代码,则实现步骤是相同的 ​​,但是当我们在对象上调用函数而不是直接将 ...args 作为参数传递时,我们将使用 rest 运算符传递 args 的第 0 个索引,因为 rest 运算符 ‘…’ 表示参数数组,并且在此数组中,我们在第 0 个索引处传递了参数数组,因此将选择该数组并将其扩展到函数中。

让我们编写最终的 bind 方法 polyfill

如果我们回想一下 bind 方法的实现。我们知道它与调用相同,但不是调用函数而是返回新函数。让我们看看实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let person = {
firstName: "William",
lastName: "wang",
};

let printName = function (country) {
console.log(this.firstName + " " + this.lastName + " from " + country);
};

Function.prototype.mybind = function (object, ...args) {
let func = this;
return function (...args1) {
return func.apply(object, [...args, ...args1]);
};
};

let newPrintName = printName.mybind(person, "China");
newPrintName();

// Output: "William wang from China"

这里,与 mycall 和 myapply 方法相同。我们在 Function.prototype 上创建了一个 mybind 方法并为其分配了一个函数。此函数接受与 bind 方法类似的对象和参数。我们已经从上面的 polyfill 实现中知道 this 关键字引用该函数。在 bind 的情况下,我们将 this 存储在名为 func 的变量中。因为 bind 返回一个新函数。我们还将返回一个匿名函数,它将充当 mybind 函数中的闭包。现在,这个返回函数还可以接受参数,这些参数将表示在调用 mybind 方法返回的新函数期间传递的参数。在这个返回函数中,我们将对 func 变量使用 apply 方法,以便为传递的对象和参数调用它。在这个 apply 方法中,我们将创建一个数组来传递参数,在这个数组中,我们将展开 args 和 args1 以将所有参数传递给函数并将其存储在新变量 newPrintName 中。

稍后,当我们调用这个 newPrintName 时。结果相同。如果我们在 newPrintName 函数中传递任何参数,args1 将在 mybind 方法中表示这些参数。

这就是有关 callapplybind 及其 polyfill 的全部内容。

请分享你对本文的评论和反馈。

祝你学习愉快!