JavaScript 專題之如何判斷兩個對象相等

JavaScript 專題系列第十二篇,講解如何判斷兩個參數是否相等git

前言

雖然標題寫的是如何判斷兩個對象相等,但本篇咱們不只僅判斷兩個對象相等,實際上,咱們要作到的是如何判斷兩個參數相等,而這必然會涉及到多種類型的判斷。github

相等

什麼是相等?在《JavaScript專題之去重》中,咱們認爲只要 === 的結果爲 true,二者就相等,然而今天咱們從新定義相等:編程

咱們認爲:數組

  1. NaN 和 NaN 是相等
  2. [1] 和 [1] 是相等
  3. {value: 1} 和 {value: 1} 是相等

不只僅是這些長得同樣的,還有瀏覽器

  1. 1 和 new Number(1) 是相等
  2. 'Curly' 和 new String('Curly') 是相等
  3. 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。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 是相等的,那又該如何判斷出 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 函數的初版了。

// 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)); // true複製代碼

deepEq 函數

如今咱們能夠寫一點 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) // ???複製代碼

雖然 personanimal 都是 {name: 'Kevin'},可是 personanimal 屬於不一樣構造函數的實例,爲了作出區分,咱們認爲是不一樣的對象。

若是兩個對象所屬的構造函數對象不一樣,兩個對象就必定不相等嗎?

並不必定,咱們再舉個例子:

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專題系列目錄地址:github.com/mqyqingfeng…

JavaScript專題系列預計寫二十篇左右,主要研究平常開發中一些功能點的實現,好比防抖、節流、去重、類型判斷、拷貝、最值、扁平、柯里、遞歸、亂序、排序等,特色是研(chao)究(xi) underscore 和 jQuery 的實現方式。

若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。

相關文章
相關標籤/搜索