How to Judge the Equality of Two Objects in the JavaScript Project

Posted by JDcrack on Thu, 06 Jun 2019 21:41:11 +0200

The twelfth article in the JavaScript series explains how to determine whether two parameters are equal

Preface

Although the title describes how to judge the equality of two objects, in this article we not only judge the equality of two objects, but in fact, what we want to do is how to judge the equality of two parameters, which will inevitably involve a variety of types of judgments.

Equal

What is equality? stay De-duplication of JavaScript Theme In our opinion, as long as the result of === is true, the two are equal, but today we redefine equality:

We believe that:

  1. NaN and NaN are equal
  2. [1] and [1] are equal
  3. {value: 1} and {value: 1} are equal

Not only do they look the same, but also

  1. 1 and new Number(1) are equal
  2. 'Curly'and new String('Curly') are equal.
  3. True and new Boolean(true) are equal

More complex, we'll see in the next section.

target

Our goal is to write an eq function to determine whether the two parameters are equal. The effect is as follows:

function eq(a, b) { ... }

var a = [1];
var b = [1];
console.log(eq(a, b)) // true

Before writing this seemingly simple function, let's first understand how to judge in some simple cases.

+ 0 and - 0

If the result of a=== b is true, are a and b equal? Usually, of course, that's true, but there's a special example, which is + 0 and - 0.

JavaScript is trying to smooth out the difference between the two.

// Performance 1
console.log(+0 === -0); // true

// Performance 2
(-0).toString() // '0'
(+0).toString() // '0'

// Performance 3
-0 < +0 // false
+0 < -0 // false

Even so, the two are still different:

1 / +0 // Infinity
1 / -0 // -Infinity

1 / +0 === 1 / -0 // false

Maybe you wonder why there are + 0 and - 0?

This is because JavaScript uses the IEEE_754 floating-point representation (adopted by almost all modern programming languages), which is a binary representation. According to this standard, the highest bit is the symbol bit (0 for positive, 1 for negative), and the rest is used to represent size. For the boundary value of zero, 1000 (-0) and 0000 (-0) both represent zero, which makes the difference between positive and negative zero.

Maybe you'll be curious when - 0 will happen?

Math.round(-0.1) // -0

So how do we distinguish 0 from - 0 to get the right result when the result is true? We can do this:

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

In this article, we think that NaN and NaN are equal, so how to judge NaN?

console.log(NaN === NaN); // false

NaN is not equal to its own characteristics, we can distinguish NaN, then how to write this eq function?

function eq(a, b) {
    if (a !== a) return b !== b;
}

console.log(eq(NaN, NaN)); // true

eq function

Now we can write the first edition of eq function.

// eq First Edition
// Used to filter out simple type comparisons, complex objects are processed using the deepEq function
function eq(a, b) {

    // === The result is true distinguishing between + 0 and - 0
    if (a === b) return a !== 0 || 1 / a === 1 / b;

    // The result of typeof null is object. The reason to judge here is to let null exit the function as soon as possible.
    if (a == null || b == null) return false;

    // Judging NaN
    if (a !== a) return b !== b;

    // Determine the parameter a type, if it is a basic type, where you can return false directly
    var type = typeof a;
    if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;

    // Deep comparisons of more complex objects using the deepEq function
    return deepEq(a, b);
};

Maybe you wonder if there's a missing typeof b!== function??

Imagine if we add this sentence, when a is the basic type and b is the function, we will enter the deepEq function. If we remove this sentence, we will enter the direct false. In fact, the basic type and function will not be equal, so there is less code to do so, and we can let a situation exit earlier.

String object

Now let's start writing the deepEq function. One of the major problems we have to deal with is how to judge the equality of'Curly'and'Curly'.

Both types are different! Believe it or not, let's look at the results of typeof's operation:

console.log(typeof 'Curly'); // string
console.log(typeof new String('Curly')); // object

But we are Judging the Types of JavaScript Themes I also learned more ways to determine types, such as Object.prototype.toString:

var toString = Object.prototype.toString;
toString.call('Curly'); // "[object String]"
toString.call(new String('Curly')); // "[object String]"

Amazingly, the toString method yields the same results, but even if you know this, you still don't know how to judge that strings and string wrappers are equal.

What about implicit type conversion?

console.log('Curly' + '' === new String('Curly') + ''); // true

It seems that we have a way of thinking: if the results of Object.prototype.toString for a and B are identical, and both are "[object String]", then we use'+a==='+b to judge.

But there are more than String objects, Boolean, Number, RegExp, Date?

More objects

In the same way as String, implicit type conversion is used.

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

Uh huh? Are you sure Number can make such a simple judgement?

var a = Number(NaN);
var b = Number(NaN);

console.log(+a === +b); // false

But a and b should be judged to be true.~

So let's change it to this:

var a = Number(NaN);
var b = Number(NaN);

function eq() {
    // Judging Number(NaN) Object(NaN) and so on
    if (+a !== +a) return +b !== +b;
    // Other judgments...
}

console.log(eq(a, b)); // true

DepEq function

Now we can write a little deepEq function.

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;
    }

    // Other judgments
}

Constructor example

Let's take an example:

