雖然標題寫的是如何判斷兩個對象相等,但本篇咱們不只僅判斷兩個對象相等,實際上,咱們要作到的是如何判斷兩個參數相等,而這必然會涉及到多種類型的判斷。git
什麼是相等?在《JavaScript專題之去重》中,咱們認爲只要 ===
的結果爲 true,二者就相等,然而今天咱們從新定義相等:github
咱們認爲:編程
不只僅是這些長得同樣的,還有數組
更復雜的咱們會在接下來的內容中看到。瀏覽器
咱們的目標是寫一個 eq 函數用來判斷兩個參數是否相等,使用效果以下:編程語言
function eq(a, b) { ... } var a = [1]; var b = [1]; console.log(eq(a, b)) // true
在寫這個看似很簡單的函數以前,咱們首先了解在一些簡單的狀況下是如何判斷的?函數
若是 a === b 的結果爲 true, 那麼 a 和 b 就是相等的嗎?通常狀況下,固然是這樣的,可是有一個特殊的例子,就是 +0 和 -0。學習
JavaScript 「處心積慮」的想抹平二者的差別:this
// 表現1 console.log(+0 === -0); // true // 表現2 (-0).toString() // '0' (+0).toString() // '0' // 表現3 -0 < +0 // false +0 < -0 // false
即使如此,二者依然是不一樣的:url
1 / +0 // Infinity 1 / -0 // -Infinity 1 / +0 === 1 / -0 // false
也許你會好奇爲何要有 +0 和 -0 呢?
這是由於 JavaScript 採用了IEEE_754 浮點數表示法(幾乎全部現代編程語言所採用),這是一種二進制表示法,按照這個標準,最高位是符號位(0 表明正,1 表明負),剩下的用於表示大小。而對於零這個邊界值 ,1000(-0) 和 0000(0)都是表示 0 ,這纔有了正負零的區別。
也許你會好奇何時會產生 -0 呢?
Math.round(-0.1) // -0
那麼咱們又該如何在 === 結果爲 true 的時候,區別 0 和 -0 得出正確的結果呢?咱們能夠這樣作:
function eq(a, b){ if (a === b) return a !== 0 || 1 / a === 1 / b; return false; } console.log(eq(0, 0)) // true console.log(eq(0, -0)) // false
在本篇,咱們認爲 NaN 和 NaN 是相等的,那又該如何判斷出 NaN 呢?
console.log(NaN === NaN); // false
利用 NaN 不等於自身的特性,咱們能夠區別出 NaN,那麼這個 eq 函數又該怎麼寫呢?
function eq(a, b) { if (a !== a) return b !== b; } console.log(eq(NaN, NaN)); // true
如今,咱們已經能夠去寫 eq 函數的初版了。
// eq 初版 // 用來過濾掉簡單的類型比較,複雜的對象使用 deepEq 函數進行處理 function eq(a, b) { // === 結果爲 true 的區別出 +0 和 -0 if (a === b) return a !== 0 || 1 / a === 1 / b; // typeof null 的結果爲 object ,這裏作判斷,是爲了讓有 null 的狀況儘早退出函數 if (a == null || b == null) return false; // 判斷 NaN if (a !== a) return b !== b; // 判斷參數 a 類型,若是是基本類型,在這裏能夠直接返回 false var type = typeof a; if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; // 更復雜的對象使用 deepEq 函數進行深度比較 return deepEq(a, b); };
也許你會好奇是否是少了一個 typeof b !== function
?
試想若是咱們添加上了這句,當 a 是基本類型,而 b 是函數的時候,就會進入 deepEq 函數,而去掉這一句,就會進入直接進入 false,實際上 基本類型和函數確定是不會相等的,因此這樣作代碼又少,又可讓一種狀況更早退出。
如今咱們開始寫 deepEq 函數,一個要處理的重大難題就是 'Curly' 和 new String('Curly') 如何判斷成相等?
二者的類型都不同吶!不信咱們看 typeof 的操做結果:
console.log(typeof 'Curly'); // string console.log(typeof new String('Curly')); // object
但是咱們在《JavaScript專題之類型判斷上》中還學習過更多的方法判斷類型,好比 Object.prototype.toString:
var toString = Object.prototype.toString; toString.call('Curly'); // "[object String]" toString.call(new String('Curly')); // "[object String]"
神奇的是使用 toString 方法二者判斷的結果倒是一致的,但是就算知道了這一點,仍是不知道如何判斷字符串和字符串包裝對象是相等的呢?
那咱們利用隱式類型轉換呢?
console.log('Curly' + '' === new String('Curly') + ''); // true
看來咱們已經有了思路:若是 a 和 b 的 Object.prototype.toString的結果一致,而且都是"[object String]",那咱們就使用 '' + a === '' + b 進行判斷。
但是不止有 String 對象吶,Boolean、Number、RegExp、Date呢?
跟 String 一樣的思路,利用隱式類型轉換。
Boolean
var a = true; var b = new Boolean(true); console.log(+a === +b) // true
Date
var a = new Date(2009, 9, 25); var b = new Date(2009, 9, 25); console.log(+a === +b) // true
RegExp
var a = /a/i; var b = new RegExp(/a/i); console.log('' + a === '' + b) // true
Number
var a = 1; var b = new Number(1); console.log(+a === +b) // true
嗯哼?你肯定 Number 能這麼簡單的判斷?
var a = Number(NaN); var b = Number(NaN); console.log(+a === +b); // false
但是 a 和 b 應該被判斷成 true 的吶~
那麼咱們就改爲這樣:
var a = Number(NaN); var b = Number(NaN); function eq() { // 判斷 Number(NaN) Object(NaN) 等狀況 if (+a !== +a) return +b !== +b; // 其餘判斷 ... } console.log(eq(a, b)); // true
如今咱們能夠寫一點 deepEq 函數了。
var toString = Object.prototype.toString; function deepEq(a, b) { var className = toString.call(a); if (className !== toString.call(b)) return false; switch (className) { case '[object RegExp]': case '[object String]': return '' + a === '' + b; case '[object Number]': if (+a !== +a) return +b !== +b; return +a === 0 ? 1 / +a === 1 / b : +a === +b; case '[object Date]': case '[object Boolean]': return +a === +b; } // 其餘判斷 }
咱們看個例子:
function Person() { this.name = name; } function Animal() { this.name = name } var person = new Person('Kevin'); var animal = new Animal('Kevin'); eq(person, animal) // ???
雖然 person
和 animal
都是 {name: 'Kevin'}
,可是 person
和 animal
屬於不一樣構造函數的實例,爲了作出區分,咱們認爲是不一樣的對象。
若是兩個對象所屬的構造函數對象不一樣,兩個對象就必定不相等嗎?
並不必定,咱們再舉個例子:
var attrs = Object.create(null); attrs.name = "Bob"; eq(attrs, {name: "Bob"}); // ???
儘管 attrs
沒有原型,{name: "Bob"}
的構造函數是 Object
,可是在實際應用中,只要他們有着相同的鍵值對,咱們依然認爲是相等。
從函數設計的角度來看,咱們不該該讓他們相等,可是從實踐的角度,咱們讓他們相等,因此相等就是一件如此隨意的事情嗎?!對啊,我也在想:undersocre,你怎麼能如此隨意呢!!!
哎,吐槽完了,咱們仍是要接着寫這個相等函數,咱們能夠先作個判斷,對於不一樣構造函數下的實例直接返回 false。
function isFunction(obj) { return toString.call(obj) === '[object Function]' } function deepEq(a, b) { // 接着上面的內容 var areArrays = className === '[object Array]'; // 不是數組 if (!areArrays) { // 過濾掉兩個函數的狀況 if (typeof a != 'object' || typeof b != 'object') return false; var aCtor = a.constructor, bCtor = b.constructor; // aCtor 和 bCtor 必須都存在而且都不是 Object 構造函數的狀況下,aCtor 不等於 bCtor, 那這兩個對象就真的不相等啦 if (aCtor !== bCtor && !(isFunction(aCtor) && aCtor instanceof aCtor && isFunction(bCtor) && bCtor instanceof bCtor) && ('constructor' in a && 'constructor' in b)) { return false; } } // 下面還有好多判斷 }
如今終於能夠進入咱們期待已久的數組和對象的判斷,不過其實這個很簡單,就是遞歸遍歷一遍……
function deepEq(a, b) { // 再接着上面的內容 if (areArrays) { length = a.length; if (length !== b.length) return false; while (length--) { if (!eq(a[length], b[length])) return false; } } else { var keys = Object.keys(a), key; length = keys.length; if (Object.keys(b).length !== length) return false; while (length--) { key = keys[length]; if (!(b.hasOwnProperty(key) && eq(a[key], b[key]))) return false; } } return true; }
若是以爲這就結束了,簡直是太天真,由於最難的部分才終於要開始,這個問題就是循環引用!
舉個簡單的例子:
a = {abc: null}; b = {abc: null}; a.abc = a; b.abc = b; eq(a, b)
再複雜一點的,好比:
a = {foo: {b: {foo: {c: {foo: null}}}}}; b = {foo: {b: {foo: {c: {foo: null}}}}}; a.foo.b.foo.c.foo = a; b.foo.b.foo.c.foo = b; eq(a, b)
爲了給你們演示下循環引用,你們能夠把下面這段已經精簡過的代碼複製到瀏覽器中嘗試:
// demo var a, b; a = { foo: { b: { foo: { c: { foo: null } } } } }; b = { foo: { b: { foo: { c: { foo: null } } } } }; a.foo.b.foo.c.foo = a; b.foo.b.foo.c.foo = b; function eq(a, b, aStack, bStack) { if (typeof a == 'number') { return a === b; } return deepEq(a, b) } function deepEq(a, b) { var keys = Object.keys(a); var length = keys.length; var key; while (length--) { key = keys[length] // 這是爲了讓你看到代碼其實一直在執行 console.log(a[key], b[key]) if (!eq(a[key], b[key])) return false; } return true; } eq(a, b)
嗯,以上的代碼是死循環。
那麼,咱們又該如何解決這個問題呢?underscore 的思路是 eq 的時候,多傳遞兩個參數爲 aStack 和 bStack,用來儲存 a 和 b 遞歸比較過程當中的 a 和 b 的值,咋說的這麼繞口呢?
咱們直接看個精簡的例子:
var a, b;
a = { foo: { b: { foo: { c: { foo: null } } } } }; b = { foo: { b: { foo: { c: { foo: null } } } } }; a.foo.b.foo.c.foo = a; b.foo.b.foo.c.foo = b; function eq(a, b, aStack, bStack) { if (typeof a == 'number') { return a === b; } return deepEq(a, b, aStack, bStack) } function deepEq(a, b, aStack, bStack) { aStack = aStack || []; bStack = bStack || []; var length = aStack.length; while (length--) { if (aStack[length] === a) { return bStack[length] === b; } } aStack.push(a); bStack.push(b); var keys = Object.keys(a); var length = keys.length; var key; while (length--) { key = keys[length] console.log(a[key], b[key], aStack, bStack) if (!eq(a[key], b[key], aStack, bStack)) return false; } // aStack.pop(); // bStack.pop(); return true; } console.log(eq(a, b))
之因此註釋掉 aStack.pop()
和bStack.pop()
這兩句,是爲了方便你們查看 aStack bStack的值。
最終的代碼以下:
var toString = Object.prototype.toString; function isFunction(obj) { return toString.call(obj) === '[object Function]' } function eq(a, b, aStack, bStack) { // === 結果爲 true 的區別出 +0 和 -0 if (a === b) return a !== 0 || 1 / a === 1 / b; // typeof null 的結果爲 object ,這裏作判斷,是爲了讓有 null 的狀況儘早退出函數 if (a == null || b == null) return false; // 判斷 NaN if (a !== a) return b !== b; // 判斷參數 a 類型,若是是基本類型,在這裏能夠直接返回 false var type = typeof a; if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; // 更復雜的對象使用 deepEq 函數進行深度比較 return deepEq(a, b, aStack, bStack); }; function deepEq(a, b, aStack, bStack) { // a 和 b 的內部屬性 [[class]] 相同時 返回 true var className = toString.call(a); if (className !== toString.call(b)) return false; switch (className) { case '[object RegExp]': case '[object String]': return '' + a === '' + b; case '[object Number]': if (+a !== +a) return +b !== +b; return +a === 0 ? 1 / +a === 1 / b : +a === +b; case '[object Date]': case '[object Boolean]': return +a === +b; } var areArrays = className === '[object Array]'; // 不是數組 if (!areArrays) { // 過濾掉兩個函數的狀況 if (typeof a != 'object' || typeof b != 'object') return false; var aCtor = a.constructor, bCtor = b.constructor; // aCtor 和 bCtor 必須都存在而且都不是 Object 構造函數的狀況下,aCtor 不等於 bCtor, 那這兩個對象就真的不相等啦 if (aCtor !== bCtor && !(isFunction(aCtor) && aCtor instanceof aCtor && isFunction(bCtor) && bCtor instanceof bCtor) && ('constructor' in a && 'constructor' in b)) { return false; } } aStack = aStack || []; bStack = bStack || []; var length = aStack.length; // 檢查是否有循環引用的部分 while (length--) { if (aStack[length] === a) { return bStack[length] === b; } } aStack.push(a); bStack.push(b); // 數組判斷 if (areArrays) { length = a.length; if (length !== b.length) return false; while (length--) { if (!eq(a[length], b[length], aStack, bStack))