javascript
JavaScript 专题之如何判断两个对象相等
JavaScript 專(zhuān)題系列第十二篇,講解如何判斷兩個(gè)參數(shù)是否相等
前言
雖然標(biāo)題寫(xiě)的是如何判斷兩個(gè)對(duì)象相等,但本篇我們不僅僅判斷兩個(gè)對(duì)象相等,實(shí)際上,我們要做到的是如何判斷兩個(gè)參數(shù)相等,而這必然會(huì)涉及到多種類(lèi)型的判斷。
相等
什么是相等?在《JavaScript專(zhuān)題之去重》中,我們認(rèn)為只要 === 的結(jié)果為 true,兩者就相等,然而今天我們重新定義相等:
我們認(rèn)為:
不僅僅是這些長(zhǎng)得一樣的,還有
更復(fù)雜的我們會(huì)在接下來(lái)的內(nèi)容中看到。
目標(biāo)
我們的目標(biāo)是寫(xiě)一個(gè) eq 函數(shù)用來(lái)判斷兩個(gè)參數(shù)是否相等,使用效果如下:
function eq(a, b) { ... }var a = [1]; var b = [1]; console.log(eq(a, b)) // true復(fù)制代碼在寫(xiě)這個(gè)看似很簡(jiǎn)單的函數(shù)之前,我們首先了解在一些簡(jiǎn)單的情況下是如何判斷的?
+0 與 -0
如果 a === b 的結(jié)果為 true, 那么 a 和 b 就是相等的嗎?一般情況下,當(dāng)然是這樣的,但是有一個(gè)特殊的例子,就是 +0 和 -0。
JavaScript “處心積慮”的想抹平兩者的差異:
// 表現(xiàn)1 console.log(+0 === -0); // true// 表現(xiàn)2 (-0).toString() // '0' (+0).toString() // '0'// 表現(xiàn)3 -0 < +0 // false +0 < -0 // false復(fù)制代碼即便如此,兩者依然是不同的:
1 / +0 // Infinity 1 / -0 // -Infinity1 / +0 === 1 / -0 // false復(fù)制代碼也許你會(huì)好奇為什么要有 +0 和 -0 呢?
這是因?yàn)?JavaScript 采用了IEEE_754 浮點(diǎn)數(shù)表示法(幾乎所有現(xiàn)代編程語(yǔ)言所采用),這是一種二進(jìn)制表示法,按照這個(gè)標(biāo)準(zhǔn),最高位是符號(hào)位(0 代表正,1 代表負(fù)),剩下的用于表示大小。而對(duì)于零這個(gè)邊界值 ,1000(-0) 和 0000(0)都是表示 0 ,這才有了正負(fù)零的區(qū)別。
也許你會(huì)好奇什么時(shí)候會(huì)產(chǎn)生 -0 呢?
Math.round(-0.1) // -0復(fù)制代碼那么我們又該如何在 === 結(jié)果為 true 的時(shí)候,區(qū)別 0 和 -0 得出正確的結(jié)果呢?我們可以這樣做:
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復(fù)制代碼NaN
在本篇,我們認(rèn)為 NaN 和 NaN 是相等的,那又該如何判斷出 NaN 呢?
console.log(NaN === NaN); // false復(fù)制代碼利用 NaN 不等于自身的特性,我們可以區(qū)別出 NaN,那么這個(gè) eq 函數(shù)又該怎么寫(xiě)呢?
function eq(a, b) {if (a !== a) return b !== b; }console.log(eq(NaN, NaN)); // true復(fù)制代碼eq 函數(shù)
現(xiàn)在,我們已經(jīng)可以去寫(xiě) eq 函數(shù)的第一版了。
// eq 第一版 // 用來(lái)過(guò)濾掉簡(jiǎn)單的類(lèi)型比較,復(fù)雜的對(duì)象使用 deepEq 函數(shù)進(jìn)行處理 function eq(a, b) {// === 結(jié)果為 true 的區(qū)別出 +0 和 -0if (a === b) return a !== 0 || 1 / a === 1 / b;// typeof null 的結(jié)果為 object ,這里做判斷,是為了讓有 null 的情況盡早退出函數(shù)if (a == null || b == null) return false;// 判斷 NaNif (a !== a) return b !== b;// 判斷參數(shù) a 類(lèi)型,如果是基本類(lèi)型,在這里可以直接返回 falsevar type = typeof a;if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;// 更復(fù)雜的對(duì)象使用 deepEq 函數(shù)進(jìn)行深度比較return deepEq(a, b); };復(fù)制代碼也許你會(huì)好奇是不是少了一個(gè) typeof b !== function?
試想如果我們添加上了這句,當(dāng) a 是基本類(lèi)型,而 b 是函數(shù)的時(shí)候,就會(huì)進(jìn)入 deepEq 函數(shù),而去掉這一句,就會(huì)進(jìn)入直接進(jìn)入 false,實(shí)際上 基本類(lèi)型和函數(shù)肯定是不會(huì)相等的,所以這樣做代碼又少,又可以讓一種情況更早退出。
String 對(duì)象
現(xiàn)在我們開(kāi)始寫(xiě) deepEq 函數(shù),一個(gè)要處理的重大難題就是 'Curly' 和 new String('Curly') 如何判斷成相等?
兩者的類(lèi)型都不一樣吶!不信我們看 typeof 的操作結(jié)果:
console.log(typeof 'Curly'); // string console.log(typeof new String('Curly')); // object復(fù)制代碼可是我們?cè)凇禞avaScript專(zhuān)題之類(lèi)型判斷上》中還學(xué)習(xí)過(guò)更多的方法判斷類(lèi)型,比如 Object.prototype.toString:
var toString = Object.prototype.toString; toString.call('Curly'); // "[object String]" toString.call(new String('Curly')); // "[object String]"復(fù)制代碼神奇的是使用 toString 方法兩者判斷的結(jié)果卻是一致的,可是就算知道了這一點(diǎn),還是不知道如何判斷字符串和字符串包裝對(duì)象是相等的呢?
那我們利用隱式類(lèi)型轉(zhuǎn)換呢?
console.log('Curly' + '' === new String('Curly') + ''); // true復(fù)制代碼看來(lái)我們已經(jīng)有了思路:如果 a 和 b 的 Object.prototype.toString的結(jié)果一致,并且都是"[object String]",那我們就使用 '' + a === '' + b 進(jìn)行判斷。
可是不止有 String 對(duì)象吶,Boolean、Number、RegExp、Date呢?
更多對(duì)象
跟 String 同樣的思路,利用隱式類(lèi)型轉(zhuǎn)換。
Boolean
var a = true; var b = new Boolean(true);console.log(+a === +b) // true復(fù)制代碼Date
var a = new Date(2009, 9, 25); var b = new Date(2009, 9, 25);console.log(+a === +b) // true復(fù)制代碼RegExp
var a = /a/i; var b = new RegExp(/a/i);console.log('' + a === '' + b) // true復(fù)制代碼Number
var a = 1; var b = new Number(1);console.log(+a === +b) // true復(fù)制代碼嗯哼?你確定 Number 能這么簡(jiǎn)單的判斷?
var a = Number(NaN); var b = Number(NaN);console.log(+a === +b); // false復(fù)制代碼可是 a 和 b 應(yīng)該被判斷成 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復(fù)制代碼deepEq 函數(shù)
現(xiàn)在我們可以寫(xiě)一點(diǎn) deepEq 函數(shù)了。
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;}// 其他判斷 }復(fù)制代碼構(gòu)造函數(shù)實(shí)例
我們看個(gè)例子:
function Person() {this.name = name; }function Animal() {this.name = name }var person = new Person('Kevin'); var animal = new Animal('Kevin');eq(person, animal) // ???復(fù)制代碼雖然 person 和 animal 都是 {name: 'Kevin'},但是 person 和 animal 屬于不同構(gòu)造函數(shù)的實(shí)例,為了做出區(qū)分,我們認(rèn)為是不同的對(duì)象。
如果兩個(gè)對(duì)象所屬的構(gòu)造函數(shù)對(duì)象不同,兩個(gè)對(duì)象就一定不相等嗎?
并不一定,我們?cè)倥e個(gè)例子:
var attrs = Object.create(null); attrs.name = "Bob"; eq(attrs, {name: "Bob"}); // ???復(fù)制代碼盡管 attrs 沒(méi)有原型,{name: "Bob"} 的構(gòu)造函數(shù)是 Object,但是在實(shí)際應(yīng)用中,只要他們有著相同的鍵值對(duì),我們依然認(rèn)為是相等。
從函數(shù)設(shè)計(jì)的角度來(lái)看,我們不應(yīng)該讓他們相等,但是從實(shí)踐的角度,我們讓他們相等,所以相等就是一件如此隨意的事情嗎?!對(duì)啊,我也在想:undersocre,你怎么能如此隨意呢!!!
哎,吐槽完了,我們還是要接著寫(xiě)這個(gè)相等函數(shù),我們可以先做個(gè)判斷,對(duì)于不同構(gòu)造函數(shù)下的實(shí)例直接返回 false。
function isFunction(obj) {return toString.call(obj) === '[object Function]' }function deepEq(a, b) {// 接著上面的內(nèi)容var areArrays = className === '[object Array]';// 不是數(shù)組if (!areArrays) {// 過(guò)濾掉兩個(gè)函數(shù)的情況if (typeof a != 'object' || typeof b != 'object') return false;var aCtor = a.constructor, bCtor = b.constructor;// aCtor 和 bCtor 必須都存在并且都不是 Object 構(gòu)造函數(shù)的情況下,aCtor 不等于 bCtor, 那這兩個(gè)對(duì)象就真的不相等啦if (aCtor == bCtor && !(isFunction(aCtor) && aCtor instanceof aCtor && isFunction(bCtor) && bCtor instanceof bCtor) && ('constructor' in a && 'constructor' in b)) {return false;}}// 下面還有好多判斷 }復(fù)制代碼數(shù)組相等
現(xiàn)在終于可以進(jìn)入我們期待已久的數(shù)組和對(duì)象的判斷,不過(guò)其實(shí)這個(gè)很簡(jiǎn)單,就是遞歸遍歷一遍……
function deepEq(a, b) {// 再接著上面的內(nèi)容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;}復(fù)制代碼循環(huán)引用
如果覺(jué)得這就結(jié)束了,簡(jiǎn)直是太天真,因?yàn)樽铍y的部分才終于要開(kāi)始,這個(gè)問(wèn)題就是循環(huán)引用!
舉個(gè)簡(jiǎn)單的例子:
a = {abc: null}; b = {abc: null}; a.abc = a; b.abc = b;eq(a, b)復(fù)制代碼再?gòu)?fù)雜一點(diǎn)的,比如:
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)復(fù)制代碼為了給大家演示下循環(huán)引用,大家可以把下面這段已經(jīng)精簡(jiǎn)過(guò)的代碼復(fù)制到瀏覽器中嘗試:
// 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]// 這是為了讓你看到代碼其實(shí)一直在執(zhí)行console.log(a[key], b[key])if (!eq(a[key], b[key])) return false;}return true;}eq(a, b)復(fù)制代碼嗯,以上的代碼是死循環(huán)。
那么,我們又該如何解決這個(gè)問(wèn)題呢?underscore 的思路是 eq 的時(shí)候,多傳遞兩個(gè)參數(shù)為 aStack 和 bStack,用來(lái)儲(chǔ)存 a 和 b 遞歸比較過(guò)程中的 a 和 b 的值,咋說(shuō)的這么繞口呢?
我們直接看個(gè)精簡(jiǎn)的例子:
之所以注釋掉 aStack.pop()和bStack.pop()這兩句,是為了方便大家查看 aStack bStack的值。
最終的 eq 函數(shù)
最終的代碼如下:
var toString = Object.prototype.toString;function isFunction(obj) {return toString.call(obj) === '[object Function]' }function eq(a, b, aStack, bStack) {// === 結(jié)果為 true 的區(qū)別出 +0 和 -0if (a === b) return a !== 0 || 1 / a === 1 / b;// typeof null 的結(jié)果為 object ,這里做判斷,是為了讓有 null 的情況盡早退出函數(shù)if (a == null || b == null) return false;// 判斷 NaNif (a !== a) return b !== b;// 判斷參數(shù) a 類(lèi)型,如果是基本類(lèi)型,在這里可以直接返回 falsevar type = typeof a;if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;// 更復(fù)雜的對(duì)象使用 deepEq 函數(shù)進(jìn)行深度比較return deepEq(a, b, aStack, bStack); };function deepEq(a, b, aStack, bStack) {// a 和 b 的內(nèi)部屬性 [[class]] 相同時(shí) 返回 truevar 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]';// 不是數(shù)組if (!areArrays) {// 過(guò)濾掉兩個(gè)函數(shù)的情況if (typeof a != 'object' || typeof b != 'object') return false;var aCtor = a.constructor,bCtor = b.constructor;// aCtor 和 bCtor 必須都存在并且都不是 Object 構(gòu)造函數(shù)的情況下,aCtor 不等于 bCtor, 那這兩個(gè)對(duì)象就真的不相等啦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;// 檢查是否有循環(huán)引用的部分while (length--) {if (aStack[length] === a) {return bStack[length] === b;}}aStack.push(a);bStack.push(b);// 數(shù)組判斷if (areArrays) {length = a.length;if (length !== b.length) return false;while (length--) {if (!eq(a[length], b[length], aStack, bStack)) return false;}}// 對(duì)象判斷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], aStack, bStack))) return false;}}aStack.pop();bStack.pop();return true;}console.log(eq(0, 0)) // true console.log(eq(0, -0)) // falseconsole.log(eq(NaN, NaN)); // true console.log(eq(Number(NaN), Number(NaN))); // trueconsole.log(eq('Curly', new String('Curly'))); // trueconsole.log(eq([1], [1])); // true console.log(eq({ value: 1 }, { value: 1 })); // truevar 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;console.log(eq(a, b)) // true復(fù)制代碼真讓人感嘆一句:eq 不愧是 underscore 中實(shí)現(xiàn)代碼行數(shù)最多的函數(shù)了!
專(zhuān)題系列
JavaScript專(zhuān)題系列目錄地址:github.com/mqyqingfeng…。
JavaScript專(zhuān)題系列預(yù)計(jì)寫(xiě)二十篇左右,主要研究日常開(kāi)發(fā)中一些功能點(diǎn)的實(shí)現(xiàn),比如防抖、節(jié)流、去重、類(lèi)型判斷、拷貝、最值、扁平、柯里、遞歸、亂序、排序等,特點(diǎn)是研(chao)究(xi) underscore 和 jQuery 的實(shí)現(xiàn)方式。
如果有錯(cuò)誤或者不嚴(yán)謹(jǐn)?shù)牡胤?#xff0c;請(qǐng)務(wù)必給予指正,十分感謝。如果喜歡或者有所啟發(fā),歡迎 star,對(duì)作者也是一種鼓勵(lì)。
總結(jié)
以上是生活随笔為你收集整理的JavaScript 专题之如何判断两个对象相等的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: javascript正则表达式总结(te
- 下一篇: gradle idea java ssm