function Person() {
    this.name = name;
}

function Animal() {
    this.name = name
}

var person = new Person('Kevin');
var animal = new Animal('Kevin');

eq(person, animal) // ???

Although both person and animal are {name:'Kevin'}, person and animal are examples of different constructors. In order to distinguish, we think they are different objects.

If two objects belong to different constructor objects, are they necessarily different?

Not necessarily. Let's take another example:

var attrs = Object.create(null);
attrs.name = "Bob";
eq(attrs, {name: "Bob"}); // ???

Although attrs have no prototype and the constructor of {name: `Bob'} is Object, in practical application, we still consider them equal as long as they have the same key-value pair.

From the point of view of function design, we should not make them equal, but from the point of view of practice, we make them equal, so equality is such a random thing?! Yes, I'm also thinking: undersocre, how can you be so casual!!!

Well, after Tucao is finished, we still have to write this equality function. We can make a judgement first and return false directly to the instances under different constructors.

function isFunction(obj) {
    return toString.call(obj) === '[object Function]'
}

function deepEq(a, b) {
    // Next to the above
    var areArrays = className === '[object Array]';
    // Not an array
    if (!areArrays) {
        // The case of filtering out two functions
        if (typeof a != 'object' || typeof b != 'object') return false;

        var aCtor = a.constructor, bCtor = b.constructor;
        // If both aCtor and bCtor must exist and are not Object constructors, aCtor is not equal to bCtor, then these two objects are really not equal.
        if (aCtor == bCtor && !(isFunction(aCtor) && aCtor instanceof aCtor && isFunction(bCtor) && bCtor instanceof bCtor) && ('constructor' in a && 'constructor' in b)) {
            return false;
        }
    }

    // There's a lot more to judge.
}

Array equality

Now we can finally enter the long-awaited array and object judgment, but in fact, this is very simple, is recursive traversal...

function deepEq(a, b) {
    // Then go on to the above.
    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;

}

Circular reference

It's naive to think that this is over, because the hardest part is finally about to start, and the problem is circular quotation! ___________

For a simple example:

a = {abc: null};
b = {abc: null};
a.abc = a;
b.abc = b;

eq(a, b)

More complicated, for example:

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)

In order to show you the circular reference, you can copy the following simplified code into the browser to try:

// 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]

        // This is to show you that the code is actually executing all the time.
        console.log(a[key], b[key])

        if (!eq(a[key], b[key])) return false;
    }

    return true;

}

eq(a, b)

Well, the above code is a dead loop.

So how can we solve this problem? The idea of underscore is eq, passing two more parameters, aStack and bStack, to store the values of a and b in the process of a and b recursive comparison. What's the twist?
Let's look directly at a simplified example:

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))

The reason why the two sentences of aStack.pop() and bStack.pop() are commented out is to make it easy for you to see the value of aStack bStack.

The final eq function

The final code is as follows:

var toString = Object.prototype.toString;

function isFunction(obj) {
    return toString.call(obj) === '[object Function]'
}

function eq(a, b, aStack, bStack) {

    // === The result is true distinguishing between + 0 and - 0
    if (a === b) return a !== 0 || 1 / a === 1 / b;

    // The result of typeof null is object. The reason to judge here is to let null exit the function as soon as possible.
    if (a == null || b == null) return false;

    // Judging NaN
    if (a !== a) return b !== b;

    // Determine the parameter a type, if it is a basic type, where you can return false directly
    var type = typeof a;
    if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;

    // Deep comparisons of more complex objects using the deepEq function
    return deepEq(a, b, aStack, bStack);
};

function deepEq(a, b, aStack, bStack) {

    // Returns true when the internal attributes of a and b [[class]] are the same
    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]';
    // Not an array
    if (!areArrays) {
        // The case of filtering out two functions
        if (typeof a != 'object' || typeof b != 'object') return false;

        var aCtor = a.constructor,
            bCtor = b.constructor;
        // If both aCtor and bCtor must exist and are not Object constructors, aCtor is not equal to bCtor, then these two objects are really not equal.
        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;

    // Check if there is a circular reference part
    while (length--) {
        if (aStack[length] === a) {
            return bStack[length] === b;
        }
    }

    aStack.push(a);
    bStack.push(b);

    // Array Judgment
    if (areArrays) {

        length = a.length;
        if (length !== b.length) return false;

        while (length--) {
            if (!eq(a[length], b[length], aStack, bStack)) return false;
        }
    }
    // Object Judgment
    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

It's amazing to say that eq is the function that implements the largest number of lines of code in underscore.

Thematic series

JavaScript Thematic Series Directory Address: https://github.com/mqyqingfeng/Blog.

The JavaScript project series is expected to write about 20 articles. It mainly studies the implementation of some functional points in daily development, such as shake-proof, throttling, de-duplication, type judgment, copy, maximum value, flat, curry, recursion, disorder, sorting and so on. It is characterized by the implementation of Chao research (xi) underscore and jQuery.

If there are any mistakes or inaccuracies, please be sure to correct them. Thank you very much. If you like it or are inspired by it, welcome star, which is also an encouragement to the author.

Topics: Javascript Programming REST less