定义
在开始之前,我们先定义一下什么叫相等。
- 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 // Infinity
1 / -0 // -Infiity
1 / +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 => fasle
if (a === b) return a !== 0 || 1 / a === 1 / b;
//因为 null !== 任意类型(除非是自身,但是上面的 if 已经判断出来了)
if (a === null || b === null) return false;
// 这个专门用来判断 NaN,因为 NaN !== NaN
if (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'); // string
console.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 和 -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
参考: