深入理解JavaScript的Apply、Call和Bind方法
函数是 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 方法允许我们设置这个值
单击下面的按钮时,文本字段将使用随机名称填充。
var user = { data:[ {name:"T. Woods", age:37}, {name:"P. Mickelson", age:43} ], clickHandler:function (event) { var randomNum = ((Math.random () * 2 | 0) + 1) - 1; //0 到 1 之间的随机数 // 这一行将从数据数组中随机添加一个人到文本字段 $("input").val (this.data[randomNum].name + " " + this.data[randomNum].age); } } // 为按钮的单击事件分配事件 $("button").click (user.clickHandler);
点击按钮时,你会得到一个错误,因为 clickHandler()方法中的这个元素绑定到按钮 HTML 元素,因为 clickHandler 方法是在这个对象上执行的。
这个问题在 JavaScript 中非常常见,以及像 JavaScript 框架 Backbone.js 和 jQuery 之类的库会自动为我们进行绑定,所以这总是绑定到我们希望绑定到的对象上。
为了解决前面例子中的问题,我们可以使用 bind 方法:
而不是这一行:
$("button").click (user.clickHandler);
我们只需将 clickHandler 方法绑定到 user 对象,如下所示:
$("button").click (user.clickHandler.bind (user));
另一种修复该值的方法是:您可以将匿名回调函数传递给 click()方法,jQuery 将在匿名函数内将其绑定到 button 对象。
因为 ECMAScript 5 引入了 bind 方法,所以它(bind)在 IE < 9 和 Firefox 3.x 中不可用。如果你的目标客户是较老的浏览器,请在代码中包含此绑定实现:
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 && oThis ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments))); }; fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; }; }
让我们继续前面使用的相同示例。如果我们将这个方法(定义它的地方)分配给一个变量,这个值也会绑定到另一个对象。这说明:
// 这个数据变量是一个全局变量 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; // 0 到 1 之间随机数 console.log(this.data[randomNum].name + " " + this.data[randomNum].age); } } // 将用户对象的 showData 方法分配给一个变量 var showDataVar = user.showData; showDataVar(); // Samantha 12(来自全局数据数组,而不是内部数据数组)
当我们执行 showDataVar()函数时,输出到控制台的值来自全局数据数组,而不是用户对象中的数据数组。这是因为 showDataVar()是作为一个全局函数执行的,而在 showDataVar()内部对它的使用被绑定到全局作用域,即浏览器中的窗口对象。
同样,我们可以通过使用 bind 方法具体设置“this”值来解决这个问题:
// 将 showData 方法绑定到用户对象 var showDataVar = user.showData.bind(user); // 现在我们从 user 对象中获取值因为这个关键字绑定到 user 对象 showDataVar(); // P. Mickelson 43
Bind()允许我们借用方法
在 JavaScript 中,我们可以传递函数、返回函数、借用函数等等。bind()方法使借用方法变得超级容易。
下面是一个使用 bind()来借用方法的例子:
// 这里我们有一个 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 个参数:
function greet(gender, age, name) { // 如果是 male, 就用 Mr., 其他适用 Ms. var salutation = gender === "male" ? "Mr. " : "Ms. "; if(age > 25) { return "Hello, " + salutation + name + "."; } else { return "Hey, " + name + "."; } }
我们使用 bind()方法来柯里化(预先设置一个或多个参数)我们的 greet()函数。bind()方法的第一个参数设置了这个值,如前所述:
// 所以我们传递 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 的更复杂用法之前,这里有一个非常快速、具有说示性的示例供初学者使用:
// 用于演示的全局变量 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 回调函数并使用它们.
// 用一些属性和方法定义一个对象 // 稍后我们将把该方法作为回调函数传递给另一个函数 var clientData = { id: 094545, fullName: "Not Set", // setUserName 是 clientData 对象上的一个方法 setUserName: function(firstName, lastName) { // 这引用了该对象中的 fullName 属性 this.fullName = firstName + " " + lastName; } }
function getUserInput(firstName, lastName, callback, callbackObj) { // 使用下面的 Apply 方法将“this”值设置为 callbackObj callback.apply(callbackObj, [firstName, lastName]); }
Apply 方法将这个值设置为 callbackObj。这允许我们显式地使用这个值集执行回调函数,所以传递给回调函数的参数将在 clientData 对象上设置:
// 应用程序方法将使用 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 这样的代码。
让我们创建一个类数组对象,并借用一些数组方法来操作类数组对象。记住,类数组对象是一个真实的对象,它不是一个数组:
// 类数组对象:注意用作键的非负整数 var anArrayLikeObj = { 0: "Martin", 1: 78, 2: 67, 3: ["Letta", "Marieta", "Pauline"], length: 4 };
现在,如果希望在对象上使用任何常用的数组方法,我们可以:
// 快速复制并将结果保存在一个真实的数组中: // 第一个参数设置“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 对象中提取传递给函数的参数。来看一下例子:
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 函数的所有参数的副本。
从这个例子中,我们了解到获取传递给函数的所有参数(作为数组)的一种快速方法是:
// 我们不使用任何参数定义函数,但是可以获得传递给它的所有参数 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 等。
借用其他方法和函数
因为我们是借用,让我们进入和借用我们自己的自定义方法和函数,而不只是从数组和字符串:
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 对象。
您可能想知道,如果我们借用的方法的原始定义发生了更改,将会发生什么。借来的(复制的)方法也会改变吗?还是复制的方法是不引用原始方法的完整副本?让我们用一个简单的例子来回答这些问题:
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()方法,函数将执行数组中的项,就好像我们这样调用函数:
createAccount (arrayOfItems[0], arrayOfItems[1], arrayOfItems[2], arrayOfItems[3]);
这种技术特别用于创建可变量,也称为可变函数。
这些函数接受任意数量的参数,而不是固定数量的参数。函数的特性指定了函数要接受的参数的数量。
max()方法是 JavaScript 中常见的变量函数的一个例子:
// 我们可以用 Math.max () 传递任意数字的参数 console.log(Math.max(23, 11, 34, 56)); // 56
但是如果我们有一个数字数组要传递给 Math.max 呢?我们不能这样做:
var allNumbers = [23, 11, 34, 56]; // 我们不能把数字数组传递给像这样的 Math.max 方法 console.log(Math.max(allNumbers)); // NaN
这就是 apply()方法帮助我们执行可变值函数的地方。因此,我们必须使用 apply()传递数字数组,而不是上面的方法:
var allNumbers = [23, 11, 34, 56]; // 使用 apply()方法,我们可以传递数字数组: console.log(Math.max.apply(null, allNumbers)); // 56
如前所述,apply()的第一个参数设置了“this”值,但是“this”不能在 Math.max ()方法中使用,因此我们传递 null。
下面是我们自己的可变参数函数的一个例子,进一步说明以这种方式使用 apply()方法的概念:
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 开发人员,你可能会经常遇到并使用这些函数,所以一定要充分理解它们。
码云笔记 » 深入理解JavaScript的Apply、Call和Bind方法