JavaScript的call()、apply()、bind()方法对比总结

目录

前言

  JavaScript中的函数其实就是一种对象。函数对象也有自己的很多方法。其中,call()方法和apply()方法就是两个用来间接调用函数的方法。两个方法都允许显式地指明调用时的上下文环境(即this的值)。也就是说,这给我们提供了一种自由—任何函数可以作为任何对象的方法来进行调用。并且,两个方法也都允许显式地指明调用时需要用到的实参。函数经call()或apply()调用后会立即执行,而经过bind()方法调用后,只是改变了函数绑定的作用域,要想执行函数,还需要对bind()的返回值(一个函数)进行一次调用。bind()方法的实现也是值得探究的内容。


call()方法

参数
  f.call(o, value1, value2, ...)
  f: 想要调用的方法
  o: 想要调用的方法的作用域(也就是在哪个对象上调用该方法)
  value1, value2, …: 调用方法时传入的一系列参数
使用示例
1.简单调用call()方法,作用在当前this对象(浏览器对象)上

2.调用call()方法给自定义对象新增属性


apply()方法

参数
  f.apply(o, [value1, value2, ...])
  f: 想要调用的方法
  o: 想要调用的方法的作用域(也就是在哪个对象上调用该方法)
  [value1, value2, …]: 调用方法时传入的一个参数数组
说明
  给apply()传入的参数数组可以是真正的数组,也可以是类数组对象

类数组对象
它不是真正的数组对象,却有着很多与数组类似的特性。很多数组算法针对类数组对象的优化做的很好,就像针对真正的数组一样。下面是一个简单的类数组对象创建的过程:

使用示例
Math.max()本来是可以接受任意个参数来判断最大值,但如果给它传入一个数组,是不能正确返回最大值的:

如果使用apply()方法,按照参数要求是接收一个数组,那么此时是可以给Math.max()传入一个数组的:


bind()方法

参数
  fun.bind(thisArg[, arg1[, arg2[, ...]]])

  • thisArg
    当绑定函数被调用时,该参数会作为原函数运行时的this指向。当使用new操作符调用绑定函数时,该参数无效。

  • arg1, arg2, …
    当绑定函数被调用时,这些参数加上绑定函数本身的参数会按照顺序作为原函数运行时的参数。

  • 返回值
    返回由指定的this值和初始化参数改造的原函数拷贝

说明
  bind() 函数会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体(在 ECMAScript 5 规范中内置的call属性)。当目标函数被调用时 this 值绑定到 bind() 的第一个参数,该参数不能被重写。绑定函数被调用时,bind() 也接受预设的参数提供给原函数。一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
this.x = 9; 
var module = {
x: 81,
getX: function() { return this.x; }
};

module.getX(); // 返回 81

var retrieveX = module.getX;
retrieveX(); // 返回 9, 在这种情况下,"this"指向全局作用域

// 创建一个新函数,将"this"绑定到module对象
var boundGetX = retrieveX.bind(module);
boundGetX(); // 返回 81


注意事项

1.在ECMAScript5严格模式中,call()和apply()的第一个实参都会变为this的值,哪怕传入的实参是原始值甚至是null或undefined。而在ECMAScript3非严格模式中,传入的null或者undefined则会被全局对象代替,而原始值则会变成相应的包装对象。

2.为什么会存在call()和apply()这两个方法?—为了动态改变函数执行的环境。有时候,我们可能想在某个对象的作用域(上下文环境)A内调用一个方法—也就是要使用这个对象内的属性和值,但是这个对象内可能没有这个方法。此时,如果别处(全局函数或者其他对象的函数)有我们需要的方法F,那么我们就可以使用call()或apply()将需要的方法F和作用域A关联起来,从而实现目的。

3.从示例中可以看出三个方法的区别:
call()和apply()放回会立即执行函数,而bind()只是将调用的函数的作用域进行绑定,如果要真正执行该函数,还要再调用一次:

1
2
var boundGetX = retrieveX.bind(module);
boundGetX(); // 返回 81

也就是说,当希望改变上下文环境之后并非立即执行,而是回调执行的时候,应使用bind()方法。

4.bind()方法的原理是什么?如何实现一个bind方法?
  一句话来说,bind()的作用在于明确指定函数执行的作用域,而不是按照javascript语言本身的机制-函数的作用域由运行时决定而非声明时决定。比如说常见的错误做法是从对象中拿出了某个方法在对象外执行,却误以为该方法还能够使用对象中的相应属性。
  bind()方法的实现涉及到了javascript语言的很多重要特性,分析过程如下:
A. 简单地指定待调用目标函数的作用域:

1
2
3
4
5
6
Function.prototype.bind = function (context) {
var self = this;
return function () {
return self.apply(context, arguments);
}
}

