走心大白話 JavaScript 教程(二)巧妙理解 call 和 apply

Terry豆發表於2017-04-27

JS大法好,JS在手,天下我有,信JS,得永生。

這個系列的教程我一開始是寫在github上的,
但是覺得放到掘金來可以讓更多需要的人看到,
就搬到掘金專欄上啦,
如果覺得本教程對你有幫助,請點這裡去github上給我一顆Star~
教程目錄也在github上哈~

本著對技術負責的態度,任何糾正/疑問,儘管提出,我會及時修正/回答。

一定要把每個例子程式碼都拷貝到你的執行環境中邊看結果邊理解,不然學習效果減半,或者沒效果。

下面開始第三篇:

巧妙理解 call 和 apply

想當年我還是個小白的時候,看到call和apply,那都是一臉懵逼啊!
再加上引數內部this,arguments什麼的,虐的我不要不要的,一度產生厭學心理。
的確,這倆方法對初學者不夠友好...

但是!作為半個老鳥,現在看到call啊什麼apply啊什麼的,也就微微一笑了。
想當初茅塞頓開的時候,那心裡叫一個痛快,現在就把開竅的過程分享出來。

1、call和apply的區別

先說一下call和apply的區別,你在完全不懂倆函式是幹嘛的情況下,你只要記住:
call和apply的功能是完全一樣的,只是第二個引數不一樣;
call可以接收無限多個引數,apply只接收倆引數,並且第二個引數只能是argument。
“而它們同樣的第一個引數,就是新的this指向!”
你先不用管引號裡的話說明了什麼,腦子裡默記下這句話就行。
好了,現在,不要多想,往下看。

2、call、apply會改變this指向

我在實際應用中,最常用的就是用call、apply去“借”另一個物件的方法來用,其實是call、apply改變了this指向。複製程式碼

上最簡單的栗子

我寫了個物件obj1,內部三個屬性,兩個數字numA、numB、還有個方法add,可以列印numA和numB之和:複製程式碼
var obj1 ={
    numA:1,
    numB:2,
    add:function(){
         console.log(this.numA + this.numB)
    }
}
obj1.add(); //列印出obj1.numA和obj1.numB的和,即3複製程式碼
現在我寫了個物件obj2,內部有隻兩個屬性數字numA和數字numB,沒有計算器,但也想求和,怎麼辦?
管obj1借啊!怎麼借?call、apply啊!
上程式碼複製程式碼
var obj2 = {
    numA:3,
    numB:4
}
//用call借:
obj1.add.call(obj2); //列印出obj2.numA和obj2.numB的和,即7;
//用apply借:
obj1.add.apply(obj2); //列印出obj2.numA和obj2.numB的和,即7;複製程式碼
有意思吧?明明是obj1的add方法裡出現了this,按照《理解JS中this指向的小技巧》中的思路,
找到的“.”左邊是obj1,說明是obj1呼叫了add,add方法內部的this應該指向obj1啊!為啥算出來的結果都是obj2裡的numA與numB之和呢?
因為用了call和apply啊!不是剛說完嘛,它們改變了this的指向啊,指向誰啊?第一個引數啊!第一個引數是誰啊?obj2啊!
所以你寫obj1.add.call(obj2),add方法內部的this指向就變成了obj2,就列印出了obj2.numA和obj2.numB的和。
就起到了obj2向Obj1“借”了方法add的效果。複製程式碼

帶引數的栗子

這個栗子是物件導向的栗子,對物件導向不夠了解的同學,請儘量讀懂不得不提的原型/原型鏈

我寫了個建構函式Obj1,內部三個屬性,兩個數字numA、numB、還有個方法add,可以列印numA和numB之和:複製程式碼
function Obj1(numA,numB){
    this.numA = numA;
    this.numB = numB;
}
Obj1.prototype.add = function(){
         console.log(this.numA + this.numB)
}
var obj1 = new Obj1(1,2);
obj1.add(); //列印出obj1.numA和obj1.numB的和,即3複製程式碼
現在我寫了個建構函式Obj2,內部有隻兩個屬性數字numA和數字numB,沒有計算器,但也想求和,怎麼辦?
管obj1借啊!怎麼借?call、apply啊!
上程式碼複製程式碼
function Obj2(numA,numB){
    this.numA = numA;
    this.numB = numB;
}
var obj2 = new Obj2(3,4);
//用call向例項obj1借:
obj1.add.call(obj2,3,4); //列印出obj2.numA和obj2.numB的和,即7;
//用apply向例項obj1借:
obj1.add.apply(obj2,[3,4]); //列印出obj2.numA和obj2.numB的和,即7;
//用call向建構函式Obj1借:
Obj1.prototype.add.call(obj2, 3, 4); //列印出obj2.numA和obj2.numB的和,即7;
//用apply向建構函式Obj1借:
Obj1.prototype.add.apply(obj2, [3, 4]); //列印出obj2.numA和obj2.numB的和,即7;複製程式碼
這個栗子恰好說明了帶引數的情況怎麼“借”另一個物件的方法,也把apply和call的不同解釋明白了,就是個傳參不同。
看這個 Obj1.prototype.add.call(obj2, 3, 4) ,眼熟嗎?
像不像 Array.prototype.forEach.call(xxx) ?就是這麼來的,xxx想借用Array.prototype的forEach方法完成遍歷。複製程式碼

