理解Underscore中的restArgs函數

雖然Underscore並無在API手冊中說起到restArgs函數,咱們仍然能夠經過_.restArgs接口使用restArgs函數。若是不去閱讀源碼,咱們很難發現Underscore中還有這樣的一個函數,對於這樣的一個「沒有存在感」的函數,咱們爲何要使用並學習它呢?html

這個函數雖然比較「低調」,可是它在Underscore中的存在感卻一點也不低。在Underscore源碼中,restArgs函數做爲工具函數,參與多個公開API的實現,可謂勞苦功高。從其屢次參與實現公開API能夠看出,這是一個十分重要的函數,爲了方便講解後面的公開API,這裏專門寫一篇文章介紹restArgs工具函數。git

爲何咱們須要restArgs?

在現實中,咱們可能有碰到過一些特殊狀況,好比咱們所寫的函數不肯定有多少個要傳遞的參數,這樣在函數內部實現參數處理時就會比較棘手。es6

好比如今咱們須要構建一個函數,這個函數接受至少兩個參數,第一個是一個數組對象,第二個以後是一些值,咱們的函數就須要把這些值添加到第一個參數的尾部。github

代碼實現:數組

function appendToArray(arr) {
    if(arguments.length < 2)
        throw new Error('funciton a require at least 2 parameters!');
    return arr.concat(Array.prototype.slice.call(arguments, 1));
}
複製代碼

如今咱們須要實現另外一個函數,該函數也是把參數附加到數組中,不過實現了過濾器功能,好比只把大於1的參數附加到數組中。瀏覽器

代碼實現:app

function appendToArrayPlus(arr, filter) {
    if(arguments.length < 3)
        throw new Error('function a require at least 3 parameters!');
    var params = Array.prototype.slice.call(arguments, 2);
    for(var i = 0; i < params.length; i++) {
        if(filter(params[i])) {
            arr.push(params[i]);
        }
    }
    return arr;
}
複製代碼

能夠看出來,咱們在開發這兩個函數時,作了重複的工做,那就是根據多餘參數的開始序號來截斷arguments對象。框架

這樣的作法,在寫一兩個函數時沒有什麼問題,可是在開發框架時,須要寫大量的參數個數不肯定的函數,這就會使得冗餘代碼大量增長,而且屢次直接操做arguments對象的作法並不十分優雅。函數

因此咱們須要一個restArgs這樣的工具函數,給它傳遞一個函數以及一個多餘參數開始索引(startIndex)做爲參數,它會返回一個函數,咱們在調用返回的函數時,開始索引以後的多餘參數會被放入到數組中,而後一併傳遞給restArgs的第一個參數函數調用(做爲最後一個參數)。工具

有人會說,ES6中已經實現了rest params的功能,參考阮老師教程,可是咱們知道一個框架的開發,必須考慮到兼容問題,不少低端瀏覽器並未實現ES6語法。因此在Underscore中,暫時還未使用ES6語法。

Underscore的實現

Underscore實現的源碼(附註釋):

// Similar to ES6's rest param (http://ariya.ofilabs.com/2013/03/es6-and-rest-parameter.html)
// This accumulates the arguments passed into an array, after a given index.

//restArgs用於把func的位於startIndex以後的參數歸類爲一個數組,
//而後返回一個函數把這個數組結合startIndex以前的參數傳遞給func調用。
var restArgs = function (func, startIndex) {
	//function.length表示function定義時,形式參數的個數。
	//注意此處是func.length,即傳入的方法參數的形參個數而不是當前函數的參數個數,須要結合具體傳入的參數來看。
	//當startIndex參數未傳遞時,默認func函數的最後一個參數開始爲多餘參數,會被整合到數組中。
	startIndex = startIndex == null ? func.length - 1 : +startIndex;
	return function () {
		//length表示構造的多餘參數數組的長度,是實際的多餘參數或者0。
		var length = Math.max(arguments.length - startIndex, 0),
			rest = Array(length),
			index = 0;
		//新建了一個rest數組,把位於startIndex索引以後的全部參數放入該數組。
		for (; index < length; index++) {
			rest[index] = arguments[index + startIndex];
		}
		//將多餘參數放入rest數組以後,直接用Function.prototype.call執行函數。
		switch (startIndex) {
			case 0: return func.call(this, rest);
			case 1: return func.call(this, arguments[0], rest);
			case 2: return func.call(this, arguments[0], arguments[1], rest);
		}
		//若是startIndex > 2,那麼使用apply傳遞數組做爲參數的方式執行func。
		//雖然調用方法發生了變化,可是仍是會把rest數組放在傳入的參數數組的最後。
		//這樣作其實與以前的方法無異(switch部分能夠刪除),可是call的效率高於apply。
		
		//args數組用於盛放startIndex以前的非多餘參數。
		var args = Array(startIndex + 1);
		for (index = 0; index < startIndex; index++) {
			args[index] = arguments[index];
		}
		args[startIndex] = rest;
		return func.apply(this, args);
	};
};
複製代碼

能夠注意到兩個重點:

  • 1 startIndex默認爲func函數的形參個數減1,那麼表明的含義就是當咱們調用restArgs函數不傳遞第二個參數時,默認從最後一個形參開始即爲多餘參數。

    好比:

    _.invoke = restArgs(function (obj, path, args) {...});
      _.invoke(obj, path, 1, 2, 3);
    複製代碼

    以上代碼中,若是咱們給_.invoke傳遞這些參數,那麼實際上執行的函數會是:

    function(obj, path, [1, 2, 3]) {
          // ...
      }
    複製代碼

    這樣咱們就能夠在寫函數_.invoke時,很方便的預處理args數組。

  • 2 switch中的內容只是後面func.apply(this, args)的一個特例,不寫switch也徹底能夠實現功能,可是之因此要寫這個switch,是由於Function.prototype.call的效率要高於Function.prototype.apply(具體請參考:Why is call so much faster than apply?)。

結語

學習完這個內部函數以後,再學習其餘API源碼時,就會好理解許多,咱們須要重點注意的一點就是當restArgs只接受一個函數做爲參數時,表示默認從接受的最後一個參數開始(包括最後一個參數)即爲多餘參數。

其他Underscore源碼解讀文章:GitHub

相關文章
相關標籤/搜索