理解Underscore的設計架構

在一個多月的畢業設計以後,我再次開始了Underscore的源碼閱讀學習,斷斷續續也寫了好些篇文章了,基本把一些比較重要的或者我的認爲有養分的函數都解讀了一遍,因此如今學習一下Underscore的總體架構。我相信不少程序員都會有一個夢想,那就是能夠寫一個本身的模塊或者工具庫,那麼咱們如今就來學習一下若是咱們要寫一個本身的Underscore,咱們該怎麼寫?javascript

大體的閱讀了一下Underscore源碼,能夠發現其基本架構以下:css

1 定義變量

在ES6以前,JavaScript開發者是沒法經過let、const關鍵字模擬塊做用域的,只有函數內部的變量會被認爲是私有變量,在外部沒法訪問,因此大部分框架或者工具庫的模式都是在當即執行函數裏面定義一系列的變量,完成框架或者工具庫的構建,這樣作的好處就是代碼不會污染全局做用域。Underscore也不例外,它也使用了經典的當即執行函數的模式:html

(function() {
    // ...
}())

此外,Underscore採用了經典的構造器模式,這使得用戶能夠經過_(obj).function()的方式使用Underscore的接口,由於任意建立的Underscore對象都具備原型上的全部方法。那麼代碼形式以下:java

(function() {
    var _ = function() {
        // ...
    };
}())

_是一個函數,可是在JavaScript中,函數也是一個對象,因此咱們能夠給_添加一系列屬性,即Underscore中的一系列公開的接口,以即可以經過_.function()的形式調用這些接口。代碼形式以下:node

(function() {
    var _ = function() {
        // ...
    };
    _.each = function() {
        // ...
    };
    // ...
}())

_變量能夠當作構造器構造一個Underscore對象,這個對象是標準化的,它具備規定的屬性,好比:_chain_wrapped以及全部Underscore的接口方法。Underscore把須要處理的參數傳遞給_構造函數,構造函數會把這個值賦給所構造對象的_wrapped屬性,這樣作的好處就是在以後以_(obj).function()形式調用接口時,能夠直接到_wrapped屬性中尋找要處理的值。這就使得在定義_構造函數的時候,須要對傳入的參數進行包裹,此外還要防止多層包裹,以及爲了防止增長new操做符,須要在內部進行對象構建,代碼形式以下:git

(function() {
    var _ = function(obj) {
        // 防止重複包裹的處理,若是obj已是_的實例,那麼直接返回obj。
        if(obj instanceof _) {
            return obj;
        }
        // 判斷函數中this的指向,若是this不是_的實例,那麼返回構造的_實例。
        // 這裏是爲了避免使用new操做符構造新對象,很巧妙,由於在經過new使用構造函數時,函數中的this會指向新構造的實例。
        if(!(this instanceof _)) {
            return new _();
        }
        // 
        this._wrapped = obj;
    };
    _.each = function() {
        // ...
    };
    // ...
}())

這一段的處理很關鍵也很巧妙。程序員

2 導出變量

既然咱們是在當即執行函數內定義的變量,那麼_的生命週期也只存在於匿名函數的執行階段,一旦函數執行完畢,這個變量所存儲的數據也就被釋放掉了,因此不導出變量的話實際上這段代碼至關於什麼都沒作。那麼該如何導出變量呢?咱們知道函數內部能夠訪問到外部的變量,因此只要把變量賦值給外部做用域或者外部做用域變量就好了。一般爲了方便實用,把變量賦值給全局做用域,不一樣的環境全局做用域名稱不一樣,瀏覽器環境下一般爲window,服務器環境下一般爲global,根據不一樣的使用環境須要作不一樣的處理,好比瀏覽器環境下代碼形式以下:es6

(function() {
    var _ = function() {
        // ...
    };
    _.each = function() {
        // ...
    };
    // ...
    window._ = _;
}())

這樣處理以後,在全局做用域就能夠直接經過_使用Underscore的接口了。github

可是僅僅這樣處理還不夠,由於Underscore面向環境不少,針對不一樣的環境要作不一樣的處理。接下來看Underscore源碼。npm

首先,Underscore經過如下代碼根據不一樣的環境獲取不一樣的全局做用域:

//獲取全局對象,在瀏覽器中是self或者window,在服務器端(Node)中是global。
//在瀏覽器控制檯中輸入self或者self.self,結果都是window。
var root = typeof self == 'object' && self.self === self && self || typeof global == 'object' && global.global === global && global || this || {};
root._ = _;