3、特殊慄:在第一個引數為this並且this指向window的情況下,apply的應用

比如有個需求,需要做到每次呼叫先前別人寫好的方法時,先在前面執行我們新增的程式碼:
下面的程式碼不一定是最好的實現本需求的程式碼,但可以演示apply的應用。
生動的具體化一下:

先前陳海寫的的程式碼:
    function foo(){
        console.log('我是陳海,我拍床戲去了');
    }
    foo();複製程式碼
現在侯亮平接手的反貪局接管了程式碼,
需求是,不改變陳海寫的程式碼的情況下,在每次呼叫陳海寫的程式碼時先列印一些話。複製程式碼
林華華自告奮勇,用一段程式碼幫侯局長完成了需求:
    function beforeFoo(num){
        console.log('侯亮平知道陳海有床戲,一共'+num+'場');
    }
    var fooOld = foo;
    foo = function(num){
        beforeFoo(num); //這裡將會被陸亦可修改
        fooOld();
    }
    foo(30); //執行一下看看效果複製程式碼
陸亦可覺得這個程式碼複用性太低,每次beforeFoo的引數個數有變化,還要一同修改下面的程式碼,於是改進了一下:
    function beforeFoo(num,text){
        console.log('侯亮平知道陳海有床戲,一共'+num+'場,',text);
    }
    var fooOld = foo;
    foo = function(){
        beforeFoo.apply(this,arguments); //陸亦可修改了這裡
        fooOld();
    }
    foo(30,'醒不過來'); //執行一下看看效果複製程式碼
剎車!陸亦可在她的程式碼裡用到了apply!
我們來分析一下她幹了啥,完成了啥功能:
修改:把beforeFoo(num)改成beforeFoo.apply(this,arguments);
完成功能:beforeFoo可以任意修改引數個數,不必再修改後續程式碼。複製程式碼
是不是挺神奇,我們來分析一下:?
首先來看看beforeFoo.apply(this,arguments)中的this1this出現在新foo的內部;
2、foo的呼叫語句是foo(30'醒不過來'),是全域性直接呼叫,找不到“.”;
根據我上一篇this教程,通過這兩點,不難發現this指向window;

那麼,根據本片文章前面提到過的,apply即“借”,beforeFoo.apply(this,arguments),
也就是this借用了beforeFoo方法,向誰借的?beforeFoo左邊沒有“.”,是全域性呼叫,原來是向window借的!
而剛剛說過,此this指向window,這就好玩了:windowwindow借用了beforeFoo方法!

你說,那不就是beforeFoo直接呼叫嗎,繞一圈幹嘛?別忘了還有arguments引數呢!
這麼繞了一圈,在繞圈呼叫的過程中,JS會解析arguments引數,自動用“,”幫你把引數分開傳入beforeFoo方法,
以後無論你如何修改beforeFoo方法的引數個數,都不用再改剩餘的程式碼了。
陸亦可利用這一點,巧妙的藉助apply完成了程式碼的可用性提高。

PS:ES6新出的擴充符可以完成一樣的效果:before.apply(this,arguments)可以寫成before(...arguments);
請細細品味,發現道理都是想通的,有趣吧。

最後,侯亮平風騷的封裝了程式碼,以後陳海再也不怕不知道自己會演多少場床戲了。複製程式碼

小結

  • 看到call、apply出現,遵循著“借”的思想,再配合“改變this指向”,
  • XX.call(YY),那麼這個“XX”就是被借的方法;
  • YY就是借方法的那個物件,this指向它;
  • XX是誰的?誰呼叫就是誰的,XX左邊沒有“.”,說明是全域性呼叫,那就是window的。
  • 一定要做到“不找出到底是向誰借的就不罷休”。

當你終於找到物主(到底是“借”的誰的方法),接著理清this指向,你也就透徹的明白call和apply了。

PS:
歡迎轉載,需要註明原址。
教程之間緊密聯絡,不懂的地方,請好好看下全系列教程目錄
有沒有你不懂的那個關鍵字在裡面。
如果幫到你,別忘了給我一顆Star~

相關文章