underscore1.11.0 中判断两个参数相同的函数为isEqual
isEqual 函数认为以下相等:
- 0 与 -0 不相等;
- NaN 与 NaN相等;
- /a/i 与 new RegExp(/a/i)相等;
- '5' 与 new String('5')相等;
- true 与 new Boolean(true)相等;
- Object(NaN) 与 Object(NaN)相等,new Number(1) 与 new Number(1)相等;
- new Date('2020-10-30') 与 new Date('2020-10-30')相等;
- new Boolean(false) 与 new Boolean(false)相等;
- [1, 2, 3] 与 [1, 2, 3]相等;
- {name: 'zxx', age: 18} 与 {name: 'zxx', age: 18}相等;
- ...
我们来一步步实现这个判断两个参数是否相等的函数
function eq(a, b) { ... }
判断+0 与 -0不相等
console.log(+0 === -0); // true
(-0).toString(); // '0'
(+0).toString(); // '0'
-0 < +0; // false
+0 < -0; // false
但两者还是不同:
1 / +0 // Infinity
1 / -0 // -Infinity
1 / +0 === 1 / -0; // false
之所以会有+0 和 -0 是因为javascript采用了IEEE_754浮点数表示法,这是一种二进制表示法,其中最高位表示符号位(0表示正,1表示负),剩下的用于表示大小。
以下情况会产生-0
Math.round(-0.1) // -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相等
console.log(NaN === NaN); // false
function eq(a, b) {
if (a !== a) {
return b !== b;
}
}
console.log(eq(NaN, NaN)); // true
第一版eq函数
// 第一版先用来过滤简单的类型比较,复杂类型使用deepEq函数进行处理
function eq(a, b) {
// === 结果为true的区别出+0 和 -0
if (a === b) return a !== 0 || 1 / a === 1 / b;
// null === null 为true, 这里做判断是为了让有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;
return deepEq(a, b);
}
剩下的就是实现比较对象是否相等的deepEq函数了
对于RegExp、String对象,我们可以统一转成字符串进行比较:
var toString = Object.prototype.toString;
var reg = /a/i, reg1 = new RegExp(/a/i);
console.log(toString.call(reg)); // [object RegExp]
console.log(toString.call(reg1)); // [object RegExp]
console.log('' + reg === '' + reg1); // true
var str = '5', str1 = new String(5);
console.log(toString.call(str)); // [object String]
console.log(toString.call(str1)); // [object String]
console.log('' + str === '' + str1); // true
对于Number,我们转成数字进行比较:
var toString = Object.prototype.toString;
var num = Object(NaN), num1 = new Number(NaN);
// 对于NaN单独处理
console.log(+num !== +num, +num1 !== num1); // true true
num = 0, num1 = new Number(-0);
// 0 与 -0
console.log(1 / +num === 1 / +num1); // false
num = 1, num1 = new Number(1);
// 非0
console.log(+num === +num1); // true
对于Date、Boolean ,我们同样转成数值原始值进行比较:
var toString = Object.prototype.toString;
var date = new Date('2020-10-30'), data1 = new Date('2020-10-30');
console.log(+date === +date1); // true
// 日期不正确会返回NaN
console.log(+new Date('2020-10-300') === +new Date('2020-10-300')); // false
var bool = true, bool1 = new Boolean(true);
console.log(+bool === +bool1); // true
下面我们来实现一部分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 (name) {
this.name = name;
}
function Animal (name) {
this.name = name;
}
var person = new Person('hello');
var animal = new Animal('hello');
两者虽然都是{name: 'hello'}, 但是这两者属于不同构造函数的实例,此时,我们认为是不同的对象。
如果两个对象所属的构造函数对象不同,两个对象能相等吗?
var obj = Object.create(null);
obj.name = 'hello';
eq(obj, {name: 'hello'});
obj对象没有原型,也没有构造函数,但在实际应用中,只要两者有着相同的键值对,我们就认为相等。
我们来实现对于不同构造函数下的实例直接返回false:
function isFunction (obj) {
return toString.call(obj) === '[object Object]';
}
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;
// 构造函数都存在且都不是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 (keys(b).length !== length) return false;
while (length--) {
// 深入比较每个元素
key = _keys[length];
if (!(b.hasOwnProperty(key) && eq(a[key], b[key]))) return false;
}
}
return true;
}
解决循环引用问题:
var a = {foo: null};
var b = {foo: null};
a.foo = a;
b.foo = b;
那underscore是如何解决这个问题的呢?答案是多传递两个参数aStack和bStack,用来存储a和b递归比较过程中的a和b的值:
var a = {foo: null};
var b = {foo: null};
a.foo = a;
b.foo = b;
function eq(a, b, aStack, bStack) {
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), key;
length = _keys.length;
if (keys(b).length !== length) return false;
while (length--) {
key = _keys[length];
console.log(a[key], b[key], aStack, bStack)
if (!(b.hasOwnProperty(key) && eq(a[key], b[key], aStack, bStack))) return false;
}
// 从遍历对象的堆栈中移除第一个对象
aStack.pop();
bStack.pop();
return true;
}
下面接着判断ArrayBuffer 、DataView类型
对于ArrayBuffer 、DataView首先需要判断兼容性:
在IE10-Edge13版本中,DataView实例toString为[object Object]
在IE11中, Map、WeakMap 、Set同样也是。
以下代码是判断ArrayBuffer 、DataView类型:
var toString = Object.prototype.toString;
function tagTester(name) {
return function(obj) {
return toString.call(obj) === '[object ' + name + ']';
};
}
var isFunction = tagTester('Function');
var hasObjectTag = tagTester('Object');
var hasStringTagBug = typeof DataView !== 'undefined' && hasObjectTag(new DataView(new ArrayBuffer(8)));
var isArrayBuffer = tagTester('ArrayBuffer');
var isDataView = tagTester('DataView');
var isFunction = tagTester('Function');
// 兼容性
var nodelist = document && document.childNodes;
if (typeof /./ != 'function' && typeof Int8Array != 'object' && typeof nodelist != 'function') {
isFunction = function(obj) {
return typeof obj == 'function' || false;
}
}
function ie10IsDataView(obj) {
return obj != null && isFunction(obj.getInt8) && isArrayBuffer(obj.buffer);
}
var isDataView$1 = (hasStringTagBug ? ie10IsDataView : isDataView);
剩下来就是如何进行比较这两种类型:
function toBufferView(bufferSource) {
return new Uint8Array(
bufferSource.buffer || bufferSource,
bufferSource.byteOffset || 0,
bufferSource == null ? undefined : bufferSource.byteLength
)
}
最后除了Array类型外,还需要考虑Int8Array、Float32Array等类型
// 代码接上面
var supportsArrayBuffer = typeof ArrayBuffer !== 'undefined',
supportsDataView = typeof DataView !== 'undefined';
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var nativeIsView = supportsArrayBuffer && ArrayBuffer.isView;
var typedArrayPattern = /\[object ((I|Ui)nt(8|16|32)|Float(32|64)|Uint8Clamped|Big(I|Ui)nt64)Array\]/;
var isBufferLike = function (obj) {
var sizeProperty = obj == null ? void 0 : obj['byteLength'];
return typeof sizeProperty == 'number' && sizeProperty >= 0 && sizeProperty <= MAX_ARRAY_INDEX;
}
function isTypedArray(obj) {
return nativeIsView ? (nativeIsView(obj) && !isDataView$1(obj)) :isBufferLike(obj) && typedArrayPattern.test(toString.call(obj));
}
最终代码如下:
var toString = Object.prototype.toString;
function tagTester(name) {
return function(obj) {
return toString.call(obj) === '[object ' + name + ']';
};
}
var isFunction = tagTester('Function');
var hasObjectTag = tagTester('Object');
var hasStringTagBug = typeof DataView !== 'undefined' && hasObjectTag(new DataView(new ArrayBuffer(8)));
var isArrayBuffer = tagTester('ArrayBuffer');
var isDataView = tagTester('DataView');
var isFunction = tagTester('Function');
// 兼容性
var nodelist = document && document.childNodes;
if (typeof /./ != 'function' && typeof Int8Array != 'object' && typeof nodelist != 'function') {
isFunction = function(obj) {
return typeof obj == 'function' || false;
}
}
function ie10IsDataView(obj) {
return obj != null && isFunction(obj.getInt8) && isArrayBuffer(obj.buffer);
}
var isDataView$1 = (hasStringTagBug ? ie10IsDataView : isDataView);
var supportsArrayBuffer = typeof ArrayBuffer !== 'undefined',
supportsDataView = typeof DataView !== 'undefined';
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var nativeIsView = supportsArrayBuffer && ArrayBuffer.isView;
var typedArrayPattern = /\[object ((I|Ui)nt(8|16|32)|Float(32|64)|Uint8Clamped|Big(I|Ui)nt64)Array\]/;
var isBufferLike = function (obj) {
var sizeProperty = obj == null ? void 0 : obj['byteLength'];
return typeof sizeProperty == 'number' && sizeProperty >= 0 && sizeProperty <= MAX_ARRAY_INDEX;
}
var getByteLength = function (obj) {
return obj == null ? void 0 : obj['byteLength'];
}
function isTypedArray(obj) {
return nativeIsView ? (nativeIsView(obj) && !isDataView$1(obj)) :isBufferLike(obj) && typedArrayPattern.test(toString.call(obj));
}
var isTypedArray$1 = supportsArrayBuffer ? isTypedArray : false;
var tagDataView = '[object DataView]';
function eq(a, b, aStack, bStack) {
// === 结果为true的区别出+0 和 -0
if (a === b) return a !== 0 || 1 / a === 1 / b;
// null === null 为true, 这里做判断是为了让有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;
return deepEq(a, b, aStack, bStack);
}
function deepEq(a, b, aStack, bStack) {
var className = toString.call(a);
if (className !== toString.call(b)) return false;
// 解决IE 10-Edge 13中的兼容性问题
if (hasStringTagBug && className == '[object Object]' && isDataView$1(a)) {
if (!isDataView$1(b)) return false;
className = tagDataView;
}
switch (className) {
// 这些类型按值进行比较
case '[object RegExp]':
case '[object String]':
return '' + a === '' + b;
case '[object Number]':
// 判断Object(NaN)
if (+a !== +a) return +b !== +b;
return +a === 0 ? 1 / +a === 1 / b : +a === +b;
case '[object Date]':
case '[object Boolean]':
// 将日期和布尔值转换为数值原始值
return +a === +b;
case '[object Symbol]':
return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b);
case '[object ArrayBuffer]':
case tagDataView:
return deepEq(toBufferView(a), toBufferView(b), aStack, bStack);
}
var areArrays = className === '[object Array]';
if (!areArrays && isTypedArray$1(a)) {
var byteLength = getByteLength(a);
if (byteLength !== getByteLength(b)) return false;
if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) return true;
areArrays = true;
}
if (!areArrays) {
if (typeof a != 'object' || typeof b != 'object') return false;
var aCtor = a.constructor, bCtor = b.constructor;
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(Object(NaN), Number(NaN))); // true
console.log(eq('5', new String('5'))); // true
console.log(eq([1, 2], [1, 2])); // true
console.log(eq({ value: 1 }, { value: 1 })); // true
console.log(eq(new Int32Array([21,31]), new Int32Array([21,31]))); // true
var a = {foo: null};
var b = {foo: null};
a.foo = a;
b.foo = b;
console.log(eq(a, b)); // true
参考资料: JavaScript专题之如何判断两个对象相等
underscore isEqual