call 和 apply
EC3給Function的原型定義了兩個方法,它們是 Function.prototype.call 和 Function.prototype.apply涩馆。在實際的開發(fā)中允坚,特別是函數式編程風格的代碼中蛾号,call和apply尤為重要。能熟練的使用這兩個方法模式我們真正成為一名JavaScript程序員的重要一步展运。
call 和 apply 的區(qū)別
它們的作用其實是一模一樣的精刷,區(qū)別僅僅在于傳入的參數形式不同。
apply 接受兩個參數埂软,第一個參數用來制定函數體內this的指向纫事,第二個參數為一個帶下標的集合,這個集合可以為數組丽惶,也可以為類數組,apply方法把這個集合中的元素作為參數傳遞給被調用的函數万哪。
varfn=function(a,b,c){
alert([a,b,c,]);// [1,2,3]
};
fn.apply(null,[1,2,3])
call 傳入的參數數量不固定知纷,第一個參用來制定函數體內的this指向琅轧,從第二個參數開始,每個參數被依次傳入函數體內乍桂。
varfn=function(a,b,c){
alert([1,2,3])
}
當使用 call 或者 apply 時睹酌,如果我們傳入的第一個參數為null,函數體內的this會默認指向宿主對象憋沿,在瀏覽器中,如果使用嚴格模式采章,則還為null。
varfn=function(){
alert(this===window)//true
}
fn.call(null)
varfn2=function(){
"use strict"
alert(this===null)//true
}
fn2.call(null)
call 和 apply 的用途
1.改變this指向担租,直接看代碼
varobj1={
name:"fq"
};
varobj2={
name:"mm"
}
window.name='window';
vargetName=function(){
alert(this.name)
}
getName()// window
getName.call(obj1)//fq
getName.call(obj2)//mm
在實際開發(fā)中抵怎,經常會遇到this指向被不經意改變的場景,比如有一個div節(jié)點尝艘,div節(jié)點的onclick事件中的this本來是指向這個div的承璃。
document.getElementById('div').onclick=function(){
alert(this.id)//div
}
假設該事件函數中有一個內部的函數fn蚌本,在事件內部調用fn函數時,fn函數體內的this就指向了window舷嗡,而不是我們預期的div嵌莉,這個時候我們就可以用call 和 apply去改變this指向了锐峭。
document.getElementById('div').onclick=function(){
alert(this.id)//div
varfn=function(){
alert(this.id)//undefined
};
fn();
};
//之前都是保存一下this,更優(yōu)雅的做法可以這樣
document.getElementById('div').onclick=function(){
alert(this.id)//div
varfn=function(){
alert(this.id)//undefined
};
fn.call(this);
};
案例:內部丟失的this
或許你某天會覺得 document.getElementById函數有點太長了,也去你會這么做:
vargetId=document.getElementById;
getId('div');//但是會報錯...
這是因為document.getElementById內部的this實際上在調用的時候 是需要指向document的,所以我們需要手動修正this
document.getElementById=(function(fn){
returnfunction(){
returnfn.apply(document,arguments);
}
})(document.getElementById)
對于上面的代碼援雇,等式右邊的函數自執(zhí)行的結果為內部的匿名函數椎扬,但是執(zhí)行的時候相當于先把之前的 document.getElementById 保存到fn中了蚕涤,如下:
varfn=document.getElementById;
document.getElementById=function(){
returnfn.apply(document,arguments)//傳進來的實參在arguments中
}
然后當用變量再次存儲document.getElementById的時候這時候實際運行的是上面第二個等式后面的函數揖铜,然后返回的之前存儲的fn運行的結果,但是在函數執(zhí)行的時候贿肩,通過apply修正了this指向document。
2.Function.prototype.bind
大部分高級瀏覽器都實現(xiàn)了內置的Function.prototype.bind方法们何,用來指定內部的this指向控轿,它返回一個修改this之后的函數,但是并不會想apply和
call那樣直接執(zhí)行函數茬射,來看下面的代碼:
varobj={
fn(){
console.log(this);
}
}
setTimeout(obj.fn,1000);//window
setTimeout(obj.fn.bind(obj),1000);//obj
那么咱們看看bind的實現(xiàn)原理是什么
Function.prototype.bind=function(context){
var_this=this;
returnfunction(){
return_this.apply(context,arguments);
}
}
也就是先把 之前的函數的引用保存起來在抛,然后返回一個新的函數,只不過這個函數在執(zhí)行的時候 返回的是保存的引用改變this之后的執(zhí)行結果肠阱。
3.借用其它對象的方法
我們都知道屹徘,杜鵑既不會筑巢衅金,也不會孵雛,而是把自己的蛋寄托給云雀等其他鳥類鉴吹,讓他們代為孵化和養(yǎng)育惩琉。同樣琳水,在JavaScript中也存在類似的借用現(xiàn)象。
借用方法的第一種場景是“借用構造函數”诚啃,通過這種技術始赎,可以實現(xiàn)一些類似繼承的效果:
varA=function(name){
console.log(name)
};
varB=function(){
A.apply(this,agruments);
};
B.prototype.getName=function(){
console.log(this.name)
}
varb=newB('momo');
b.getName();// momo
借用方法的第二種場景跟我們更加密切造垛。
函數的參數列表arguments是一個類數組的對象,雖然它也有“小標”,但它并非正在的數組办斑,所以不能像數組一樣進行排序操作或者往集合里面添加一個新元素乡翅。這種情況下罪郊,我們常常會借用Array.prototype對象上的方法悔橄。比如想往arguments中添加一個新元素癣疟,通常會借用Array.prototype.push;
(function(){
Array.prototype.push.call(arguments,3);
console.log(arguments);// [1, 2, 3]
})(1,2)
在操作arguments的時候我們經常頻繁的去找Array.prototype對象借用方法争舞。
想把arguments轉換成真正的數組的時候澈灼,可以借用Array.prototype.slice方法叁熔,想截取arguments列表中第一個元素的時候荣回,由可以借用Array.prototype.shift方法心软。這些借用其實很常見,沒什么好說的耳贬,那么他們內部實現(xiàn)的機制原理是什么呢咒劲? 不妨咱們翻開v8引擎的源碼來看看吧腐魂!
functionArrayPush(){
varn=TO_UINT32(this.length);//被push對象的length
varm=%_ArgumentsLength();//push的參數個數
for(vari=0;i
this[i+n]=%_Arguments[i];//賦值元素
}
this.length=m+n;
returnthis.length;
}
通過上面這段代碼可以看到蛔屹,Array.prototype.push實際上是一個屬性賦值的過過程,把參數按照下標依次添加到被push的對象上面嫉父,順便修改了這個對象的length屬性绕辖。至于被修改的對象是誰仪际,到底是個數組還是個對象树碱,這個并不重要。
那么改寫成 JavaScript 的代碼 push 應該是這樣的
varUtils={
push(){
varn=arguments[0].length||0,
m=arguments.length-1;
for(vari=0;i
arguments[0][i+n]=arguments[i+1]
}
arguments[0].length=m+n;
returnarguments[0].length;
}
}
varo={};
Utils.push(o,1,2,3);// 3
console.log(o);//Object {0: 1, 1: 2, 2: 3, length: 3}
由此可以推斷我們可以把“任意”的對象傳入Array.prototype.push赎婚。為什么要把“任意”這兩個字加引號呢挣输? 因為這個對象其實還要滿足2各條件:
對象本身可以存儲屬性
對象的length屬性可讀可寫
對于第一個條件,對象本身存取屬性并沒有問題福贞,但是如果借用Array.prototype.push方法的不是一個Object類型數據撩嚼,而是一個number類型的數據呢?我們無法在number身上存取其他數據挖帘,那么從下面的測試代碼可以發(fā)現(xiàn)完丽,一個number類型的數據不可能借用到這個方法:
vara=1;
Array.prototype.push.call(a,'first');
alert(a.length)// undefined
alert(a[0])//undefined
對于第二個條件,函數的length屬性就是只讀的拇舀,表示形參的個數逻族,我們嘗試把一個函數當做this傳入Array.prototype.push:
varfn=function(){};
Array.prototype.push.call(fn,'first');//報錯
alert(fn.length);