解释
A1. 实现的方法一定是Function原型链上的。
A2. 该方法的参数是context,即指定的执行作用域。
A3. 该方法只是改变了函数的执行作用域,并不立即执行,所以不能直接返回self.apply(context, arguments); 而是要返回一个函数,由该函数返回self.apply(context, arguments);
A4. 返回的function的执行必然在bind这个function之外,由于闭包机制,返回的function的this将会丢失原来的指向调用bind方法的目标函数的引用,因此,采取的办法是,在bind方法内部使用一个变量self保存this,返回的函数在self上进行调用。这样,就避免了this的丢失和改变,self可以始终指向调用bind的目标函数。

B. 上面的实现有一个问题:不能在调用时传入额外的参数,也就是函数柯里化的问题。为此,改写如下:

1
2
3
4
5
6
7
8
9
Function.prototype.bind = function(context){  
var args = Array.prototype.slice.call(arguments, 1),
self = this;
return function(){
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return self.apply(context,finalArgs);
};
};

解释
B1. arguments是一个类数组对象,使用Array.prototype.slice.call(arguments, 1),可以将其转换为真正的数组。而参数“1”的意思是,去掉bind()方法传入的第一个参数,因为第一个参数是指定的作用域,从第二个参数开始才是执行目标函数所需要的额外的参数。这样我们就得到了一个数组args,它存放的是调用bind()方法时传入的各个参数。
B2. 在bind方法内部使用一个遍历self保存this,原因同上。
B3. 返回的function中的arguments和B1中的不同,指的是传入待调用目标函数本身的参数项。这样,将args和innerArgs拼接起来,就既能支持bind传参,也能满足原目标函数的参数传入。

C. 现在已经有了bind()方法的实现。但是,Javascript中函数还可以作为构造函数,那么绑定后的函数如果以这种方式调用时,情况就会有所不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function.prototype.bind = function(context){  
var args = Array.prototype.slice(arguments, 1),
F = function(){},
self = this,
bound = function(){
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return self.apply((this instanceof F ? this : (context || {}), finalArgs);
};

F.prototype = self.prototype;
bound.prototype = new F();
return bound;
};

解释
C1. 与B中情况相比,新增了F = function(){}作为构造函数。js中的构造函数和其他面向对象的语言不太一样,js的构造函数并不是一类特殊的函数,准确地说应该是普通函数的构造调用

构造函数需要使用new关键字进行调用。以这种方式进行调用会经历以下4个阶段:
1.创建一个新对象
2.将构造函数的作用域赋给新对象(即使得构造函数中的this指向这个新对象)
3.执行构造函数中的代码
4.返回新对象

C2.将目标函数自身的参数和执行bind()时候传入的参数拼接在一起后,通过apply方法交由self也就是目标函数进行执行。而具体的作用域的选择与之前有所不同:
(this instanceof F ? this : context)
注意,这里的this与self不同,比如bind的使用方式一般如下:

1
2
var func = foo.bind(obj);
func();

在这种方式下,self指的是调用bind函数的目标函数,即foo;foo.bind(obj);会返回一个函数bound,也就是改变了作用域的foo函数,这里用变量func表示。
(this instanceof F ? this : context)中的this则相对比较复杂:

这实质上是一种polyfill代码,用于旧浏览器的兼容。总的来说,这段代码的作用是判断绑定后的函数是否是通过new关键字调用的(而非直接调用),如果是的话,就会使用新创建的this(即上面描述的new的4个阶段的第2个)代替硬绑定的context。由此也可以看出,new绑定方式的优先级是高于显式绑定的(调用bind()方法进行绑定叫做硬绑定,它是显式绑定的一种)。

C3.通过设置一个中转构造函数F,使绑定后的函数与调用bind()的函数处于同一原型链上,用new操作符调用绑定后的函数,返回的对象也能正常使用instanceof。

D.为了在浏览器中能支持bind()函数,需要对C中实现进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Function.prototype.bind = function (oThis) {  
if (typeof this !== "function") {
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 || window),
aArgs.concat(Array.prototype.slice.call(arguments))
);
};

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

return fBound;
};

解释
D1.与C相比,对bind方法进行增强,判断调用bind方法的对象是不是一个函数,如果不是,抛出错误。这种判断是与ES5内部的实现最为接近的用于IsCallable()函数。
D2.关于this instanceof fNOP && oThis ? this : (oThis || window)这一句,即判断新绑定的函数是否是通过new关键字调用的,如果是,且由于new绑定的优先级高,就要把函数的作用域绑定到new调用所创建的作用域this上,否则的话,说明是直接调用的,那么就继续使用调用bind()函数时候传入的作用域,为了健壮性,如果调用时未传入要绑定的对象或者传入了null,那么就默认绑定到window对象上。