兄臺息怒,關於arguments,您的想法和大神是同樣同樣的----閒聊JS中的apply和call

JavaScript提供了apply和call兩種調用方式來肯定函數體中this的指向,表現出來的特徵就是:對象能夠'借用'其餘對象的方法。
以前的幾篇博客回顧了一些Web控件的一些開發方法,咱們聊了如何實現一個自定義的組合框,也聊了一個相對複雜一點的地址控件的開發,從上一篇開始,開始聊一些JavaScript語言自己的話題,回顧了閉包和原型繼承,今天咱們就一塊兒來聊聊apply和call這兩種調用方式的前世此生。
固然,儘管主題在變,可是基於業務場景來剖析理論知識的寫做風格不會變。
咱們仍是從一個生活中的例子提及:
小明家有水果,也有一臺'果汁機',小紅家也有水果,可是沒有果汁機。有一天,小紅也想把水果榨成果汁來喝,這時候,小紅會怎麼作呢?固然就是小紅能夠"借"小明家的果汁機用一下,用完以後再回去,由於不用時放在本身家裏還佔地方,下次要用,再去借就是了,由於'互助'是JavaScript社區的美德。
咱們看看如何用JavaScript展現這種狀況:html

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這樣的'類數組'(僞數組)呢?
咱們再構造一個場景來驗證一下,最近小區又搬來了一位王奶奶,有一天王奶奶也想喝果汁,她知道小明家有榨汁機,原本想找小明幫忙,可是小明出差了。小明跟王奶奶說,你想好了要多少許的果汁以及想打多長時間,找小紅幫忙就能夠了。
如今,咱們就來實現這樣的場景,重點在於王奶奶求助小紅的函數。app

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。ui

【思考】

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調用方式的差別。今天的話題就聊到這裏,感謝諸位捧場。

相關文章
相關標籤/搜索