关于call、apply和bind方法,网上搜出来的相关内容实在是太多,其中不免有些是纯复制粘贴的,但也有不少诚然写得不错的。我对这上述的三种方法的认识虽没有那么深刻、透彻,但博客毕竟是可以用来分享的,就在这里作一次小小的浅析,权当温习和巩固一下知识。
call
call方法即Function.prototype.call()
,是一个能调用函数并使其具有指定的this值和分别地提供的参数(参数的列表)的方法。
语法
func.call(thisArg[, arg1[, arg2[, ...]]])
参数
thisArg
指在fun函数运行时指定的this值。
arg1, arg2, ...
为指定的参数列表。
返回值
返回结果包括指定的this值和参数。
应用
1. 调用父构造函数,实现继承
在一个子构造函数中,可以通过调用父构造函数的 call 方法来实现继承,类似于Java中的写法。
function Cat(name,skill){
this.name=name;
this.skill=skill;
// someMethods ...
}
function Ragdoll(name,skill){
Cat.call(this,name,skill);
this.species=Ragdoll;
}
var ragdoll=new Ragdoll('Kate','tease');
console.log(ragdoll.name+' can '+ragdoll.skill); // "Kate can tease"
2. 调用匿名函数
这里我举一个比较经典的例子:延时输出一组数。
for(var i=0;i<=5;i++){
(function(j){
setTimeout(function(){
console.log(j);
},j*100);
}).call(this,i);
}
// "0 1 2 3 4 5"
在for循环体中,通过创建一个匿名函数,使用其call方法将this指向外作用域调用栈,实现参数i的调用。
这里还有一个例子可以更加明显地展示call的调用和this指向:
var dogs = [
{species: 'Alaska', name: 'Dodo'},
{species: 'Golden', name: 'Bobo'},
// someOtherData ...
];
for (var i = 0; i < dogs.length; i++) {
(function (i) {
this.outcome = function () {
console.log('#' + i + ' ' + this.species + ': ' + this.name);
}
this.outcome();
}).call(dogs[i], i);
}
// #0 Alaska: Dodo
// #1 Golden: Bobo
3. 调用函数并改变上下文的this
指向
通过将某个值(对象)作为call方法的第一个参数可以使this指向上下文,之后再将参数传进实现引用。用起来比较简单,这不就是call方法需要体现的用处嘛。
function doSomething(work){
this.work=work;
this.next();
}
var todo={
now:function(){
console.log("I'm busy!");
},
next:function(){
console.log("Well, I'm going to do "+this.work)
}
}
doSomething.call(todo,'nothing'); // Well, I'm going to do nothing
p.s. 关于this的更多问题,将在下面进一步说明。
apply
Function.prototype.apply()
是一个能调用函数并使其具有一个指定的this值和一个参数数组(或类似数组的对象)的方法。
注意:call方法的作用和 apply方法类似,它们的区别就在于,在方法体上,call方法接受的是若干个参数的参数列表,而apply方法接受的是一个包含多个参数的参数数组。
语法
func.apply(thisArg, [argsArray])
参数
thisArg
在 fun 函数运行时指定的 this 值。
argsArray
一个数组或者类数组对象,其中的数组元素将作为单独的参数传给func
函数。如果该参数的值为null
或 undefined
,则表示不需要传入任何参数。
应用
传递arguments,创建包裹函数
function foo(a){
console.log(this.a,a);
return this.a+a;
}
var obj={
a:3
// someOtherDara...
}
var bar=function(){
return foo.apply(obj,arguments);
}
var baz=bar(2);
console.log(baz);
区别于call(),传入的参数必须是数组形式([arg1,arg2…]):
function foo(){
console.log(arguments[0]);
}
foo.apply(null,[1]); // 1
foo.apply(null,1); // TypeError: CreateListFromArrayLike called on non-object
调用函数以及改变this指向的一些应用,跟上文的call方法基本一致,唯一不同的,则是参数是传入形式不同,前者为参数数组(,[args...])
,后者则为参数列表(,arg1,arg2,...)
上面亦有提及,这里便不再赘述。
关于this
在这里对上面在call和apply方法中所提及的this指向问题作进一步阐释:
无论是 call()、apply(),还是下面将提到的 bind()(虽然比较特殊,会涉及“硬绑定”问题),需要注意的是,其指定的this值并非一定是该函数执行时真正的this值,这里又再次涉及到上下文(context)的概念。如果这个函数处于非严格模式下,指定为
null
和undefined
的this在调用时会被忽略,会自动指向全局对象(也就是浏览器默认的window对象),同时值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象;若在严格模式(”use strict”)下,this则指向undefined
。
bind
Function.prototype.bind()
是一个可以创建新的函数,当其被调用时可将this设置为提供的值并提供一个给定的参数序列的方法。
注意:bind方法只进行函数绑定,并不会在绑定后立即执行,需要跟call和apply方法区分开来。
语法
func.bind(thisArg[, arg1[, arg2[, ...]]])
参数
thisArg
当绑定函数被调用时,该参数会作为原函数运行时的this
指向。
(注意:当使用new
操作符调用绑定函数时,该参数无效。)
arg1, arg2, ...
当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。
返回值
返回由指定的this
值和初始化参数改造的原函数拷贝。
应用
1. 绑定函数并改变this指向
这个应用跟上面没啥异同,看看代码就知道了。
function foo(a){
console.log(this.a,a);
return this.a+a;
}
var obj={
a:2
// someOtherDara...
}
var bar=foo.bind(obj);
var baz=bar(3);
console.log(baz);
// 2 3
// 5
2. 使用bind()进行柯里化(currying)
在ES6之前,由于没有扩展运算符的简便应用,参数引入的操作变得尤为麻烦,用bind()来进行柯里化操作是个不错的方法。下面举个简单点的例子:
function foo(a,b){
console.log(a,b);
}
var bar=foo.bind(null,2);
bar(3); // currying
// 2 , 3
3. 构造偏函数,使函数具有预设参数
bind()另一个简单的应用是使一个函数拥有预设的初始参数。通过bind方法将参数引入,将其保存在绑定函数内。若该绑定函数在调用时插入有若干参数,则会将这些参数一并传入,插在该函数参数列表的后面,即排在bind所绑定的参数后面。
function foo(){
console.log(Array.prototype.slice.call(arguments));
}
var baz=foo.bind(null,1);
baz(); // [1]
baz(2,3) // [1,2,3]
注意
bind 函数在 ECMA-262 第五版 才被加入,可能无法在所有浏览器上运行。为了避免兼容性问题可能带来的困扰,有一个比较推荐的方法是采用Polyfill。你可以选择在脚本开头加入以下代码,就能使它运作,让不支持的浏览器也能使用 bind() 功能。
Code:
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5
// internal IsCallable 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 () {
fBound.prototype = this instanceof fNOP ? new fNOP() : fBound.prototype
return fToBind.apply(this instanceof fNOP
? this
: oThis || this,
aArgs.concat(Array.prototype.slice.call(arguments)))
}
if( this.prototype ) {
// Function.prototype doesn't have a prototype property
fNOP.prototype = this.prototype
}
return fBound
}
}
例外
关于 call & apply
首先上一段代码:
var a=1;
function foo(){
console.log(this.a);
}
obj1={
a:2
}
obj2={
a:3
}
foo.call(obj1);
foo.call(obj2);
foo.apply(obj1);
foo.apply(obj2);
关于答案,估计你也已经知道了。没错,答案是:2 3 2 3
。
由于call和apply方法都是同种改变this指向的方法,在指定上下文后,this可以指向该对象。这对于无论是call还是apply来说,this的指向都是可以反复更改的。
关于bind
前面已经阐述了call、apply和bind的相关知识。还需指出,这三者当中,call和apply在某种意义上是一样的,只是传入的参数形式不同,而bind是一种函数绑定方法。我们应当把bind和前两者区别对待,由于bind()会涉及到“硬绑定”的问题,故在实际运用当中应当非常谨慎,不然会无故制造出一大堆麻烦。
这里举个简单例子:
var a=1;
function foo(){
console.log(this.a);
}
obj1={
a:2
}
obj2={
a:3
}
var bar=foo.bind(obj1);
bar();
bar.bind(obj2)();
bar.call(obj2);
bar.apply(obj2);
猜猜结果是怎样的?你也许会觉得,答案是:2 3 3 3
。
其实不然。结果却是:2 2 2 2
。为什么会这样呢?
还记得上面说过,bind是个绑定函数的方法,在其绑定某个函数后,this就已经指向了该函数,无论后面再使用call还是apply方法,都无法改变this的指向,故答案为“2 2 2 2”。因此,在使用bind()绑定this之前,call和apply是有效的,this可以随之更改,一旦bind()被执行,this将绑定在对应的目标函数或者对象上,且无法再次做出更改。像这种无法使用其它方法改变其this指向的问题,就是所谓的“硬绑定”问题。与之对应的,回顾上面提到的call和apply方法的应用情形,在一定程度上属于“软绑定”。
有关“函数绑定”以及
this
词法的更多内容,将在后期和大家分享。
关于new
上面提到过,当使用bind方法指定this
时,使用new
操作符调用绑定函数会使该参数无效。
来段代码试试:
var a=1;
function foo(a){
this.a=a;
}
obj1={
a:2
}
obj2={
a:3
}
console.log(obj1.a);
var bar=foo.bind(obj1);
bar(1);
console.log(obj1.a);
var baz=new bar(obj2.a);
console.log(obj1.a);
console.log(baz.a);
再来猜猜答案吧!
答案是:2 1 1 3 。跟你想的一样吗?
由此可见,当使用new关键字时,this被指向了新创建的实例,这也就解释了为何obj1.a没有随new bar()里边的参数而改变,因为创建实例后的this是指向baz的。
关于bind和new共存
有人觉得,如果使用new来改变this指向,跟bind一同使用就显得没有必要了。
其实,bind和new搭配使用并不会有什么大的冲突,其未尝不是一种尚可的方法,或者更多的可以认为是一种使用风格。
相信你也从前面了解了,尽管使用了new去更改this指向,但是原来绑定的目标对象的值并没有随之改变。正因如此,结合两者的特点,从某种程度上我们也可以考虑利用两者搭配使用的方法进行函数柯里化处理。
function foo(s1,s2){
this.str=s1+s2;
}
var bar = foo.bind(null,'abc');
var baz = new bar('def'); // currying
baz.str; // "abcdef"
参考链接:Function.prototype.call()、Function.prototype.apply()、Function.prototype.bind()
参考书籍:《你不知道的JavaScript(上卷)》