兄台息怒,關於arguments,您的想法和大神是一樣一樣的----閑聊JS中的apply和call


JavaScript提供了apply和call兩種調用方式來確定函數體中this的指向,表現出來的特征就是:對象可以'借用'其他對象的方法。
之前的幾篇博客回顧了一些Web控件的一些開發方法,我們聊了如何實現一個自定義的組合框,也聊了一個相對復雜一點的地址控件的開發,從上一篇開始,開始聊一些JavaScript語言本身的話題,回顧了閉包和原型繼承,今天我們就一起來聊聊apply和call這兩種調用方式的前世今生。
當然,盡管主題在變,但是基於業務場景來剖析理論知識的寫作風格不會變。
我們還是從一個生活中的例子說起:
小明家有水果,也有一台'果汁機',小紅家也有水果,但是沒有果汁機。有一天,小紅也想把水果榨成果汁來喝,這時候,小紅會怎么做呢?當然就是小紅可以"借"小明家的果汁機用一下,用完之后再回去,因為不用時放在自己家里還占地方,下次要用,再去借就是了,因為'互助'是JavaScript社區的美德。
我們看看如何用JavaScript展示這種情況:

var xiaoming = {
    name:'小明',
    fruit:'橙子',
    makeJuice:function(){
        console.log( '正在榨:' + this.name + ' 家的' + this.fruit + '汁!');
    }
}
var xiaohong = {
    name:'小紅',
    fruit:'蘋果'
}
xiaoming.makeJuice( );                 //輸出:正在榨:小明 家的橙子汁!
xiaoming.makeJuice.apply( xiaohong );  //輸出:正在榨:小紅 家的蘋果汁!

apply方法最核心的意義就是這樣,顯然,如果某個函數體當中根本沒有引用this,那是不是也就失去了調用apply的意義?也並非如此,有時候還需要處理傳入的參數。

進一步來看,如果調用的函數需要傳遞參數,那么調用apply時要如何處理呢?我們改進一下上面的例子,假設榨果汁的時候,需要傳入參數:加水的量以及要榨多長時間。
這時候該如何使用apply呢?

var xiaoming = {
    name:'小明',
    fruit:'橙子',
    makeJuice:function( water, time ){
        console.log( '正在榨:' + this.name + ' 家的' + this.fruit + '汁,加水:' + water + ' mL,用時:' + time + ' 分鍾。');
    }
}
var xiaohong = {
    name:'小紅',
    fruit:'蘋果'
}
var task_info = [ 500 , 1 ] ;          //把要傳入的參數放到一個數組里
xiaoming.makeJuice.apply( xiaohong , task_info ) ;  //輸出:正在榨:小紅 家的蘋果汁,加水:500 mL,用時:1 分鍾。

【分析】
在使用apply方式使用一個函數時:

  • 第1個參數為thisObject,調用時采用傳入的thisObject代替函數體中this的指向。
  • 第2個參數傳入一個數組,函數會用數組的值取代"參數列表"。

回到上面的例子,相當於這樣的場景:

小紅:小明,你幫我榨一下蘋果汁吧。
小明:可以啊,你把我家的榨汁機拿去用就可以了。 用之前你得先想清楚'准備加多少水,要榨幾分鍾'
小紅:好的。

小紅把榨汁機拿來之后,就先把'加 500mL 水,榨 1 分鍾'的內容寫到'紙'上,准備好原材料之后,就按'紙'上的信息操作榨汁機,避免手忙腳亂。用來寫任務相關信息的'紙'就相當於在apply方式調用時用來傳遞參數列表信息的數組

說到參數列表,自然就想到了arguments,在調用函數時,函數的運行時環境會自動產生一個變量arguments指向實參列表。很多資料上都會說,arguments是具有數組某些特性的'類數組'(偽數組)。那么,當使用apply方式調用函數時,傳入的第2個參數是否可以是一個像arguments這樣的'類數組'(偽數組)呢?
我們再構造一個場景來驗證一下,最近小區又搬來了一位王奶奶,有一天王奶奶也想喝果汁,她知道小明家有榨汁機,本來想找小明幫忙,但是小明出差了。小明跟王奶奶說,你想好了要多少量的果汁以及想打多長時間,找小紅幫忙就可以了。
現在,我們就來實現這樣的場景,重點在於王奶奶求助小紅的函數。

