聊聊call、apply和bind

133

我们在之前的前端基础系列文章中,提到过 callapply

其实在平时的工作中,除了在写一些基础类,或者公用库方法的时候会用到它们,其他时候 callapply 的应用场景并不多。

今天,就让我们来探究一下,这两个方法的区别以及一些妙用。最后,还会介绍与之用法相似的 bind 方法。

applycall简介

javascript 中,callapply 都是为了改变某个函数运行时的上下文(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 方法,那么我们可以通过 callapplyapplesay 方法:

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);
}

一道面试题

下面就借用一道面试题,让大家更深入理解 applycall

定义一个 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

我们再来说说 bindbind() 方法与 applycall 很相似,也是可以改变函数体内 this 的指向。

MDN的解释是:

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

语法:

function.bind(thisArg[, arg1[, arg2[, ...]]])

bind 不是立即调用函数,而是返回一个新的函数。

还有一点,如果 bind 函数的参数列表为空,或者第一个参数是 nullundefined,执行作用域的 this 将被视为新函数的 thisArgapplycall 则是如果这个函数处于非严格模式下,则指定为 nullundefined 时会自动替换为指向全局对象,原始值会被包装。

我们来看一个 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 是无法生效的。