JavaScript 專題系列第十二篇,講解如何判斷兩個參數是否相等git
雖然標題寫的是如何判斷兩個對象相等,但本篇咱們不只僅判斷兩個對象相等,實際上,咱們要作到的是如何判斷兩個參數相等,而這必然會涉及到多種類型的判斷。github
什麼是相等?在《JavaScript專題之去重》中,咱們認爲只要 ===
的結果爲 true,二者就相等,然而今天咱們從新定義相等:編程
咱們認爲:數組
不只僅是這些長得同樣的,還有瀏覽器
更復雜的咱們會在接下來的內容中看到。編程語言
咱們的目標是寫一個 eq 函數用來判斷兩個參數是否相等,使用效果以下:函數
function eq(a, b) { ... }
var a = [1];
var b = [1];
console.log(eq(a, b)) // true複製代碼
在寫這個看似很簡單的函數以前,咱們首先了解在一些簡單的狀況下是如何判斷的?學習
若是 a === b 的結果爲 true, 那麼 a 和 b 就是相等的嗎?通常狀況下,固然是這樣的,可是有一個特殊的例子,就是 +0 和 -0。ui
JavaScript 「處心積慮」的想抹平二者的差別:this
// 表現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)) // 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)) 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專題系列目錄地址:github.com/mqyqingfeng…。
JavaScript專題系列預計寫二十篇左右,主要研究平常開發中一些功能點的實現,好比防抖、節流、去重、類型判斷、拷貝、最值、扁平、柯里、遞歸、亂序、排序等,特色是研(chao)究(xi) underscore 和 jQuery 的實現方式。
若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。