var wang = {
    name: '王奶奶',
    helpFromXiaohong: function( water , time ){
        //小紅自己沒有榨汁機,還是要使用小明的榨汁機,使用apply方式調用函數
        //王奶奶的要求 與 使用榨汁機時要准備的'任務內容'完全一樣,
        //所以,這里直接傳入arguments看看
        //至於水果嘛,小紅當然不會向王奶奶要了,就用自己家的
        //於是,調用方式如下:
        xiaoming.makeJuice.apply( xiaohong , arguments ) ;
    }
}
wang.helpFromXiaohong( 400 , 2 ) ;   //老人家喝的量不多,但是希望把水果打爛一點

//輸出:正在榨:小紅 家的蘋果汁,加水:400 mL,用時:2 分鍾。

發現和我們預期的內容是完全一樣的,這就意味着,傳入apply中的第二個參數,
也可以是一個'類數組',最常見的當然就是直接將arguments傳入作為第2個參數。
'類數組'的特征:

  • 具有一個length成員,'表示'包含的'元素個數'。
  • 能夠用1,2,3等數字來檢索它的成員。

到現在我們已經對apply調用方式有了一些認識,再回到我們日常的工作當中。我們經常會看到這樣的調用方式:

var w = Array.prototype.shift.apply( arguments );

這行代碼表示什么意思也許大家都很清楚,就是將隱含的'類數組'arguments的第一個參數值取出來,然后賦給變量w。

【思考】

1. 為什么不直接用arguments調用shift函數呢?

因為arguments不是真正的'數組',從JavaScript的語言特征來說,arguments僅僅是具有某些'數組特征'的對象。它不是通過new Array()的方式創建,它的原型鏈也沒有鏈向'Array.prototype',所以不能直接使用shift()函數。
"靠,既然是語言自帶的東東,為什么不直接設計成數組呢?搞得老子每次想用一下數組的相關方法還得拐個彎。"
兄台息怒,其實這樣想的人並不是您一個人,包括JavaScript的大師老道(Douglas Crockfod)也是這么想的,正所謂英雄所見略同。

2. 如何理解var w = Array.prototype.shift.apply( arguments );這一個語句呢?

我們了解到,根據apply的調用模式,它會用傳入的第1個參數代替函數體中的this。從這里來看,就是用arguments這個對象(具有'數組'特征的特殊對象)代替了Array.prototype.shift中的this。

我們知道,如果用一個數組對象去調用shift是沒有問題的。
例如:

console.log( ['A','B','C'].shift() ) ; //輸出: A

因為在['A','B','C'].shift()的調用過程中,沒有傳入任何參數,所以,可以推斷Array.prototype.shift的函數體中,肯定引用了this。通過對this.length 以及 this[0] 這種方式的處理來計算運算結果,

顯然,這個特殊的對象arguments進行arguments.length 以及 arguments[0] 這樣的使用方式是沒有問題的,是能體現出它的'數組特征'的,所以,通過調用Array.prototype.shift.apply( arguments );能夠獲得傳入的第1個參數值。

為了增加'畫面感',我們把它放入前面王奶奶求助的函數中:

var wang = {
    name: '王奶奶',
    helpFromXiaohong: function( water , time ){
        var w = Array.prototype.shift.apply( arguments );
        console.log( '王奶奶想喝 ' + w + ' mL的果汁。' );  
        xiaoming.makeJuice.apply( xiaohong , arguments ) ;
        }
    }
   wang.helpFromXiaohong( 400 , 2 ) ;  

>期望輸出:
王奶奶想喝 400 mL的果汁。
正在榨:小紅 家的蘋果汁,加水:400 mL,用時:2 分鍾。
>實際輸出:
王奶奶想喝 400 mL的果汁。
正在榨:小紅 家的蘋果汁,加水:2 mL,用時:undefined 分鍾。

【分析】