註釋寫在了代碼中,若是既不是瀏覽器環境也不是Node環境的話,就獲取值爲this,經過this獲取全局做用域,若是this仍然爲空,就賦值給一個空的對象。感謝大神@冴羽的指教,賦值給空對象的做用是防止在開發微信小程序時報錯,由於在微信小程序這種特殊環境下,window和global都是undefined,而且強制開啓了strict模式,這時候this也是undefined(嚴格模式下禁止this指向全局變量),因此指定一個空對象給root,防止報錯,具體參考:`this` is undefined in strict mode

這裏值得學習的地方還有做者關於賦值的寫法,十分簡潔,嘗試了一下,對於下面的寫法:

const flag = val1 && val2 && val3 || val4 && val5;

程序會從左到右依次判斷val一、val二、val3的值,假設||把與運算分爲許多組,那麼:

  • 一旦當前判斷組的某個值轉換爲Boolean值後爲false,那麼就跳轉到下一組進行判斷,直到最後一組,若是最後一組仍然有值被判斷爲false,那麼爲false的值被賦給flag。
  • 若是當前判斷組全部的值轉換後都爲true,那麼最後一個值會被賦給flag。

好比:

const a = 1 && 2 && 3 || 2 && 3;
// a === 3
const b = 1 && false && 2 || 2 && 3;
// b === 3
const c = 1 && false && 2 || false && 2
// c === false
const d = 1 && false && 2 || 0 && 2
// d === 0
const e = 1 && false && 2 || 1 && 2
// e === 2

除了要考慮給全局做用域賦值的差別之外,還要考慮JavaScript模塊化規範的差別,JavaScript模塊化規範包括AMD、CMD等。

經過如下代碼兼容AMD規範:

//兼容AMD規範的模塊化工具,好比RequireJS。
if (typeof define == 'function' && define.amd) {
	define('underscore', [], function () {
		return _;
	});
}

若是define是一個函數而且define.amd不爲null或者undefined,那就說明是在AMD規範的工做環境下,使用define函數導出變量。

經過如下代碼兼容CommonJS規範:

//爲Node環境導出underscore,若是存在exports對象或者module.exports對象而且這兩個對象不是HTML DOM,那麼即爲Node環境。
//若是不存在以上對象,把_變量賦值給全局環境(瀏覽器環境下爲window)。
if (typeof exports != 'undefined' && !exports.nodeType) {
	if (typeof module != 'undefined' && !module.nodeType && module.exports) {
		exports = module.exports = _;
	}
	exports._ = _;
} else {
	root._ = _;
}

此外,經過以上代碼能夠支持ES6模塊的import語法。具體原理參考阮一峯老師的教程:ES6 模塊加載 CommonJS 模塊。若是既不是AMD規範也不是CommonJS規範,那麼直接將_賦值給全局變量。這一點能夠經過將Underscore源碼複製到瀏覽器的控制檯回車後再查看__.prototype的值獲得結論。

導出變量以後,在外部就可使用咱們定義的接口了。

3 實現鏈式調用

許多出名的工具庫都會提供鏈式調用功能,好比jQuery的鏈式調用:$('...').css().click();,Underscore也提供了鏈式調用功能:_.chain(...).each().unzip();

鏈式調用基本都是經過返回原對象實現的,好比返回this,在Underscore中,能夠經過_.chain函數開始鏈式調用,實現原理以下:

// Add a "chain" function. Start chaining a wrapped Underscore object.
//將傳入的對象包裝爲鏈式調用的對象,將其標誌位置位true。
_.chain = function (obj) {
	var instance = _(obj);
	instance._chain = true;
	return instance;
};

它構造一個_實例,而後將其_chain鏈式標誌位屬性值爲true表明鏈式調用,而後返回這個實例。這樣作就是爲了強制經過_().function()的方式調用接口,由於在_的原型上,全部接口方法與_的屬性方法有差別,_原型上的方法多了一個步驟,它會對其父對象的_chain屬性進行判斷,若是爲true,那麼就繼續使用_.chain方法進行鏈式調用的包裝,在一部分在後續會繼續討論。

4 實現接口擴展

在許多出名的工具庫中,均可以實現用戶擴展接口,好比jQuery的$.extend$.fn.extend方法,Underscore也不例外,其_.mixin方法容許用戶擴展接口。

這裏涉及到的一個概念就是mixin設計模式,mixin設計模式是JavaScript中最多見的設計模式,能夠理解爲把一個對象的屬性拷貝到另一個對象上,具體能夠參考:摻雜模式(mixin)

先看Underscore中_.mixin方法的源代碼:

_.mixin = function (obj) {
	// _.functions函數用於返回一個排序後的數組,包含全部的obj中的函數名。
	_.each(_.functions(obj), function (name) {
		// 先爲_對象賦值。
		var func = _[name] = obj[name];
		// 爲_的原型添加函數,以增長_(obj).mixin形式的函數調用方法。
		_.prototype[name] = function () {
			// this._wrapped做爲第一個參數傳遞,其餘用戶傳遞的參數放在後面。
			var args = [this._wrapped];
			push.apply(args, arguments);
			// 使用chainResult對運算結果進行鏈式調用處理,若是是鏈式調用就返回處理後的結果,
			// 若是不是就直接返回運算後的結果。
			return chainResult(this, func.apply(_, args));
		};
	});
	return _;
};

這段代碼很好理解,就是對於傳入的obj對象參數,將對象中的每個函數拷貝到_對象上,同名會被覆蓋。與此同時,還會把obj參數對象中的函數映射到_對象的原型上,爲何說是映射,由於並非直接拷貝的,還進行了鏈式調用的處理,經過chainResult方法,實現了了鏈式調用,因此第三節中說_對象原型上的方法與_對象中的對應方法有差別,原型上的方法多了一個步驟,就是判斷是否鏈式調用,若是是鏈式調用,那麼繼續經過_.chain函數進行包裝。chainResult函數代碼以下:

// Helper function to continue chaining intermediate results.
//返回一個鏈式調用的對象,經過判斷instance._chain屬性是否爲true來決定是否返回鏈式對象。
var chainResult = function (instance, obj) {
	return instance._chain ? _(obj).chain() : obj;
};

實現mixin函數以後,Underscore的設計者很是機智的運用了這個函數,代碼中只能夠看到爲_自身定義的一系列函數,好比_.each_.map等,但看不到爲_.prototype所定義的函數,爲何還能夠經過_().function()的形式調用接口呢?這裏就是由於做者經過_.mixin函數直接將全部_上的函數映射到了_.prototype上,在_.mixin函數定義的下方,有一句代碼:

// Add all of the Underscore functions to the wrapper object.
_.mixin(_);

這句代碼就將全部的_上的函數映射到了_.prototype上,有點令我歎爲觀止。

經過_.mixin函數,用戶能夠爲_擴展自定義的接口,下面的例子來源於中文手冊

_.mixin({
    capitalize: function(string) {
        return string.charAt(0).toUpperCase() + string.substring(1).toLowerCase();
    }
});
_("fabio").capitalize();
=> "Fabio"

5 實現noConflict

在許多工具庫中,都有實現noConflict,由於在全局做用域,變量名是獨一無二的,可是用戶可能引入多個類庫,多個類庫可能有同一個標識符,這時就要使用noConflict實現無衝突處理。

具體作法就是先保存原來做用域中該標誌位的數據,而後在調用noConflict函數時,爲全局做用域該標誌位賦值爲原來的值。代碼以下:

// Save the previous value of the `_` variable.
//保存以前全局對象中_屬性的值。
var previousUnderscore = root._;
// Run Underscore.js in *noConflict* mode, returning the `_` variable to its
// previous owner. Returns a reference to the Underscore object.
_.noConflict = function () {
	root._ = previousUnderscore;
	return this;
};

在函數的最後,返回了Underscore對象,容許用戶使用另外的變量存儲。

6 爲變量定義一系列基本屬性

做爲一個對象,應該有一些基本屬性,好比toString、value等等,須要重寫這些屬性或者函數,以便使用時返回合適的信息。此外還須要添加一些版本號啊什麼的屬性。

7 總結

作完以上全部的工做以後,一個基本的工具庫基本就搭建完成了,完成好測試、壓縮等工做以後,就能夠發佈在npm上供你們下載了。想要寫一個本身的工具庫的同窗能夠嘗試一下。

另外若是有錯誤之處或者有補充之處的話,歡迎你們不吝賜教,一塊兒學習,一塊兒進步!

更多Underscore源碼解析:GitHub

相關文章
相關標籤/搜索