this、call和apply的用法(二)

call和apply

ECAMScript3给Function的原型定义了两个方法,分别是Function.prototype.callFunction.prototype.apply。在实际开发中,特别是一些函数式风格的代码编写中,call和apply尤为重要。

1.call和apply的区别
Function.prototype.callFunction.prototype.apply都是非常常用的方法,它们的作用一模一样,区别仅在于传入参数形式的不同。

apply接受两个参数,第一个参数指定了函数体内this对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply方法把这个集合中的元素作为参数传递给被调用的函数。

1
2
3
4
var fun = function(a,b,c){
alert([a,b,c]);//输出1,2,3
};
fun.apply(null,[1,2,3]);

在这段代码中,参数1,2,3被放在数组中一起传入func函数,它们分别对应func参数列表中的a,b,c。

call传入的参数数量不固定,跟apply相同的是,第一个参数也是代表函数体内的this指向,从第二个参数开始往后,每个参数被依次传入函数。

1
2
3
4
var func= function(a,b,c){
alert([a,b,c]); //输出1,2,3
};
func.call(null,1,2,3,4);

当调用一个函数时,javascript的解释器并不会计较形参和实参在数量类型以及顺序上的区别,Javascript的参数在内部就是用一个数组来表示的。从这个意义上说,apply比call的使用率更高,我们不必关心具体有多少参数被传入函数,只要用apply一股脑地推过去就可以了。

如果我们明确地知道函数接受多少个参数,而且想一目了然地表达形参和实参的对应关系,那么也可以用call来传达参数。

当使用call或apply时,如果我们传入的第一个参数为null,函数体内的this会指向默认的宿主对象,在浏览器中则是window。

1
2
3
4
var func = function(a,b,c){
alert(this == window); //输出true
};
func.apply(null,[1,2,3]);

如果是在严格模式下,函数体内的this还是为null

有时候我们使用call和apply的目的不在于指定this指向,而是另有用途,比如借用其他对象的方法,那么我们可以传入null来代替某个具体的对象:

Math.max.apply(null,[1,2,5,3,4]);


2.call和apply的用途

1.改变this指向
来看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj1 = {
name : 'sven'
};
var obj2 = {
name: 'anne'
};
window.name = "Window";
var getName = function(){
alert(this.name);
};
getName();
getName.call(obj1);
getName.call(obj2);

当执行getName.call(obj1)这句代码时,getName函数体内的this就指向obj1对象,所以此处的
1
2
3
var getName = function(){
alert(this.name);
};

实际上相当于
1
2
3
var getName = function(){
alert(obj1.name); //输出sven
};

在实际开发中,经常会遇到this指向被不经意改变的场景,
比如有一个div节点,div节点的onclick事件中的this本来指向这个div。
假如该事件函数中有一个内部函数func,在事件内部调用func函数时,func函数体内的this就指向了window,而不是我们预期的div。
这时候我们就用call来修正func函数内的this,使其依然指向div.

1
2
3
4
5
6
7
8
document.getElementById('div1').onclick = function(){
alert(this.id); //输出div1
var func = function(){
alert(this.id);
};
func(); //输出undefined
func.call(this);//输出div1
};

使用call也可以修正上一节遇到的一个问题,document.getElementById函数内部丢失的this.

1
2
3
4
5
6
7
8
document.getElementById = (function(func){
return function(){
return func.apply(document,arguments);
}
})(document.getElementById);
var getId = document.getElementById;
var div = getId('div1');
alert(div.id);

2.Function.prototype.bind

bind()方法和call、apply很相似,也是可以改变函数体内this的指向。
大部分高级浏览器都实现了内置的Function.prototype.bind,当没有原生的Function.prototype.bind时,我们可以模拟一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
Function.prototype.bind = function(context){
var self = this; //保存原函数
return function(){ //返回一个新的函数
return self.apply(context,arguments); //执行新的函数的时候,会把之前传入的context当做新函数体内的this
}
};
var obj = {
name: 'sven'
};
var func = function(){
alert(this.name); //输出:sven
}.bind(obj);
func();

这段代码我们通过Function.prototype.bind来“包装”func函数,并且传入一个对象context当做参数,这个context对象就是我们想修正的this对象。
在bind内部实现中,我们先把func函数的引用保存起来,然后返回一个新的函数,当我们在将来执行func函数时,实际上先执行的是这个刚刚返回的新函数。
在新函数内部,self.apply(context,arguments)这句代码才是执行原来的func函数,并且指定context对象为func函数体内的this。

上面是一个简化版的,通常我们会把它实现的稍微复杂一些,使得可以往func函数中预先填入一些参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Function.prototype.bind = function(){
var self = this, //保存原函数
context = [].shift.call(arguments), //需要绑定的this上下文
args = [].slice.call(arguments); //剩余的参数转成数组
return function(){ //返回一个新的函数
return self.apply(context,[].concat.call(args, [].slice.call(arguments)));
//执行新的函数的时候,会把之前传入的context当作新函数体内的this
//并且组合两次分别传入的参数,作为新函数的参数
}
};
var obj = {
name : 'sven'
};
var func = function(a,b,c,d){
alert(this.name);
alert([a,b,c,d]);
}.bind(obj,1,2);
func(3,4);

3.借用其他对象的方法
借用方法的第一种场景是”借用构造函数”,通过这种技术,可以实现一些类似继承的效果:

1
2
3
4
5
6
7
8
9
10
11
var A = function(name){
this.name = name;
};
var B = function(){
A.apply(this,arguments);
};
B.prototype.getName = function(){
return this.name;
};
var b = new B('sven');
console.log(b.getName());

借用方法的第二种运用场景跟我们的关系更加密切。
函数的参数列表arguments是一个类数组对象,虽然它也有“下标”,但它并非真正的数组,所以也不能像数组一样,进行排序操作或者往集合里添加一个新的元素。
这种情况下,我们常常借用Array.prototype对象上的方法,比如想往arguments中添加一个新元素,通常会借用Array.prototype.push:

1
2
3
4
(function(){
Array.prototype.push.call(arguments,3);
console.log(arguments); //输出[1,2,3]
})(1,2);

那么这种机制的内部实现原理是什么呢,我们以Array.prototype.push为例,看看V8引擎中的具体实现:
1
2
3
4
5
6
7
8
9
function ArrayPush(){
var n = TO_UINT32(this.length);
var m = %_ArgumentsLength();
for(var i=0; i<m;i++){
this[i +n]=%_Arguments(i);
}
this.length = n+m;
return this.length;
};

通过这段代码可以看到,Array.prototype.push实际上是一个属性复制的过程,把参数按照下标依次添加到被push的对象上面,顺便修改了这个对象的length属性,至于被修改的到底是谁,是数组还是类数组对象,并不重要。
由此可见,我们可以把“任意”对象传入Array.prototype.push:

1
2
3
4
5
var a = {};
Array.prototype.push.call(a,"first");
alert(a.length); //输出1
alert(a[0]); //first

这段代码在绝大部分浏览器都能顺利执行,但由于引擎的内部实现存在差异,在低版本IE中执行,会输出undifined,所以我们必须显式地给对象a设置length属性。

1
2
3
4
5
6
7
var a = {
length: 0
};
Array.prototype.push.call(a,"first");
alert(a.length); //输出1
alert(a[0]); //first

由此我们知道在借用push方法时,要满足两个条件:
1.对象本身要可以存取属性
2.对象的length属性可读写


参考:

《javascript设计模式与开发实践》曾探著
arguments详解