我们在之前的前端基础系列文章中,提到过 call
和 apply
。
其实在平时的工作中,除了在写一些基础类,或者公用库方法的时候会用到它们,其他时候 call
和 apply
的应用场景并不多。
今天,就让我们来探究一下,这两个方法的区别以及一些妙用。最后,还会介绍与之用法相似的 bind
方法。
apply
和 call
简介
在 javascript
中,call
和 apply
都是为了改变某个函数运行时的上下文(context
)而存在的,换句话说,就是为了改变函数体内部 this
的指向。
JavaScript
的一大特点是,函数存在「定义时上下文
」和「运行时上下文
」以及「上下文是可以改变的
」这样的概念。
比如 A
对象有一个方法,而 B
对象因为某种原因,也需要用到同样的方法,那么这时候不用单独为 B
对象扩展一个方法,可以直接借用 A 对象的方法。这样既完成了需求,又减少了内存的占用。
直接来看个例子:
function fruits() {}
fruits.prototype = {
color: "red",
say: function() {
console.log("My color is " + this.color);
}
}
var apple = new fruits();
apple.say(); //My color is red
但是如果我们有一个对象 banana= {color : "yellow"}
,我们不想对它重新定义 say
方法,那么我们可以通过 call
或 apply
用 apple
的 say
方法:
banana = {
color: "yellow"
}
apple.say.call(banana); //My color is yellow
apple.say.apply(banana); //My color is yellow
apply、call 的区别
对于 apply、call 二者而言,作用完全一样,只是接受参数的方式不太一样。例如,有一个函数定义如下:
var func = function(arg1, arg2) {
};
就可以通过如下方式来调用:
func.call(this, arg1, arg2)
func.apply(this, [arg1, arg2])
其中 this
想指定的上下文,可以是任何一个 JavaScript
对象(JavaScript
中一切皆对象),call
需要把参数按顺序传递进去,而 apply
则是把参数放在数组里。
JavaScript
中,某个函数的参数数量是不固定的,因此要说适用条件的话,当你的参数是明确知道数量时用 call
。
而不确定的时候用 apply
,然后把参数 push
进数组传递进去。当参数数量不确定时,函数内部也可以通过 arguments
这个伪数组来遍历所有的参数。
apply、call 的常见用法
- 数组之间追加
在 ES6
的扩展运算符出现之前,我们可以用 Array.prototype.push
来实现
var array1 = [12 , "foo" , {name "Joe"} , -2458];
var array2 = ["Doe" , 555 , 100];
Array.prototype.push.apply(array1, array2);
/* array1 值为 [12 , "foo" , {name "Joe"} , -2458 , "Doe" , 555 , 100] */
ES6 中可以使用 [...array1, ...array2]
实现。
- 获取数组中的最大值和最小值
number 本身没有 max 方法,但是 Math 有,我们可以借助 call
或者 apply
使用其方法。
var numbers = [5, 458 , 120 , -215 ];
var maxInNumbers = Math.max.apply(Math, numbers), //458
maxInNumbers = Math.max.call(Math,5, 458 , 120 , -215); //458
- 验证是否是数组(前提是toString()方法没有被重写过)
function isArray(obj){
return Object.prototype.toString.call(obj) === '[object Array]' ;
}
- 类(伪)数组使用数组方法
Javascript中存在一种名为伪数组的对象结构。比较特别的是 arguments
对象,还有像调用 getElementsByTagName
, document.childNodes
之类的,它们返回 NodeList
对象都属于伪数组。不能调用 Array
下的 push
, pop
等方法。
但是我们能通过 Array.prototype.slice.call
转换为真正的数组的带有 length
属性的对象,这样 domNodes
就可以应用 Array
下的所有方法了。
var domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));
- 继承
function Animal(name){
this.name = name;
this.showName = function(){
console.log(this.name);
}
}
function Cat(name){
Animal.call(this, name);
}
一道面试题
下面就借用一道面试题,让大家更深入理解 apply
和 call
。
定义一个 log 方法,让它可以代理 console.log
方法,并且每个log消息添加一个"(面试题宝典)
"的前缀。
大家可能会想到下面这个方法:
function log(msg) {
console.log(`(面试题宝典)${msg}`);
}
log(1); //1
log(1,2); //1
但是对于传入的参数数量不确定时,这个方法不能将传入的参数全部打印出来。这个时候就可以考虑使用 apply
或者 call
,注意这里传入多少个参数是不确定的,所以使用apply是最好的,方法如下:
function log(){
var args = Array.prototype.slice.call(arguments);
args.unshift('(面试题宝典)');
console.log.apply(console, arguments);
};
log(1); //1
log(1,2); //1 2
bind
我们再来说说 bind
。bind()
方法与 apply
和 call
很相似,也是可以改变函数体内 this
的指向。
MDN的解释是:
bind()
方法创建一个新的函数,在bind()
被调用时,这个新函数的this
被指定为bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
语法:
function.bind(thisArg[, arg1[, arg2[, ...]]])
bind
不是立即调用函数,而是返回一个新的函数。
还有一点,如果 bind
函数的参数列表为空,或者第一个参数是 null
或 undefined
,执行作用域的 this
将被视为新函数的 thisArg
。apply
和 call
则是如果这个函数处于非严格模式下,则指定为 null
或 undefined
时会自动替换为指向全局对象,原始值会被包装。
我们来看一个 bind
的例子:
this.x = 9; // 在浏览器中,this 指向全局的 "window" 对象
var module = {
x: 81,
getX: function() { return this.x; }
};
module.getX(); // 81
var retrieveX = module.getX;
retrieveX();
// 返回 9 - 因为函数是在全局作用域中调用的
// 创建一个新函数,把 'this' 绑定到 module 对象
// 新手可能会将全局变量 x 与 module 的属性 x 混淆
var boundGetX = retrieveX.bind(module);
boundGetX(); // 81
还有个有趣的问题,如果连续 bind()
多次,会有什么效果呢?像这样:
var bar = function(){
console.log(this.x);
}
var foo = {
x:3
}
var sed = {
x:4
}
var func = bar.bind(foo).bind(sed);
func(); //?
var fiv = {
x:5
}
var func = bar.bind(foo).bind(sed).bind(fiv);
func(); //?
答案是,两次都输出 3 ,而非期待中的 4 和 5 。原因是多次 bind() 是无效的。更深层次的原因与 bind()
的实现有关,相当于使用函数在内部包了一个 call / apply
,第二次 bind()
相当于再包住第一次 bind()
,故第二次以后的 bind
是无法生效的。