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

目录
文章目录隐藏
  1. JavaScript 中 bind()方法
  2. JavaScript 中 bind 方法允许我们设置这个值
  3. Bind()允许我们借用方法
  4. JavaScript 函数绑定允许我们柯里化
  5. JavaScript 中 apply()和 call()方法
  6. 在回调函数中使用 Call 或 Apply 来设置它
  7. 具有 Apply 和 Call 的借用函数(必须知道)
  8. 使用 Apply()执行可变函数
  9. 结束语

函数是 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 开发人员,你可能会经常遇到并使用这些函数,所以一定要充分理解它们。

「点点赞赏,手留余香」

3

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
码云笔记 » 深入理解JavaScript的Apply、Call和Bind方法

发表回复