web前端开发个人技术博客
当前位置: 前端技术 > 深入理解JavaScript的Apply、Call和Bind方法

深入理解JavaScript的Apply、Call和Bind方法

2018-12-10 分类:前端技术 作者:码云 阅读(54)

函数是JavaScript中的对象,如果你已经阅读过其他相关的文章,那么你现在应该知道了。作为对象,函数具有方法,包括强大的Apply、Call和Bind方法。一方面,Apply和Call几乎是相同的,在JavaScript中经常用于借用方法和显式设置这个值。我们也用Apply来表示变量函数;稍后您将了解更多关于此的内容。

另一方面,我们使用Bind在方法和局部套用函数中设置这个值。

我们将讨论在JavaScript中使用这三种方法的每个场景。当ECMAScript 3(在IE 6、7、8和现代浏览器上可用)附带Apply和Call时,ECMAScript 5(仅在现代浏览器上可用)添加了bind方法。这3个函数方法是非常有用的,有时你绝对需要它们中的一个。让我们从bind方法开始。

JavaScript中bind()方法

我们主要使用Bind()方法来显式地调用这个值集的函数。换句话说,bind()允许我们在调用函数或方法时轻松地设置将哪个特定对象绑定到它。

这可能看起来比较简单,但是当您需要将特定对象绑定到函数的这个值时,通常必须显式地设置方法和函数中的这个值。

当我们在方法中使用这个关键字并从接收对象调用该方法时,通常需要进行绑定;在这种情况下,有时这并没有绑定到我们希望绑定到的对象,从而导致应用程序出现错误。如果你没有完全理解前面的句子,不要担心,很快你就会理解透彻。

在查看本节的代码之前,我们应该了解JavaScript中的这个关键字。如果你还没有在JavaScript中理解这一点,请阅读我的文章《如何理解JavaScript中的this》,并掌握它。如果你不能很好地理解这一点,您将很难理解下面讨论的一些概念。事实上,我在本文中讨论的关于设置“this”值的许多概念,我也在《如何理解JavaScript中的this》文章中讨论过。

JavaScript中bind方法允许我们设置这个值

单击下面的按钮时,文本字段将使用随机名称填充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var user = {
    data:[
        {name:"T. Woods", age:37},
        {name:"P. Mickelson", age:43}
    ],
    clickHandler:function (event) {
        var randomNum = ((Math.random () * 2 | 0) + 1) - 1; //01之间的随机数

        // 这一行将从数据数组中随机添加一个人到文本字段
        $ ("input").val (this.data[randomNum].name + " " + this.data[randomNum].age);
    }

}

// 为按钮的单击事件分配事件
$ ("button").click (user.clickHandler);

点击按钮时,你会得到一个错误,因为clickHandler()方法中的这个元素绑定到按钮HTML元素,因为clickHandler方法是在这个对象上执行的。
这个问题在JavaScript中非常常见,以及像JavaScript框架Backbone.js和jQuery之类的库会自动为我们进行绑定,所以这总是绑定到我们希望绑定到的对象上。
为了解决前面例子中的问题,我们可以使用bind方法:
而不是这一行:

1
$ ("button").click (user.clickHandler);

我们只需将clickHandler方法绑定到user对象,如下所示:

1
$ ("button").click (user.clickHandler.bind (user));

另一种修复该值的方法是:您可以将匿名回调函数传递给click()方法,jQuery将在匿名函数内将其绑定到button对象。

因为ECMAScript 5引入了bind方法,所以它(bind)在IE < 9和Firefox 3.x中不可用。如果你的目标客户是较老的浏览器,请在代码中包含此绑定实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if(!Function.prototype.bind) {
    Function.prototype.bind = function(oThis) {
        if(typeof this !== "function") {
            // 最可能的ECMAScript 5内部IsCallable函数
            throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
        }

        var aArgs = Array.prototype.slice.call(arguments, 1),
            fToBind = this,
            fNOP = function() {},
            fBound = function() {
                return fToBind.apply(this instanceof fNOP &amp;&amp; oThis ?
                    this :
                    oThis,
                    aArgs.concat(Array.prototype.slice.call(arguments)));
            };

        fNOP.prototype = this.prototype;
        fBound.prototype = new fNOP();

        return fBound;
    };
}

