【underscore 源碼解讀】JavaScript 中如何判斷兩個元素是否 "相同"

Why underscore

最近開始看 underscore.js 源碼,并將 underscore.js 源碼解讀 放在了我的 2016 計劃中。

閱讀一些著名框架類庫的源碼,就好像和一個個大師對話,你會學(xué)到很多。為什么是 underscore?最主要的原因是 underscore 簡短精悍(約 1.5k 行),封裝了 100 多個有用的方法,耦合度低,非常適合逐個方法閱讀,適合樓主這樣的 JavaScript 初學(xué)者。從中,你不僅可以學(xué)到用 void 0 代替 undefined 避免 undefined 被重寫等一些小技巧 ,也可以學(xué)到變量類型判斷、函數(shù)節(jié)流&函數(shù)去抖等常用的方法,還可以學(xué)到很多瀏覽器兼容的 hack,更可以學(xué)到作者的整體設(shè)計思路以及 API 設(shè)計的原理(向后兼容)。

之后樓主會寫一系列的文章跟大家分享在源碼閱讀中學(xué)習(xí)到的知識。

歡迎圍觀~ (如果有興趣,歡迎 star & watch~)您的關(guān)注是樓主繼續(xù)寫作的動力

_.isEqual

本文跟大家聊聊 JavaScript 中如何判斷兩個參數(shù) "相同",即 underscore 源碼中的 _.isEqual 方法。這個方法可以說是 underscore 源碼中實現(xiàn)最復(fù)雜的方法(用了百來行),幾乎沒有之一。

那么,我說的 "相同" 到底是什么意思?舉個栗子,1new Number(1) 被認為是 equal,[1][1] 被認為是 equal(盡管它們的引用并不相同),當然,兩個引用相同的對象肯定是 equal 的了。

那么,如何設(shè)計這個 _.isEqual 函數(shù)呢?我們跟著 underscore 源碼,一步步來看它的實現(xiàn)。后文中均假設(shè)比較的兩個參數(shù)為 a 和 b。

首先我們判斷 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,那么可以特判下,不用繼續(xù)比較了。源碼實現(xiàn):

// A strict comparison is necessary because `null == undefined`.
// 如果 a 和 b 有一個為 null(或者 undefined)
// 判斷 a === b
if (a == null || b == null) return a === b;

個人覺得這里寫的有點多余,因為根據(jù)上面的判斷過濾,a === b 肯定是返回 false 的。

ok,我們繼續(xù),接下來我們可以先根據(jù) a 和 b 的類型來判斷,如果類型不一樣,那么就沒必要繼續(xù)判斷了。如何獲取變量類型?沒錯,就是神奇的 Object.prototype.toString.call!

如果類型是 RegExp 和 String,我們可以將 a 和 b 分別轉(zhuǎn)為字符串進行比較(如果是 String 就已經(jīng)是字符串了),舉個栗子:

var a = /a/;
var b = new RegExp("a");

console.log(_.isEqual(a, b));  // => true

其實它在 underscore 內(nèi)部是這樣判斷的:

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!這里規(guī)定,NaN 僅和 NaN 相同,與別的 Number 類型均 unequal。這里我們將引用類型均轉(zhuǎn)為基本類型,看如下代碼:

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() 形式轉(zhuǎn)為基本類型
// 如果 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 類型相似,它們也可以用 + 轉(zhuǎn)化為基本類型的數(shù)字!看下面代碼:

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 點的毫秒數(shù)(millisecond),可能你聽說過時間戳,其實時間戳就是這個數(shù)據(jù)除以 1000,也就是秒數(shù)。在用 canvas 做動畫時,我經(jīng)常用 +new Date 來當時間戳。

so,如果 a 和 b 均是 Date 類型或者 Boolean 類型,我們可以用 +a === +b 來判斷是否 equal。

程序接著走,我們接著看,似乎還有兩類重要的類型沒有判斷?沒錯,Array 和 Object!underscore 對此采用遞歸方法展開來比較。

還是舉個栗子吧,舉例比較直觀。

假設(shè) 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。如果是數(shù)組呢?那就一個一個元素比較嘍。因為數(shù)組可能嵌套對象,對象的 value 又可能是數(shù)組,所以這里用了遞歸。

還是以上面的例子,我們可以把它拆成三次比較,分別比較三個 key 的 value 值是否相同。對于 loveCity 這個 key 的 value,因為其 value 又是個數(shù)組,所以我們將這個 value 傳入比較函數(shù),通過這個比較的結(jié)果,來判斷最后的比較結(jié)果。遞歸就是這樣,可以將大的東西,拆成一個個小的,根據(jù)小的結(jié)果,來匯總得到大的結(jié)果。

最后,給出代碼位置。關(guān)于 _.isEqual 方法的源碼,大家可以參考 https://github.com/hanzichi/underscore-analysis/blob/master/underscore-1.8.3.js/src/underscore-1.8.3.js#L1094-L1190

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容