1. 對於我們剛才想驗證的結論,發現我們的假設是正確的。arguments對象成功'借用'了數組的shift函數,所以輸出:王奶奶想喝 400 mL的果汁。
2. 但是,在下面的調用中,居然輸出的是:正在榨:小紅 家的蘋果汁,加水:2 mL,用時:undefined 分鍾。

這個很好理解,shift函數的作用是:'彈出'數組的'第1個元素'並返回。這就意味着,經過var w = Array.prototype.shift.apply( arguments );調用之后,arguments中的內容也發生了變化,arguments[0]的值已經不是400!

這也再一次說明:

  • apply的調用方式,除了替換函數體中的this的指向之外,函數的其他邏輯沒有發生任何變化。'借用'的函數的效果,就跟對象自己擁有這個函數一樣。
  • arguments這個'類似數組',除了不是'原型繼承自'Array.prototype之外,其他的特征和數組也是一樣一樣的。

3. 在剛才的場景下,如果你確實需要在調用xiaoming.makeJuice.apply( xiaohong , arguments ) ;之前顯示一下王奶奶想喝多少mL的果汁,直接調用var w = arguments[0]; 就可以了,何必要'彈'人家呢。

完整的例子如下:

var xiaoming = {
    name:'小明',
    fruit:'橙子',
    makeJuice:function( water, time ){
        console.log( '正在榨:' + this.name + ' 家的' + this.fruit + '汁,加水:' + water + ' mL,用時:' + time + ' 分鍾。');
    }
}
var xiaohong = {
    name:'小紅',
    fruit:'蘋果'
}
var wang = {
    name: '王奶奶',
    helpFromXiaohong: function( water , time ){
        var w = arguments[0];
        console.log( '王奶奶想喝 ' + w + ' mL的果汁。' );  
        xiaoming.makeJuice.apply( xiaohong , arguments ) ;
        }
}
wang.helpFromXiaohong( 400 , 2 ) ;  

最后,我們再補充說明一個特性:如果我們在使用apply調用方式時,第1個參數傳入的是null,但是,被'借用'函數的函數體本身又使用了this,那么,會不會報異常呢?因為直接寫null.name這樣的方式肯定是不行的。這里不賣關子,先把答案擱這里。答案是:不會報異常。
話說過了幾天,王奶奶又想要喝果汁了,於是又給小明打電話,但是小明還在出差啊,於是小明又叫王奶奶去找小紅幫忙,另外,小明也給小紅打了個電話解釋情況。這回接到小明的電話,小紅可有點不樂意了,心想:'小明你是什么人啊?好名聲你來拿,麻煩事我去做。'盡管心里有些不爽,但是畢竟是熱心腸的好女孩,王奶奶的忙還是幫了,不過,這一回,她可就沒有拿自己家的水果去榨了,而是直接用了社區的水果。

回到王奶奶的求助函數,既然不用xiaohong家的水果,就不用傳入xiaohong這個對象了,傳一個null試試看。

xiaoming.makeJuice.apply( xiaohong , arguments ) ;

修改為:

xiaoming.makeJuice.apply( null , arguments ) ;

並且在代碼的開頭增加以下變量:

var name = '社區';
var fruit = '西瓜' ;

這時候,輸出的內容為:
王奶奶想喝 400 mL的果汁。
正在榨:社區 家的西瓜汁,加水:400 mL,用時:2 分鍾。

也就是說,如果第一個傳入的參數是null,那么,在函數體內的this會指向全局對象,在瀏覽器中就是window(在Chrome瀏覽器中是這樣的效果)。很顯然,如果函數體內用到了this,而你在用apply方式調用它時,給它傳一個null過去,這是一種'小紅同學賭氣'的行為,不是一個好的習慣。

所以,對於apply調用方式,我們可以總結如下:

  1. 如果某個函數的函數體中明顯使用了this,那么,就應該傳入一個明確的thisObj對象,並且這個對象應該包含相關的屬性。類似於人家把'榨汁機'借給你用已經不錯了,你不能自己連水果也不准備吧?
  2. 如果某個函數就是'工具'類型的,那么,在使用apply調用方式時,可以傳入一個null作為thisObject。類似於'水果刀',使用時放在跕板上的水果是什么,它就切什么,跟拿着水果刀的人有什么沒有半毛錢關系。

