1 爲何要判斷?
可能有些同窗看到這個標題就會產生疑惑,爲何咱們要判斷JavaScript中的兩個變量是否相等,JavaScript不是已經提供了雙等號「==」以及三等號「===」給咱們使用了嗎?javascript
其實,JavaScript雖然給咱們提供了相等運算符,可是仍是存在一些缺陷,這些缺陷不符合咱們的思惟習慣,有可能在使用的時候獲得一些意外的結果。爲了不這種狀況的出現,咱們須要本身函數來實現JavaScript變量之間的對比。php
2 JavaScript等號運算符存在哪些缺陷?
2.1 0與-0
在JavaScript中:java
0 === 0 //true +0 === -0 //true
相等運算符認爲+0和-0是相等的,可是咱們應當認爲二者是不等的,具體緣由源碼中給出了一個連接:Harmony egal proposal.git
2.2 null和undefined
在JavaScript中:github
null == undefined //true null === undefined //false
咱們應當認爲null不等於undefined,因此在比較null和undefined時,應當返回false。正則表達式
2.3 NaN
前文有說過,NaN是一個特殊的值,它是JavaScript中惟一一個自身不等於自身的值。express
NaN == NaN //false NaN === NaN //false
可是咱們在對比兩個NaN時,咱們應當認爲它們是相等的。數組
2.4 數組之間的對比
因爲在JavaScript中,數組是一個對象,因此若是兩個變量不是引用的同一個數組的話,即便兩個數組如出一轍也不會返回true。app
var a = []; //undefined var b = []; //undefined a=== b //false a==b //false
可是咱們應當認爲,兩個元素位置、順序以及值相同的數組是相等的。ecmascript
2.5 對象之間的對比
凡是涉及到對象的變量,只要不是引用同一個對象,都會被認爲不相等。咱們須要作出一些改變,兩個徹底一致的對象應當被認爲是相等的。
var a = {}; //undefined var b = {}; //undefined a == b //false a === b //false
這種狀況在全部JavaScript內置對象中也適用,好比咱們應當認爲兩個同樣的RegExp對象是相等的。
2.6 基本數據類型與包裝數據類型之間的對比
在JavaScript中,數值2和Number對象2是不嚴格相等的:
2 == new Number(2); //true 2 === new Number(2); //false
可是咱們在對比2和new Number(2)時應當認爲二者相等。
3 underscore的實現方法
咱們實現的方法固然仍是依賴於JavaScript相等運算符的,只不過針對特例須要有特定的處理。咱們在比較以前,首先應該作的就是處理特殊狀況。
underscore的代碼中,沒有直接將邏輯寫在_.isEqual方法中,而是定義了兩個私有方法:eq和deepEq。在GitHub用戶@hanzichi的repo中,咱們能夠看到1.8.3版本的underscore中並無deepEq方法,爲何後來添加了呢?這是由於underscore的做者把一些特例的處理提取了出來,放到了eq方法中,而更加複雜的對象之間的對比被放到了deepEq中(同時使得deepEq方法更加便於遞歸調用)。這樣的作法使得代碼邏輯更加鮮明,方法的功能也更加單一明確,維護代碼更加簡潔快速。
eq方法的源代碼:
var eq = function (a, b, aStack, bStack) { // 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). //除了0 === -0這個特例以外,其他全部a === b的例子都表明它們相等。 //應當判斷0 !== -0,可是JavaScript中0 === -0。 //下面這行代碼就是爲了解決這個問題。 //當a !== 0或者1/a === 1/b時返回true,一旦a === 0而且1/a !== 1/b就返回false。 //而a === 0且1/a !== 1/b就表明a,b有一個爲0,有一個爲-0。 if (a === b) return a !== 0 || 1 / a === 1 / b; //一旦a、b不嚴格相等,就進入後續檢測。 //a == b成立可是a === b不成立的例子中須要排除null和undefined,其他例子須要後續判斷。 // `null` or `undefined` only equal to itself (strict comparison). //一旦a或者b中有一個爲null就表明另外一個爲undefined,這種狀況能夠直接排除。 if (a == null || b == null) return false; // `NaN`s are equivalent, but non-reflexive. //自身不等於自身的狀況,一旦a,b都爲NaN,則能夠返回true。 if (a !== a) return b !== b; // Exhaust primitive checks //若是a,b都不爲JavaScript對象,那麼通過以上監測以後還不嚴格相等的話就能夠直接判定a不等於b。 var type = typeof a; if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; //若是a,b是JavaScript對象,還須要作後續深刻的判斷。 return deepEq(a, b, aStack, bStack); };
對於源碼的解讀我已經做爲註釋寫在了源碼中。 那麼根據源碼,能夠將其邏輯抽象出來:
deepEq的源碼:
var deepEq = function (a, b, aStack, bStack) { // Unwrap any wrapped objects. //若是a,b是_的一個實例的話,須要先把他們解包出來再進行比較。 if (a instanceof _) a = a._wrapped; if (b instanceof _) b = b._wrapped; // Compare `[[Class]]` names. //先根據a,b的Class字符串進行比較,若是兩個對象的Class字符串都不同, //那麼直接能夠認爲二者不相等。 var className = toString.call(a); if (className !== toString.call(b)) return false; //若是二者的Class字符串相等,再進一步進行比較。 //優先檢測內置對象之間的比較,非內置對象再日後檢測。 switch (className) { // Strings, numbers, regular expressions, dates, and booleans are compared by value. //若是a,b爲正則表達式,那麼轉化爲字符串判斷是否相等便可。 case '[object RegExp]': // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') case '[object String]': // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is // equivalent to `new String("5")`. //若是a, b是字符串對象,那麼轉化爲字符串進行比較。由於一下兩個變量: //var x = new String('12'); //var y = new String('12'); //x === y是false,x === y也是false,可是咱們應該認爲x與y是相等的。 //因此咱們須要將其轉化爲字符串進行比較。 return '' + a === '' + b; case '[object Number]': //數字對象轉化爲數字進行比較,而且要考慮new Number(NaN) === new Number(NaN)應該要成立的狀況。 // `NaN`s are equivalent, but non-reflexive. // Object(NaN) is equivalent to NaN. if (+a !== +a) return +b !== +b; // An `egal` comparison is performed for other numeric values. //排除0 === -0 的狀況。 return +a === 0 ? 1 / +a === 1 / b : +a === +b; case '[object Date]': //Date類型以及Boolean類型均可以轉換爲number類型進行比較。 //在變量前加一個加號「+」,能夠強制轉換爲數值型。 //在Date型變量前加一個加號「+」能夠將Date轉化爲毫秒形式;Boolean類型同上(轉換爲0或者1)。 case '[object Boolean]': // Coerce dates and booleans to numeric primitive values. Dates are compared by their // millisecond representations. Note that invalid dates with millisecond representations // of `NaN` are not equivalent. return +a === +b; case '[object Symbol]': return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b); } var areArrays = className === '[object Array]'; //若是不是數組對象。 if (!areArrays) { if (typeof a != 'object' || typeof b != 'object') return false; // Objects with different constructors are not equivalent, but `Object`s or `Array`s // from different frames are. //比較兩個非數組對象的構造函數。 var aCtor = a.constructor, bCtor = b.constructor; if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor && _.isFunction(bCtor) && bCtor instanceof bCtor) && ('constructor' in a && 'constructor' in b)) { return false; } } // Assume equality for cyclic structures. The algorithm for detecting cyclic // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. // Initializing stack of traversed objects. // It's done here since we only need them for objects and arrays comparison. //初次調用eq函數時,aStack以及bStack均未被傳遞,在循環遞歸的時候,會被傳遞進來。 //aStack和bStack存在的意義在於循環引用對象之間的比較。 aStack = aStack || []; bStack = bStack || []; var length = aStack.length; while (length--) { // Linear search. Performance is inversely proportional to the number of // unique nested structures. if (aStack[length] === a) return bStack[length] === b; } // Add the first object to the stack of traversed objects. //初次調用eq函數時,就把兩個參數放入到參數堆棧中去,保存起來方便遞歸調用時使用。 aStack.push(a); bStack.push(b); // Recursively compare objects and arrays. //若是是數組對象。 if (areArrays) { // Compare array lengths to determine if a deep comparison is necessary. length = a.length; //長度不等,直接返回false認定爲數組不相等。 if (length !== b.length) return false; // Deep compare the contents, ignoring non-numeric properties. while (length--) { //遞歸調用。 if (!eq(a[length], b[length], aStack, bStack)) return false; } } else { // Deep compare objects. //對比純對象。 var keys = _.keys(a), key; length = keys.length; // Ensure that both objects contain the same number of properties before comparing deep equality. //對比屬性數量,若是數量不等,直接返回false。 if (_.keys(b).length !== length) return false; while (length--) { // Deep compare each member key = keys[length]; if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false; } } // Remove the first object from the stack of traversed objects. //循環遞歸結束,把a,b堆棧中的元素推出。 aStack.pop(); bStack.pop(); return true; };
對於源碼的解讀我已經做爲註釋寫在了源碼中。 那麼根據源碼,能夠將其邏輯抽象出來:
-
1 使用Object.prototype.toString方法獲取兩參數類型,若是兩參數的原始數據類型都不一樣,那麼能夠認爲兩個參數不相等。
-
2 若是進入了第二步,那麼說明兩個參數的原始類型相同。針對獲取到的字符串進行分類,若是是除Object和Array以外的類型,進行處理。
- RegExp以及String對象轉化爲字符串進行比較。
- Number類型的話,須要先使用+運算符強制轉化爲基本數據類型中的數值型,而後處理特例。好比NaN === NaN,0 !== -0.
- Date以及Boolean對象轉化爲數字類型進行對比。(+運算符強制轉換,Date轉化爲13位的毫秒形式,Boolean轉化爲0或1)
- Symbol類型使用Symbol.prototype.valueOf獲取字符串,而後進行對比(即認爲傳遞給Symbol函數相同字符串所獲取到的Symbol對象應該相等)。
-
3 通過以上比較,所剩類型基本只剩Array和基本對象了。若是不是數組對象,那麼構造函數不一樣的對象能夠被認爲是不相等的對象。
-
4 初始化對象棧aStack以及bStack,由於初次調用deepEq函數時不會傳遞這兩個參數,因此須要手動初始化。由於以後比較的數組對象以及基本對象須要用到對象棧,因此如今應該把當前的a,b推入到兩個棧中。
-
5 針對數組,先比較長度,長度不等則數組不等。長度相等再遞歸調用deepGet比較數組的每一項,有一項不等則返回false。
-
6 基本對象類型比較,先使用_.keys獲取對象的全部鍵。鍵數量不一樣的兩對象不一樣,若是鍵數目相等,再遞歸調用deepEq比較每個鍵的屬性,有一個鍵值不等則返回false。
-
7 通過全部檢測若是都沒有返回false的話,能夠認爲兩參數相等,返回true。在返回以前會把棧中的數據推出一個。
4 underscore的精髓
4.1 將RegExp對象和String對象用相同方法處理
有同窗可能會疑惑:/[a-z]/gi
與/[a-z]ig/
在乎義上是同樣的,可是轉化爲字符串以後比較會不會是不相等的?
這是一個很是好的問題,同時也是underscore處理的巧妙之所在。在JavaScript中,RegExp對象重寫了toString方法,因此在強制將RegExp對象轉化爲字符串時,flags會按規定順序排列,因此將以前兩個RegExp對象轉化爲字符串,都會獲得/[a-z]/gi
。這就是underscore能夠放心大膽的將RegExp對象轉化爲字符串處理的緣由。
4.2 Date對象和Boolean對象使用相同方法處理
underscore選擇將Date對象和Boolean對象都轉化爲數值進行處理,這避免了紛繁複雜的類型轉換,簡單粗暴。並且做者沒有使用強制轉換方法進行轉換,而是隻使用了一個「+」符號,就強制將Date對象和Boolean對象轉換成了數值型數據。
4.3 使用對象棧保存當前比較對象的上下文
不少童鞋在閱讀源碼時,可能會很疑惑aStack以及bStack的做用在哪裏。aStack和bStack用於保存當前比較對象的上下文,這使得咱們在比較某個對象的子屬性時,還能夠獲取到其自身。這樣作的好處就在於咱們能夠比較循環引用的對象。
var a = { name: 'test' }; a['test1'] = a; var b = { name: 'test' }; b['test1'] = b; _.isEqual(a, b); //true
underscore使用aStack和bStack做比較的代碼:
aStack = aStack || []; bStack = bStack || []; var length = aStack.length; while (length--) { // Linear search. Performance is inversely proportional to the number of // unique nested structures. if (aStack[length] === a) return bStack[length] === b; }
上面的測試代碼中,a、b對象的test1屬性都引用了它們自身,這樣的對象在比較時會消耗沒必要要的時間,由於只要a和b的test1屬性都等於其某個父對象,那麼能夠認爲a和b相等,由於這個被遞歸的方法返回以後,還要繼續比較它們對應的那個父對象,父對象相等,則引用的對象屬性必相等,這樣的處理方法節省了不少的時間,也提升了underscore的性能。
4.4 優先級分明,有的放矢
underscore的處理具備很強的優先級,好比在比較數組對象時,先比較數組的長度,數組長度不相同則數組一定不相等;好比在比較基本對象時,優先比較對象鍵的數目,鍵數目不等則對象一定不等;好比在比較兩個對象參數以前,優先對比Object.prototype.toString返回的字符串,若是基本類型不一樣,那麼兩個對象一定不相等。
這樣的主次分明的對比,大大提升了underscore的工做效率。因此說每個小小的細節,均可以體現出做者的處心積慮。閱讀源碼,可以使咱們學習到太多的東西。
5 underscore的缺陷之處
咱們能夠在其餘方法中看到underscore對ES6中新特徵的支持,好比_.is[Type]
方法已經支持檢測Map(_.isMap)
和Set(_.isSet
)等類型了。可是_.isEqual
卻沒有對Set和Map結構的支持。若是咱們使用_.isEqual
比較兩個Map或者兩個Set,老是會獲得true的結果,由於它們能夠經過全部的檢測。
在underscore的官方GitHub repo上,我看到有同窗已經提交了PR添加了_.isEqual
對Set和Map的支持。
咱們能夠看一下源碼:
var size = a.size; // Ensure that both objects are of the same size before comparing deep equality. if (b.size !== size) return false; while (size--) { // Deep compare the keys of each member, using SameValueZero (isEq) for the keys if (!(isEq(a.keys().next().value, b.keys().next().value, aStack, bStack))) return false; // If the objects are maps deep compare the values. Value equality does not use SameValueZero. if (className === '[object Map]') { if (!(eq(a.values().next().value, b.values().next().value, aStack, bStack))) return false; } }
能夠看到其思路以下:
- 1 比較兩參數的長度(或者說是鍵值對數),長度不一者即爲不等,返回false。
- 2 若是長度相等,就逐一遞歸比較它們的每一項,有任意一項不等者就返回false。
- 3 所有經過則能夠認爲是相等的,返回true。
這段代碼有一個很巧妙的地方在於它沒有區分究竟是Map對象仍是Set對象,先直接使用a.keys().next().value
以及b.keys().next().value
獲取Set的元素值或者Map的鍵。後面再進行類型判斷,若是是Map對象的話,再使用a.values().next().value
以及b.values().next().value
獲取Map的鍵值,Map對象還須要比較其鍵值是否相等。
我的認爲,這段代碼也有其侷限性,由於Set和Map能夠認爲是一個數據集,這區別於數組對象。咱們能夠說[1,2,3]不等於[2,1,3],由於其相同元素的位置不一樣;可是我認爲new Set([1,2,3])應該認爲等於new Set([2,1,3]),由於Set是無序的,它內部的元素具備單一性。