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:
- NaN and NaN are equal
- [1] and [1] are equal
- {value: 1} and {value: 1} are equal
Not only do they look the same, but also
- 1 and new Number(1) are equal
- 'Curly'and new String('Curly') are equal.
- 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.