第二種情況的一個常見的使用場景是:函數的接口要求傳入一個'參數列表',但你手頭只有一個數組。
例如:希望你這樣調用Math.max( 2 , 10 , 6 , 1 ); 但是,你的手頭只有[2,10,6,1]這樣的一個數組。
-如果直接調用Math.max([2,10,6,1]); 會輸出NaN。因為Math.max會認為第一個參數[2,10,6,1]根本就不是一個number。
-或者你可以這樣:

var test_array = [2,10,6,1];
Math.max( test_array[0] , test_array[1] , test_array[2] , test_array[3] );

顯然,這種方式是在練習打字,而不是在編程序^_^~~

[參數列表]和[數組],[數組]和[參數列表].......apply調用方式不就可以實現轉換嘛,所以,我們可以這樣操作:

 var test_array = [2,10,6,1];
 console.log(  Math.max.apply( null , test_array ) );  //輸出:10     

輸出了我們期望的結果:10,是的,當時就是這樣。

如果小紅也不用社區的水果,直接用小明家的水果呢?那就不需要用apply方式調用了,直接用如下方式就可以了:

xiaoming.makeJuice( arguments ) ; 

我們再來看一下這時候的王奶奶的求助函數:

var wang = {
name: '王奶奶',
helpFromXiaohong: function( water , time ){
     var w = arguments[0];
     console.log( '王奶奶想喝 ' + w + ' mL的果汁。' ); 
     xiaoming.makeJuice( arguments ) ; 
  }
}

看函數名稱是helpFromXiaohong(從小紅處獲得幫助),實際上卻是xiaoming.makeJuice(小明在提供幫助)。我們隱約感受到了某些'設計模式'的感覺。關於設計模式,我們下次有空再聊。

今天的話題就聊到這里...

什么?還沒有講到call呢!

差點搞忘了,call方式和apply方式的差別主要體現在傳入的形式參數的不一樣,當采用call調用的時候,第1個參數傳入thisObject,第2個參數以及后面的參數組成'實參列表'傳遞給函數。
例如:
用apply方式調用時,我們會這樣寫:

var task_info = [400,2];
xiaoming.makeJuice.apply( xiaohong , task_info ) ;

而用call方式調用時,我們會這樣寫:

xiaoming.makeJuice.call( xiaohong , 400 , 2 ) 

有人說,其實call調用方式就是使用apply方式實現的,類似於給Function.prototype定義了一個成員函數call。JavaScript引擎到底如何實現,我們無從知曉,不過從JavaScript大神老道在他的書中只字未提call方式,可見我們只要理解了apply調用方式,並且知道apply和call在形式參數上的不同,也許就可以了。

形式參數上的差異,我們可以再用一個生活中的例子理解一下:

小明經常去韓國出差,小紅呢,經常委托小明幫忙在韓國帶一些化妝品。
往常在小明出差前,小紅都會把要買的化妝品寫在一個清單里(相當於:apply模式把參數值放到一個數組里),
小明到了韓國的商場之后,只要拿出購物清單對照着買就可以了,相當於應用(apply)一下這個購物清單,這就是apply調用方式。
但是,有一次,小明因為出差時間短,就沒有告訴小紅,快回國到機場的時候,接到了小紅的電話呼叫(call),沒錯,是小紅call過來的。
"死鬼,去韓國出差也不說一聲!是存心躲着本姑奶奶嗎?!" "你聽我解釋,這不時間太短了,我馬上就回國了,現在已經快到釜山機場了。" "現在在機場了也得給我買!你聽好了我要買什么!" "好吧,我現在去給你買,你說一下你要買什么。" 顯然,小明手里頭沒有一個明確的購物清單,時間又那么緊,不可能先整理一張清單出來再去商場購買。 於是,小明就到商場里面,聽着小紅的電話,小紅說一個,小明就把對應商品往購物車里放,這就是call方式。

【總結】

在本文當中,我們解釋了函數的apply調用方式的價值和使用方式,並結合常見的使用場景,解釋了函數體中隱含的arguments對象的一些特征,也解釋了apply調用方式的一些注意事項,最后指出了apply調用方式和call調用方式的差異。
今天的話題就聊到這里,感謝諸位捧場。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM