最近開始看 underscore.js 源碼,並將 underscore.js 源碼解讀 放在了個人 2016 計劃中。javascript
閱讀一些著名框架類庫的源碼,就好像和一個個大師對話,你會學到不少。爲何是 underscore?最主要的緣由是 underscore 簡短精悍(約 1.5k 行),封裝了 100 多個有用的方法,耦合度低,很是適合逐個方法閱讀,適合樓主這樣的 JavaScript 初學者。從中,你不只能夠學到用 void 0 代替 undefined 避免 undefined 被重寫等一些小技巧 ,也能夠學到變量類型判斷、函數節流&函數去抖等經常使用的方法,還能夠學到不少瀏覽器兼容的 hack,更能夠學到做者的總體設計思路以及 API 設計的原理(向後兼容)。php
以後樓主會寫一系列的文章跟你們分享在源碼閱讀中學習到的知識。java
underscore-1.8.3 源碼解讀項目地址 https://github.com/hanzichi/underscore-analysisgit
underscore-1.8.3 源碼全文註釋 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/underscore-1.8.3-analysis.jsgithub
underscore-1.8.3 源碼解讀系列文章 https://github.com/hanzichi/underscore-analysis/issuescanvas
歡迎圍觀~ (若是有興趣,歡迎 star & watch~)您的關注是樓主繼續寫做的動力數組
本文跟你們聊聊 JavaScript 中如何判斷兩個參數 "相同",即 underscore 源碼中的 _.isEqual 方法。這個方法能夠說是 underscore 源碼中實現最複雜的方法(用了百來行),幾乎沒有之一。瀏覽器
那麼,我說的 "相同" 究竟是什麼意思?舉個栗子,1
和 new Number(1)
被認爲是 equal,[1]
和 [1]
被認爲是 equal(儘管它們的引用並不相同),固然,兩個引用相同的對象確定是 equal 的了。框架
那麼,如何設計這個 _.isEqual 函數呢?咱們跟着 underscore 源碼,一步步來看它的實現。後文中均假設比較的兩個參數爲 a 和 b。ecmascript
首先咱們判斷 a === b
,爲 true 的狀況有兩種,其一是 a 和 b 都是基本類型,那麼就是兩個基本類型的值相同,其二就是兩個引用類型,那麼就是引用類型的引用相同。那麼若是 a === b
爲 true,是否就是說 a 和 b 是 equal 的呢?事實上,99% 的狀況是這樣的,還得考慮 0 和 -0 這個 special case,0 === -0
爲 true,而 0 和 -0 被認爲是 unequal,至於緣由,能夠參考 http://wiki.ecmascript.org/doku.php?id=harmony:egal。
這部分代碼能夠這樣表示:
// Identical objects are equal. `0 === -0`, but they aren't identical. // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). // a === b 時 // 須要注意 `0 === -0` 這個 special case // 0 和 -0 不相同 // 至於緣由能夠參考上面的連接 if (a === b) return a !== 0 || 1 / a === 1 / b;
接下去的狀況,也就是 a !== b
的狀況了。
若是 a 和 b 中有一個是 null 或者 undefined,那麼能夠特判下,不用繼續比較了。源碼實現:
// A strict comparison is necessary because `null == undefined`. // 若是 a 和 b 有一個爲 null(或者 undefined) // 判斷 a === b if (a == null || b == null) return a === b;
我的以爲這裏寫的有點多餘,由於根據上面的判斷過濾,a === b 確定是返回 false 的。
ok,咱們繼續,接下來咱們能夠先根據 a 和 b 的類型來判斷,若是類型不同,那麼就不必繼續判斷了。如何獲取變量類型?沒錯,就是神奇的 Object.prototype.toString.call
!
若是類型是 RegExp 和 String,咱們能夠將 a 和 b 分別轉爲字符串進行比較(若是是 String 就已是字符串了),舉個栗子:
var a = /a/; var b = new RegExp("a"); console.log(_.isEqual(a, b)); // => true
其實它在 underscore 內部是這樣判斷的:
var a = /a/; var b = new RegExp("a"); var _a = '' + a; // => /a/ var _b = '' + b; // => /a/ console.log(_a === _b); // => true
若是是 Number 類型呢?這裏又有個 special case,就是 NaN!這裏規定,NaN 僅和 NaN 相同,與別的 Number 類型均 unequal。這裏咱們將引用類型均轉爲基本類型,看以下代碼:
var a = new Number(1); console.log(+a); // 1
沒錯,加個 +
就解決了,其餘的不難理解,都在註釋裏了。
// `NaN`s are equivalent, but non-reflexive. // Object(NaN) is equivalent to NaN // 若是 +a !== +a // 那麼 a 就是 NaN // 判斷 b 是否也是 NaN 便可 if (+a !== +a) return +b !== +b; // An `egal` comparison is performed for other numeric values. // 排除了 NaN 干擾 // 還要考慮 0 的干擾 // 用 +a 將 Number() 形式轉爲基本類型 // 若是 a 爲 0,判斷 1 / +a === 1 / b // 不然判斷 +a === +b return +a === 0 ? 1 / +a === 1 / b : +a === +b; // 若是 a 爲 Number 類型 // 要注意 NaN 這個 special number // NaN 和 NaN 被認爲 equal
接下來咱們看 Date 和 Boolean 兩個類型。跟 Number 類型類似,它們也能夠用 +
轉化爲基本類型的數字!看下面代碼:
var a = new Date(); var b = true; var c = new Boolean(false); console.log(+a); // 1464180857222 console.log(+b); // 1 console.log(+c); // 0
很是簡單,其實 +new Date() (或者也能夠寫成 +new Date)獲取的正是當前時間和 1970 年 1 月 1 日 0 點的毫秒數(millisecond),可能你據說過期間戳,其實時間戳就是這個數據除以 1000,也就是秒數。在用 canvas 作動畫時,我常常用 +new Date 來當時間戳。
so,若是 a 和 b 均是 Date 類型或者 Boolean 類型,咱們能夠用 +a === +b
來判斷是否 equal。
程序接着走,咱們接着看,彷佛還有兩類重要的類型沒有判斷?沒錯,Array 和 Object!underscore 對此採用遞歸方法展開來比較。
仍是舉個栗子吧,舉例比較直觀。
假設 a,b 以下:
var a = {name: "hanzichi", loveCity: [{cityName: "hangzhou", province: "zhenjiang"}], age: 30}; var b = {name: "hanzichi", loveCity: [{cityName: "hangzhou", province: "zhenjiang"}], age: 25};
首先 a,b 是對象,咱們能夠分別比較其鍵值對,若是有一個鍵值對不一樣(或者說一個鍵值對 a 和 b 有一個沒有),則 a 和 b unequal。若是是數組呢?那就一個一個元素比較嘍。由於數組可能嵌套對象,對象的 value 又多是數組,因此這裏用了遞歸。
仍是以上面的例子,咱們能夠把它拆成三次比較,分別比較三個 key 的 value 值是否相同。對於 loveCity 這個 key 的 value,由於其 value 又是個數組,因此咱們將這個 value 傳入比較函數,經過這個比較的結果,來判斷最後的比較結果。遞歸就是這樣,能夠將大的東西,拆成一個個小的,根據小的結果,來彙總獲得大的結果。
最後,給出代碼位置。關於 _.isEqual 方法的源碼,你們能夠參考 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L1094-L1190