让我们继续前面使用的相同示例。如果我们将这个方法(定义它的地方)分配给一个变量,这个值也会绑定到另一个对象。这说明:

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
27
28
29
30
31
32
33
34
// 这个数据变量是一个全局变量
var data = [{
        name: "Samantha",
        age: 12
    },
    {
        name: "Alexis",
        age: 14
    }
]

var user = {
    // user内部数据变量
    data: [{
            name: "T. Woods",
            age: 37
        },
        {
            name: "P. Mickelson",
            age: 43
        }
    ],
    showData: function(event) {
        var randomNum = ((Math.random() * 2 | 0) + 1) - 1; // 01之间随机数

        console.log(this.data[randomNum].name + " " + this.data[randomNum].age);
    }

}

// 将用户对象的showData方法分配给一个变量
var showDataVar = user.showData;

showDataVar(); // Samantha 12(来自全局数据数组,而不是内部数据数组)

当我们执行showDataVar()函数时,输出到控制台的值来自全局数据数组,而不是用户对象中的数据数组。这是因为showDataVar()是作为一个全局函数执行的,而在showDataVar()内部对它的使用被绑定到全局作用域,即浏览器中的窗口对象。

同样,我们可以通过使用bind方法具体设置“this”值来解决这个问题:

1
2
3
4
5
// 将showData方法绑定到用户对象
var showDataVar = user.showData.bind(user);

// 现在我们从user对象中获取值因为这个关键字绑定到user对象
showDataVar(); // P. Mickelson 43

Bind()允许我们借用方法

在JavaScript中,我们可以传递函数、返回函数、借用函数等等。bind()方法使借用方法变得超级容易。

下面是一个使用bind()来借用方法的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 这里我们有一个cars对象,它没有将数据打印到控制台的方法
var cars = {
    data: [{
            name: "Honda Accord",
            age: 14
        },
        {
            name: "Tesla Model S",
            age: 2
        }
    ]

}

// 我们可以从上一个示例中定义的用户对象中借用showData()方法。
// 这里我们绑定user。我们刚刚创建的cars对象的showData方法。
cars.showData = user.showData.bind(cars);
cars.showData(); // Honda Accord 14

这个例子的一个问题是,我们在cars对象上添加了一个新方法(showData),我们可能不希望这样做只是为了借用一个方法,因为cars对象可能已经有了属性或方法名showData。我们不想意外地覆盖它。正如我们将在下面的应用和调用讨论中看到的,最好使用应用或调用方法来借用方法。

JavaScript函数绑定允许我们柯里化

函数柯里化,也称为局部函数应用程序,是使用一个函数(接受一个或多个参数)返回一个新函数,其中一些参数已经设置。返回的函数可以访问外部函数的存储参数和变量。我之前写过一篇关于柯里化的文章大家可以看看,[干货] 如何理解函数的柯里化。这听起来比实际情况复杂得多,所以让我们编写代码。

让我们使用bind()方法进行柯里化。首先我们有一个简单的greet()函数,它接受3个参数:

1
2
3
4
5
6
7
8
9
10
function greet(gender, age, name) {
    // 如果是 male, 就用 Mr., 其他适用 Ms.
    var salutation = gender === "male" ? "Mr. " : "Ms. ";

    if(age &gt; 25) {
        return "Hello, " + salutation + name + ".";
    } else {
        return "Hey, " + name + ".";
    }
}

我们使用bind()方法来柯里化(预先设置一个或多个参数)我们的greet()函数。bind()方法的第一个参数设置了这个值,如前所述:

1
2
3
4
5
6
7
8
// 所以我们传递null是因为我们在greet函数中没有使用“this”关键字。
var greetAnAdultMale = greet.bind(null, "male", 45);

greetAnAdultMale("John Hartlove"); // "Hello, Mr. John Hartlove."

var greetAYoungster = greet.bind(null, "", 16);
greetAYoungster("Alex"); // "Hey, Alex."
greetAYoungster("Emma Waterloo"); // "Hey, Emma Waterloo."

当我们使用bind()方法进行局部柯里化时,除了最后一个(最右边的)参数外,greet()函数的所有参数都是预先设置的。因此,当我们调用从greet()函数中提取的新函数时,这是最正确的参数。同样,我将在另一篇博客文章中详细讨论局部柯里化,你将看到如何使用局部柯里化和组合两个函数JavaScript概念轻松创建功能强大的函数。

因此,使用bind()方法,我们可以显式地为调用对象上的方法设置这个值,我们可以借用

复制方法,并将方法赋给要作为函数执行的变量。正如柯里化小贴士中概述的那样

