摘要:其他判斷構造函數實例我們看個例子雖然和都是,但是和屬于不同構造函數的實例,為了做出區分,我們認為是不同的對象。
前言JavaScript 專題系列第十二篇,講解如何判斷兩個參數是否相等
雖然標題寫的是如何判斷兩個對象相等,但本篇我們不僅僅判斷兩個對象相等,實際上,我們要做到的是如何判斷兩個參數相等,而這必然會涉及到多種類型的判斷。
相等什么是相等?在《JavaScript專題之去重》中,我們認為只要 === 的結果為 true,兩者就相等,然而今天我們重新定義相等:
我們認為:
NaN 和 NaN 是相等
[1] 和 [1] 是相等
{value: 1} 和 {value: 1} 是相等
不僅僅是這些長得一樣的,還有
1 和 new Number(1) 是相等
"Curly" 和 new String("Curly") 是相等
true 和 new Boolean(true) 是相等
更復雜的我們會在接下來的內容中看到。
目標我們的目標是寫一個 eq 函數用來判斷兩個參數是否相等,使用效果如下:
function eq(a, b) { ... } var a = [1]; var b = [1]; console.log(eq(a, b)) // true
在寫這個看似很簡單的函數之前,我們首先了解在一些簡單的情況下是如何判斷的?
+0 與 -0如果 a === b 的結果為 true, 那么 a 和 b 就是相等的嗎?一般情況下,當然是這樣的,但是有一個特殊的例子,就是 +0 和 -0。
JavaScript “處心積慮”的想抹平兩者的差異:
// 表現1 console.log(+0 === -0); // true // 表現2 (-0).toString() // "0" (+0).toString() // "0" // 表現3 -0 < +0 // false +0 < -0 // false
即便如此,兩者依然是不同的:
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)) // falseNaN
在本篇,我們認為 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)); // trueeq 函數
現在,我們已經可以去寫 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,實際上 基本類型和函數肯定是不會相等的,所以這樣做代碼又少,又可以讓一種情況更早退出。
String 對象現在我們開始寫 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)); // truedeepEq 函數
現在我們可以寫一點 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的值。
最終的 eq 函數最終的代碼如下:
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)) 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], aStack, bStack))) return false; } } aStack.pop(); bStack.pop(); return true; } console.log(eq(0, 0)) // true console.log(eq(0, -0)) // false console.log(eq(NaN, NaN)); // true console.log(eq(Number(NaN), Number(NaN))); // true console.log(eq("Curly", new String("Curly"))); // true console.log(eq([1], [1])); // true console.log(eq({ value: 1 }, { value: 1 })); // true 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; console.log(eq(a, b)) // true
真讓人感嘆一句:eq 不愧是 underscore 中實現代碼行數最多的函數了!
專題系列JavaScript專題系列目錄地址:https://github.com/mqyqingfeng/Blog。
JavaScript專題系列預計寫二十篇左右,主要研究日常開發中一些功能點的實現,比如防抖、節流、去重、類型判斷、拷貝、最值、扁平、柯里、遞歸、亂序、排序等,特點是研(chao)究(xi) underscore 和 jQuery 的實現方式。
如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/84789.html
摘要:專題系列共計篇,主要研究日常開發中一些功能點的實現,比如防抖節流去重類型判斷拷貝最值扁平柯里遞歸亂序排序等,特點是研究專題之函數組合專題系列第十六篇,講解函數組合,并且使用柯里化和函數組合實現模式需求我們需要寫一個函數,輸入,返回。 JavaScript 專題之從零實現 jQuery 的 extend JavaScritp 專題系列第七篇,講解如何從零實現一個 jQuery 的 ext...
摘要:寫在前面專題系列是我寫的第二個系列,第一個系列是深入系列。專題系列自月日發布第一篇文章,到月日發布最后一篇,感謝各位朋友的收藏點贊,鼓勵指正。 寫在前面 JavaScript 專題系列是我寫的第二個系列,第一個系列是 JavaScript 深入系列。 JavaScript 專題系列共計 20 篇,主要研究日常開發中一些功能點的實現,比如防抖、節流、去重、類型判斷、拷貝、最值、扁平、柯里...
摘要:專題系列第十八篇,講解遞歸和尾遞歸定義程序調用自身的編程技巧稱為遞歸。然而非尾調用函數,就會創建多個執行上下文壓入執行上下文棧。所以我們只用把階乘函數改造成一個尾遞歸形式,就可以避免創建那么多的執行上下文。 JavaScript 專題系列第十八篇,講解遞歸和尾遞歸 定義 程序調用自身的編程技巧稱為遞歸(recursion)。 階乘 以階乘為例: function factorial(n...
摘要:專題系列第三篇,講解各種數組去重方法,并且跟著寫一個前言數組去重方法老生常談,既然是常談,我也來談談。它類似于數組,但是成員的值都是唯一的,沒有重復的值。 JavaScript 專題系列第三篇,講解各種數組去重方法,并且跟著 underscore 寫一個 unique API 前言 數組去重方法老生常談,既然是常談,我也來談談。 雙層循環 也許我們首先想到的是使用 indexOf 來循...
閱讀 830·2021-09-22 15:18
閱讀 1181·2021-09-09 09:33
閱讀 2758·2019-08-30 10:56
閱讀 1184·2019-08-29 16:30
閱讀 1488·2019-08-29 13:02
閱讀 1458·2019-08-26 13:55
閱讀 1643·2019-08-26 13:41
閱讀 1941·2019-08-26 11:56