定义
在开始之前,我们先定义一下什么叫相等。
- NaN 和 NaN 是相等
 - [1] 和 [1] 是相等
 - {value: 1} 和 {value: 1} 是相等
 
另外
- 1 和 new Number(1) 是相等
 - ‘Curly’ 和 new String(‘Curly’) 是相等
 - true 和 new Boolean(true) 是相等
 
一些有趣的现象
a === b 是 true 的话,a 和 b 就一定相等吗?一般情况下是这样的,但有个例外。
+0 和 -0
console.log(+0 === -0);(+0).tostring() // 0(-0).toString() // 0-0 < +0 // false+0 < -0 // false
即使如此两者还是不同
1 / +0 // Infinity1 / -0 // -Infiity1 / +0 === -1 / 0 //false
为什么会有 +0 和 -0 了?那是因为 Javascript 采用了 IEEE_754 浮点数表法(这是一种二进制表示法,最高位是符号位(0 代表 + ,1 代表 -)),所以零有两个值(1000) 和 (0000)。
那如何判断出来了?可以利用 1 / + 0 === -1 / 0 => false 这特性。
function eq(a, b) {if(a === b) return a !== 0 || 1 / a === 1 / b;return false}
NaN
虽然 NaN === NaN => false 但是我们认为 NaN 应该是等于自身的,我们可以利用他这个特性判断出来。
function eq(a, b) {if(a !== a) return b !== b;}
eq 函数(第一版)
function eq(a, b) {// 判断一些基本类型如: 1 === 1,true === true, 'str' === 'str', null === null// a !== 0 很简单就是用来判断不是 0 的情况// 我们认为 -0 !== +0,因为 1 / -0 => -Infinity,而 1 / +0 => Infinity,所以 1 / -0 === 1 / +0 => fasleif (a === b) return a !== 0 || 1 / a === 1 / b;//因为 null !== 任意类型(除非是自身,但是上面的 if 已经判断出来了)if (a === null || b === null) return false;// 这个专门用来判断 NaN,因为 NaN !== NaNif (a !== a) return b !== b;//来到这里的情况也就几种,// 1. a, b 都为基本类型但不相等分两种情况:同类型不相等(如:1 !== 2,true !== false,'str1' !== 'str2'),或者不同类型(1 !== true,'str' !== false)// 2. a, b 有一个是对象(object),另外一个是基本类型(如:a = 1,b = new Number(1); a = true, b = new Boolean(true); ....)// 3. a,b 都是对象(function,array, object)// 这里的 if 是判断第 1 种情况// 你可能会问 if 语句是不是差一个判断:type b !== 'function', 其实不是的因为如何假设 b 是一个函数,a 经过前两个 typeof 判断肯定是一个基本类型,但是基本类型肯定不等于函数啊。// 所以为了让这种情况早点判断出来所以就不写 type b !== 'function'const type = typeof a;if (type !== "function" && type !== "object" && typeof b !== "object") return false;// 把第 2,3 种情况放到 deepEq 函数来判断return deeepEq(a, b);}
string
在开始写 deepEq 函数之前,我们怎么判断如:’Curly’ 和 new String(‘Curly’) 这种情况了?
console.log(typeof 'Curly'); // stringconsole.log(typeof new String('Curly')); // object
一个是字符创,一个是对象。怎么办了?可以利用 Object.prototype.toString 方法。
console.log(Object.prototype.toString.call('Curly')); // "[object String]"console.log(Object.prototype.toString.call(new String('Curly'))); // "[object String]"
只到调用 toString 方法结果就是一样的。
还可以利用隐式类型转换。
console.log('Curly' + '' === new String('Curly') + '') // true
思路:如果 a 和 b 的 toString 的结果是一致,并且都是 “[object String]” 的话,那么可以利用 ‘’ + a === ‘’ + b 来判断。
更多对象
Boolean
const a = true;const b = new Boolean(true);console.log(+a === +b) // true
Date
const a = new Date(2009, 9, 25);const b = new Date(2009, 9, 25);console.log(+a === +b) // true
RegExp
const a = /a/i;const b = new RegExp(/a/i);console.log('' + a === '' + b) // true
Number
const a = 1;const b = new Number(1);console.log(+a === +b) // true
Number 就这么容易判断出来吗?
const a = Number(NaN);const b = Number(NaN);console.log(+a === +b); // false
期望结果是 true,但是却是 false。
修改成这样
const a = Number(NaN);const b = Number(NaN);function eq() {// 判断 Number(NaN) Object(NaN) 等情况if (+a !== +a) return +b !== +b;// 其他判断 ...}console.log(eq(a, b)); // true
deepEq 函数(第一版)
// 原理(以 string 为例)// var toString = Object.prototype.toString;// const a = 'Curly', b = new String('Curly')// toString.call(a); // "[object String]"// toString.call(b); // "[object String]"// "" + a === "" + b => true//下面是判断第 2 种情况var toString = Object.prototype.toString;function deepEq(a, b) {const 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]":// 用来判断 a = new Number(NaN),b = new Number(NaN)if (+a !== +a) return +b !== +b;// 如果 a 是 +0,-0,0 (注意:+a 只是把 a 变成数字,不会改变符号 +(-0) => -0)则进入第二个判断,否则进入第三个判断。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}const person = new Person('David');const animal = new Animal('David');
虽然都是 {name: "David"} 但是我们认为这两个对象是不想的,因为分别属于不用的构造函数实例。
但是如何两个对象所属的构造函数所属不同,两个对象就一定不相等吗?
不一定,举个例子
const attrs = Object.create(null);attr.name = 'Bob';eq(attrs, {name: 'Bob'}); // ???
虽然 attrs 没有原型, {name: "Bob"} 的原型是 Object ,但是在实际应用中,只要他们有着相同的键值对,就认为是相等。
继续写判断,对于不同的构造函数下的实例直接返回 false。
// 现在只剩下第三种情况了,a,b 都为对象(function,array,object)function isFunction(obj) {return toString.call(obj) === "[object Function]";}function deepEq(a, b) {// 接着上面的内容var areArrays = className === "[object Array]";// 不是数组,只能是(object,function)if (!areArrays) {// 过滤掉两个函数的情况(只要有一个是函数都不可能相等)if (typeof a !== "object" || typeof b !== "object") return false;// a,b 都为对象(object)var aCtor = a.constructor,bCtor = b.constructor;// aCtor 和 bCtor 必须都存在并且都不是 Object 构造函数的情况下,aCtor 不等于 bCtor, 那这两个对象就真的不相等啦// 这个 if 可以过滤掉 第1种情况if (aCtor !== bCtor &&!(isFunction(aCtor) && aCtor instanceof aCtor && isFunction(bCtor) && bCtor instanceof bCtor) &&"constructor" in a &&"constructor" in b) {return false;}}// 下面还有好多判断}// a,b 可能的情况function Person(name) {this.name = name;}function Animal(name) {this.name = name;}// 第1种a = new Person("test");b = new Animal("test");// 第2种a = Object.create(null);a.test = 1;b = { test: 1 };// 第3种a = { test: 1 };b = { test: 2 };// 第4种a = [1,2,3];b = [1,2,3];
剩下的 2,3,4 留到下面判断。
数组相等
就是递归遍历一遍,代码。
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 {let 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;}
循环引用
你以就这么简单,那太天真了!!!最难的问题来了:循环引用。
例子
const a = {abc: null};const b = {abc: null};a.abc = a;b.abc = b;eq(a, b)
再复杂一点。
const a = {foo: {b: {foo: {c: {foo: null}}}}};const b = {foo: {b: {foo: {c: {foo: null}}}}};a.foo.b.foo.c.foo = a;b.foo.b.foo.c.foo = b;eq(a, b)
为了演示,写一个精简后的代码。
const 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) {let keys = Object.keys(a);let length = keys.length;let 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 的值,咋说的这么绕口呢?
例子
const 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 和 -0if (a === b) return a !== 0 || 1 / a === 1 / b;// typeof null 的结果为 object ,这里做判断,是为了让有 null 的情况尽早退出函数if (a == null || b == null) return false;// 判断 NaNif (a !== a) return b !== b;// 判断参数 a 类型,如果是基本类型,在这里可以直接返回 falsevar 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]] 相同时 返回 truevar 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)) // trueconsole.log(eq(0, -0)) // falseconsole.log(eq(NaN, NaN)); // trueconsole.log(eq(Number(NaN), Number(NaN))); // trueconsole.log(eq('Curly', new String('Curly'))); // trueconsole.log(eq([1], [1])); // trueconsole.log(eq({ value: 1 }, { value: 1 })); // truevar 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
参考:
