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調用方式,我們可以總結如下:
- 如果某個函數的函數體中明顯使用了this,那么,就應該傳入一個明確的thisObj對象,並且這個對象應該包含相關的屬性。類似於人家把'榨汁機'借給你用已經不錯了,你不能自己連水果也不准備吧?
- 如果某個函數就是'工具'類型的,那么,在使用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調用方式的差異。
今天的話題就聊到這里,感謝諸位捧場。