早些时候,你可以使用bind进行局部柯里化。

JavaScript中apply()和call()方法

Apply和Call方法是JavaScript中最常用的两个函数方法,这是有原因的:它们允许我们借用函数并在函数调用中设置这个值。此外,apply函数尤其允许我们使用参数数组执行一个函数,这样当函数执行时,每个参数都被单独传递给函数——这对于可变值函数来说非常好;可变参数函数采用不同数量的参数,而不是像大多数函数那样采用固定数量的参数。

使用Apply或Call设置此值

就像在bind()示例中一样,我们也可以在使用Apply或Call方法调用函数时设置这个值。调用和应用方法中的第一个参数将此值设置为调用函数的对象。

在我们深入了解Apply和Call的更复杂用法之前,这里有一个非常快速、具有说示性的示例供初学者使用:

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
27
28
29
30
31
32
33
34
// 用于演示的全局变量
var avgScore = "global avgScore";

//全局函数
function avg(arrayOfScores) {
    // 把所有的分数加起来,然后返回总分
    var sumOfScores = arrayOfScores.reduce(function(prev, cur, index, array) {
        return prev + cur;
    });

    // 这里的“this”关键字将绑定到全局对象,除非我们使用Call或Apply设置“this”
    this.avgScore = sumOfScores / arrayOfScores.length;
}

var gameController = {
    scores: [20, 34, 55, 46, 77],
    avgScore: null
}

// 如果执行avg函数,则函数内部的“this”绑定到全局窗口对象:
avg(gameController.scores);
// 证明avgScore是在全局窗口对象上设置的
console.log(window.avgScore); // 46.4
console.log(gameController.avgScore); // null

// 重置全局avgScore
avgScore = "global avgScore";

// 要显式设置“this”值,以便“this”绑定到gameController,
// 我们使用call()方法:
avg.call(gameController, gameController.scores);

console.log(window.avgScore); //global avgScore
console.log(gameController.avgScore); // 46.4

注意,call()的第一个参数设置了这个值。在前面的例子中,它被设置为gameController对象。第一个参数之后的其他参数作为参数传递给avg()函数。

在设置这个值时,apply和call方法几乎是相同的,只是将函数参数作为数组传递给apply(),而必须单独列出参数,将它们传递给call()方法。更多信息请见下文。同时,apply()方法还有一个call()方法没有的特性,我们很快就会看到。

在回调函数中使用Call或Apply来设置它

了解JavaScript回调函数并使用它们.

1
2
3
4
5
6
7
8
9
10
11
// 用一些属性和方法定义一个对象
// 稍后我们将把该方法作为回调函数传递给另一个函数
var clientData = {
    id: 094545,
    fullName: "Not Set",
    // setUserName是clientData对象上的一个方法
    setUserName: function(firstName, lastName) {
        // 这引用了该对象中的fullName属性
        this.fullName = firstName + " " + lastName;
    }
}
1
2
3
4
function getUserInput(firstName, lastName, callback, callbackObj) {
    // 使用下面的Apply方法将“this”值设置为callbackObj
    callback.apply(callbackObj, [firstName, lastName]);
}

Apply方法将这个值设置为callbackObj。这允许我们显式地使用这个值集执行回调函数,所以传递给回调函数的参数将在clientData对象上设置:

1
2
3
4
// 应用程序方法将使用clientData对象设置“this”值
getUserInput("Barack", "Obama", clientData.setUserName, clientData);
// clientData上的fullName属性设置正确
console.log(clientData.fullName); // Barack Obama

Apply,Call和 Bind方法都用于在调用方法时设置这个值,它们的方法略有不同,以便在JavaScript代码中使用直接控制和通用性。JavaScript中的这个值与该语言的任何其他部分一样重要,我们有前面提到的3个方法,它们是设置和正确使用这个值的基本方法。

具有Apply和Call的借用函数(必须知道)

JavaScript中apply和call方法最常见的用法可能是借用函数。我们可以使用Apply和Call方法来借用函数,就像使用bind方法一样,但是方式更加通用。

看一下这些例子:

借用数组的方法

数组提供了许多用于迭代和修改数组的有用方法,但不幸的是,对象没有那么多原生方法。尽管如此,由于对象可以以类似数组(称为类数组对象)的方式表示,而且最重要的是,因为所有的数组方法都是通用的(toString和toLocaleString除外),所以我们可以借用数组方法并在类数组的对象上使用它们。

类数组对象是一个键定义为非负整数的对象。最好在具有对象长度的对象上专门添加一个length属性,因为length属性并不存在于数组上的对象上。

我应该注意(为了清晰起见,特别是对于新的JavaScript开发人员),在下面的示例中调用Array时。在prototype中,我们将访问数组对象及其原型(其中定义了用于继承的所有方法)。我们就是从那里借用数组方法的。因此使用了像Array.prototype这样的代码。

让我们创建一个类数组对象,并借用一些数组方法来操作类数组对象。记住,类数组对象是一个真实的对象,它不是一个数组:

1
2
3
4
5
6
7
8
// 类数组对象:注意用作键的非负整数
var anArrayLikeObj = {
    0: "Martin",
    1: 78,
    2: 67,
    3: ["Letta", "Marieta", "Pauline"],
    length: 4
};

现在,如果希望在对象上使用任何常用的数组方法,我们可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 快速复制并将结果保存在一个真实的数组中:
// 第一个参数设置“this”值
var newArray = Array.prototype.slice.call(anArrayLikeObj, 0);

console.log(newArray); // ["Martin", 78, 67, Array[3]]

// 在类数组对象中搜索“Martin”
console.log(Array.prototype.indexOf.call(anArrayLikeObj, "Martin") === -1 ? false : true); // true

// 尝试使用不带call()或apply()的数组方法
console.log(anArrayLikeObj.indexOf("Martin") === -1 ? false : true); // Error: Object has no method 'indexOf'

// 调整对象:
console.log(Array.prototype.reverse.call(anArrayLikeObj));
// {0: Array[3], 1: 67, 2: 78, 3: "Martin", length: 4}

// 我们也可以使用pop
console.log(Array.prototype.pop.call(anArrayLikeObj));
console.log(anArrayLikeObj); // {0: Array[3], 1: 67, 2: 78, length: 3}

// 如何push?
console.log(Array.prototype.push.call(anArrayLikeObj, "Jackie"));
console.log(anArrayLikeObj); // {0: Array[3], 1: 67, 2: 78, 3: "Jackie", length: 4}

当我们将对象设置为类数组的对象并借用数组方法时,我们可以在对象上使用数组方法。所有这些都可以通过call或apply方法实现。

作为所有JavaScript函数属性的arguments对象是一个类数组的对象,因此,call()和apply()方法最常用的用途之一是从arguments对象中提取传递给函数的参数。来看一下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function transitionTo(name) {
    // 因为arguments对象是类数组的对象
    // 我们可以在上面使用slice()数组方法
    // 数字“1”参数表示:返回数组从索引1到末尾的副本。或者干脆跳过第一项

    var args = Array.prototype.slice.call(arguments, 1);

    // 我加了这个位,这样我们可以看到args值
    console.log(args);

    // 我注释掉了最后一行,因为它超出了这个示例
    //doTransition(this, name, this.updateURL, args);
}

// 因为从索引1复制到末尾的slice方法,所以第一个项目“contact”没有返回
transitionTo("contact", "Today", "20"); // ["Today", "20"]

args变量是一个真实的数组。它具有传递给transitionTo函数的所有参数的副本。

从这个例子中,我们了解到获取传递给函数的所有参数(作为数组)的一种快速方法是:

1
2
3
4
5
6
7
// 我们不使用任何参数定义函数,但是可以获得传递给它的所有参数
function doSomething() {
    var args = Array.prototype.slice.call(arguments);
    console.log(args);
}

doSomething("Water", "Salt", "Glue"); // ["Water", "Salt", "Glue"]

我们将再次讨论如何将带参数类数组对象的apply方法用于可变值函数。稍后将对此进行更多介绍。

使用带Apply和Call的字符串方法

与前面的示例一样,我们还可以使用apply()和call()来借用字符串方法。由于字符串是不可变的,只有非操作数组才能处理它们,因此不能使用reverse、pop等。

借用其他方法和函数

因为我们是借用,让我们进入和借用我们自己的自定义方法和函数,而不只是从数组和字符串:

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
27
28
29
30
31
32
33
34
35
var gameController = {
    scores: [20, 34, 55, 46, 77],
    avgScore: null,
    players: [{
            name: "Tommy",
            playerID: 987,
            age: 23
        },
        {
            name: "Pau",
            playerID: 87,
            age: 33
        }
    ]
}

var appController = {
    scores: [900, 845, 809, 950],
    avgScore: null,
    avg: function() {

        var sumOfScores = this.scores.reduce(function(prev, cur, index, array) {
            return prev + cur;
        });

        this.avgScore = sumOfScores / this.scores.length;
    }
}

// 注意,我们正在使用apply()方法,所以第二个参数必须是一个数组
appController.avg.apply(gameController);
console.log(gameController.avgScore); // 46.4

// appController.avgScore仍然为空;它没有更新,只有gameController.avgScore更新
console.log(appController.avgScore); // null

当然,借用我们自己的自定义方法和函数也同样容易,甚至值得推荐。gameController对象借用appController对象的avg()方法。在avg()方法中定义的“this”值将被设置为第一个参数——gameController对象。

您可能想知道,如果我们借用的方法的原始定义发生了更改,将会发生什么。借来的(复制的)方法也会改变吗?还是复制的方法是不引用原始方法的完整副本?让我们用一个简单的例子来回答这些问题:

1
2
3
4
5
6
appController.maxNum = function() {
    this.avgScore = Math.max.apply(null, this.scores);
}

appController.maxNum.apply(gameController, gameController.scores);
console.log(gameController.avgScore); // 77

正如预期的那样,如果我们更改了原始方法,这些更改将反映在该方法的借来的实例中。这样做是有充分理由的:我们从来没有完全复制这个方法,我们只是借用了它(直接引用它的当前实现)。

使用Apply()执行可变函数

结束对Apply、Call和Bind方法的通用性和实用性的讨论,我们将讨论Apply方法的一个简洁的小特性:使用参数数组执行函数。

我们可以将带有参数的数组传递给函数,并且通过使用apply()方法,函数将执行数组中的项,就好像我们这样调用函数:

1
createAccount (arrayOfItems[0], arrayOfItems[1], arrayOfItems[2], arrayOfItems[3]);

这种技术特别用于创建可变量,也称为可变函数。

这些函数接受任意数量的参数,而不是固定数量的参数。函数的特性指定了函数要接受的参数的数量。

max()方法是JavaScript中常见的变量函数的一个例子:

1
2
// 我们可以用Math.max () 传递任意数字的参数
console.log(Math.max(23, 11, 34, 56)); // 56

但是如果我们有一个数字数组要传递给Math.max呢?我们不能这样做:

1
2
3
var allNumbers = [23, 11, 34, 56];
// 我们不能把数字数组传递给像这样的Math.max方法
console.log(Math.max(allNumbers)); // NaN

这就是apply()方法帮助我们执行可变值函数的地方。因此,我们必须使用apply()传递数字数组,而不是上面的方法:

1
2
3
var allNumbers = [23, 11, 34, 56];
// 使用apply()方法,我们可以传递数字数组:
console.log(Math.max.apply(null, allNumbers)); // 56

如前所述,apply()的第一个参数设置了“this”值,但是“this”不能在Math.max ()方法中使用,因此我们传递null。

下面是我们自己的可变参数函数的一个例子,进一步说明以这种方式使用apply()方法的概念:

1
2
3
4
5
6
7
8
9
10
11
12
var students = ["Peter Alexander", "Michael Woodruff", "Judy Archer", "Malcolm Khan"];

// 没有定义特定的参数,因为可以接受任意数量的参数
function welcomeStudents() {
    var args = Array.prototype.slice.call(arguments);

    var lastItem = args.pop();
    console.log("Welcome " + args.join(", ") + ", and " + lastItem + ".");
}

welcomeStudents.apply(null, students);
// Welcome Peter Alexander, Michael Woodruff, Judy Archer, and Malcolm Khan.

结束语

call、apply和bind方法确实很实用,应该是JavaScript库的一部分,用于在函数中设置这个值,用于创建和执行可变函数,以及用于借用方法和函数。作为一个JavaScript开发人员,你可能会经常遇到并使用这些函数,所以一定要充分理解它们。

「本文为原创文章,版权归码云笔记所有,欢迎分享本文,转载请保留出处!」

赞(2) 打赏

觉得文章有用就打赏一下文章作者

支付宝
微信
2

觉得文章有用就打赏一下文章作者

支付宝
微信

上一篇:

下一篇:

你可能感兴趣

共有 0 条评论 - 深入理解JavaScript的Apply、Call和Bind方法

博客简介

码云笔记: mybj123.com,一个关注Web前端开发技术的博客,主要记录和总结前端工作中常用的知识及我的生活。
更多博客详情请看关于博客

精彩评论