前言
学习的underscore.js 源码版本为1.13.1
_.isEqual
执行两个对象之间的深度比较,确定他们是否应被视为相等。
先看使用效果:
// +0 与 -0 不相等
_.isEqual(0, -0); // false
// NaN 与 NaN 相等
_.isEqual(NaN, NaN); // true
// '5' 与 new String('5') 相等
_.isEqual('5', new String('5')); // true
// true 与 new Boolean(true) 相等
_.isEqual(true, new Boolean(true)); // true
// new Date('2021-12-06') 与 new Date('2021-12-06') 相等
_.isEqual(new Date('2021-12-06'), new Date('2021-12-06')); // true
// /d+/g 与 new RegExp('d+', 'g') 相等
_.isEqual(/d+/g, new RegExp('d+', 'g')); // true
// {name: 'zxx', age: 18, hobbies: ['movie', 'sport']} 与 {age: 18, hobbies: ['movie', 'sport'], name: 'zxx'} 相等
_.isEqual({name: 'zxx', age: 18, hobbies: ['movie', 'sport']}, {age: 18, hobbies: ['movie', 'sport'], name: 'zxx'}); // true
var a = {foo: {b: {foo: {c: {foo: null}}}}};
var b = {foo: {b: {foo: {c: {foo: null}}}}};
a.foo.b.foo.c.foo = a;
b.foo.b.foo.c.foo = b;
_.isEqual(a, b); // true
我们来一步步实现这个判断两个对象/参数是否相等的函数
function eq(a, b) {
// 主体比较代码
}
+0 与 -0
如果a === b的结果为true, 那么 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
实现 +0 与 -0 不相等的eq函数
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
可以很容易的实现eq:
function eq(a, b) {
if (a !== a) {
return b !== b;
}
}
console.log(eq(NaN, NaN)); // true
比较的两参数中有null、undefined、 函数的情况
function eq(a, b) {
// 参数中有null、undefined的情况
if (a == null || b == null) return false;
// a是基本类型且b不是对象类型(没有必要判断b类型是不是function, 因为基本类型和函数肯定是不会相等的)就直接返回false
var type = typeof a;
if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
}
小结
第一版eq函数
// 第一版先用来过滤简单的类型比较,复杂类型使用deepEq函数进行处理
function eq(a, b) {
// === 结果为true的区别出+0 和 -0
if (a === b) return a !== 0 || 1 / a === 1 / b;
// 参数中有null、undefined的情况
if (a == null || b == null) return false;
// 判断NaN
if (a !== a) return b !== b;
// a是基本类型且b不是对象类型(没有必要判断b类型是不是function, 因为基本类型和函数肯定是不会相等的)就直接返回false
var type = typeof a;
if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
// 深度比较两对象
return deepEq(a, b);
}
处理RegExp、String、Number、Date、Boolean对象
先看如下代码:
'' + /d+/g = '' + new RegExp('d+', 'g'); // true
'5' === '' + new String('5'); // true
1 === + new Number(1); // true
+ new Date('2021-12-06') === + new Date('2021-12-06'); // true
+ false === + new Boolean(false); // true
很明显是利用了隐式类型转换进行最终的比较。
所以我们可以先利用Object.prototype.toString来判断类型是否一致
var toString = Object.prototype.toString;
toString.call('5') === toString.call(new String('5')); // 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]":
// 判断Object(NaN)、Number(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;
}
// ...
}
注: 需要考虑Object(NaN)、Number(NaN)
处理Symbol
Symbol表示独一无二的值,处不处理都一样
var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null;
// ...省略
case '[object Symbol]':
return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b);
处理Object
Objects with different constructors and identical own properties are not equal 具有不同构造函数和相同自身属性的对象不相等
function First () {
this.value = 1;
}
function Second () {
this.value = 1;
}
var first = new First();
var second = new Second();
eq(first, second); // false
实现
function isFunction (func) {
return Object.prototype.toString.call(func) === '[object Function]'
};
function deepEq (a, b) {
// 接以上代码
var areArrays = className === '[object Array]';
// 不是数组
if (!areArrays) {
// 过滤掉函数的情况
if (typeof a != 'object' || typeof b != 'object') return false;
// 构造函数不相同且不全是Object 构造函数的情况下 不相等
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;
}
}
}
Objects containing equivalent members are equal 包含相同成员的对象是相等的
eq({a: /Curly/g, b: new Date(2009, 11, 13)}, {a: /Curly/g, b: new Date(2009, 11, 13)}); // true
所以我们需要比较属性、值是否一致
var nativeKeys = Object.keys;
function isObject(obj) {
var type = typeof obj;
return type === "function" || (type === "object" && !!obj);
}
// 获取对象自身可枚举属性组成的数组
function keys(obj) {
if (!isObject(obj)) return [];
if (nativeKeys) return nativeKeys(obj);
}
function has(obj, key) {
return obj != null && Object.prototype.hasOwnProperty.call(obj, key);
}
function deepEq (a, b) {
var areArrays = className === '[object Array]';
if (!areArrays) {
// 过滤掉函数的情况
if (typeof a != 'object' || typeof b != 'object') return false;
// 构造函数不相同且都不全是Object 构造函数的情况下 不相等
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;
}
var _keys = keys(a), key;
length = _keys.length;
// 在递归比较之前,确保两个对象包含相同数量的属性
if (keys(b).length !== length) return false;
while (length--) {
// 属性名及属性值比较
key = _keys[length];
if (!(has(b, key) && eq(a[key], b[key]))) return false;
}
}
// 其他判断
return true;
}
处理Array
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 = keys(a), key;
length = _keys.length;
// 在递归比较之前,确保两个对象包含相同数量的属性
if (keys(b).length !== length) return false;
while (length--) {
// 递归比较
key = _keys[length];
if (!(has(b, key) && eq(a[key], b[key]))) return false;
}
}
return true;
}
处理循环引用
var a = {abc: null};
var b = {abc: null};
a.abc = a;
b.abc = 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;
eq(a, b); // true
underscore 处理这个问题是多传递两个参数为 aStack 和 bStack,用来储存 a 和 b 递归比较过程中的 a 和 b 的值:
function eq(a, b, aStack, bStack) {
// ...
return deepEq(a, b, aStack, bStack);
}
function deepEq (a, b, aStack, bStack) {
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]":
// 判断Object(NaN)、Number(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);
}
var areArrays = className === '[object Array]';
if (!areArrays) {
// 过滤掉函数的情况
if (typeof a != 'object' || typeof b != 'object') return false;
// 构造函数不相同且都不全是Object 构造函数的情况下 不相等
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 = keys(a), key;
length = _keys.length;
// 在递归比较之前,确保两个对象包含相同数量的属性
if (keys(b).length !== length) return false;
while (length--) {
// 递归比较
key = _keys[length];
if (!(has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
}
}
return true;
}
其它
处理ArrayBuffer及DataView
在IE 10 - Edge 13浏览器中
var buffer = new ArrayBuffer(16);
var view = new DataView(buffer);
// IE 10 - Edge 13浏览器中为true
Object.prototype.toString.call(DataView) === '[object Object]'
可以将它们转换成Uint8Array进行比较
var tagDataView = '[object DataView]';
var supportsDataView = typeof DataView !== 'undefined';
var hasObjectTag = function (obj) {
return Object.prototype.toString.call(obj) === '[object Object]'
};
var isDataView = function (dataView) {
return Object.prototype.toString.call(dataView) === '[object DataView]'
};
// IE 10 - Edge 13浏览器, `DataView` has string tag `'[object Object]'`.
var hasStringTagBug = (supportsDataView && hasObjectTag(new DataView(new ArrayBuffer(8))));
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 ? void 0 : bufferSource['byteLength'])
);
}
function deepEq(a, b, aStack, bStack) {
switch (className) {
// ...
case '[object ArrayBuffer]':
case tagDataView:
return deepEq(toBufferView(a), toBufferView(b), aStack, bStack);
}
}
处理 TypedArray
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var supportsArrayBuffer = typeof ArrayBuffer !== "undefined";
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 (collection) {
var sizeProperty = collection == null ? void 0 : collection["byteLength"];
return typeof sizeProperty == "number" && sizeProperty >= 0 && sizeProperty <= MAX_ARRAY_INDEX;
};
function constant(value) {
return function () {
return value;
};
}
function isTypedArray(obj) {
return nativeIsView
? nativeIsView(obj) && !isDataView$1(obj)
: isBufferLike(obj) && typedArrayPattern.test(toString.call(obj));
}
var isTypedArray$1 = supportsArrayBuffer ? isTypedArray : constant(false);
function deepEq(a, b, aStack, bStack) {
// ...
// 处理类型化数组
if (!areArrays && isTypedArray$1(a)) {
var byteLength = a.byteLength;
if (byteLength !== b.byteLength) return false;
if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) return true;
areArrays = true;
}
// ...
}
最终代码
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var tagDataView = '[object DataView]';
var supportsDataView = typeof DataView !== 'undefined';
var hasObjectTag = function (obj) {
return Object.prototype.toString.call(obj) === '[object Object]'
};
var isDataView = function (dataView) {
return Object.prototype.toString.call(dataView) === '[object DataView]'
};
// IE 10 - Edge 13浏览器, `DataView` has string tag `'[object Object]'`.
var hasStringTagBug = (supportsDataView && hasObjectTag(new DataView(new ArrayBuffer(8))));
var supportsArrayBuffer = typeof ArrayBuffer !== "undefined";
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 (collection) {
var sizeProperty = collection == null ? void 0 : collection["byteLength"];
return typeof sizeProperty == "number" && sizeProperty >= 0 && sizeProperty <= MAX_ARRAY_INDEX;
};
function ie10IsDataView(obj) {
return obj != null && isFunction(obj.getInt8) && isArrayBuffer(obj.buffer);
}
var isDataView$1 = (hasStringTagBug ? ie10IsDataView : isDataView);
function constant(value) {
return function () {
return value;
};
}
function isTypedArray(obj) {
return nativeIsView
? nativeIsView(obj) && !isDataView$1(obj)
: isBufferLike(obj) && typedArrayPattern.test(toString.call(obj));
}
var isTypedArray$1 = supportsArrayBuffer ? isTypedArray : constant(false);
function toBufferView(bufferSource) {
return new Uint8Array(
bufferSource.buffer || bufferSource,
bufferSource.byteOffset || 0,
(bufferSource == null ? void 0 : bufferSource['byteLength'])
);
}
var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null;
var toString = Object.prototype.toString;
var nativeKeys = Object.keys;
function isObject(obj) {
var type = typeof obj;
return type === "function" || (type === "object" && !!obj);
}
function isFunction (func) {
return Object.prototype.toString.call(func) === '[object Function]'
};
// 获取对象自身可枚举属性组成的数组
function keys(obj) {
if (!isObject(obj)) return [];
if (nativeKeys) return nativeKeys(obj);
}
function has(obj, key) {
return obj != null && Object.prototype.hasOwnProperty.call(obj, key);
}
function eq(a, b, aStack, bStack) {
// === 结果为true的区别出+0 和 -0
if (a === b) return a !== 0 || 1 / a === 1 / b;
// 参数中有null、undefined的情况
if (a == null || b == null) return false;
// 判断NaN
if (a !== a) return b !== b;
// a是基本类型且b不是对象类型(没有必要判断b类型是不是function, 因为基本类型和函数肯定是不会相等的)就直接返回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;
switch (className) {
case "[object RegExp]":
case "[object String]":
return "" + a === "" + b;
case "[object Number]":
// 判断Object(NaN)、Number(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 = a.byteLength;
if (byteLength !== b.byteLength) 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;
// 构造函数不相同且都不全是Object 构造函数的情况下 不相等
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 = keys(a), key;
length = _keys.length;
// 在递归比较之前,确保两个对象包含相同数量的属性
if (keys(b).length !== length) return false;
while (length--) {
// 递归比较
key = _keys[length];
if (!(has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
}
}
return true;
}
测试:
可以看下underscorejs.net/test/object… 关于_.isEqual的测试用例
eq(0, -0); // false
eq(NaN, NaN); // false
var a = {
name: new String("Moe Howard"),
age: new Number(77),
stooge: true,
hobbies: ["acting"],
film: {
name: "Sing a Song of Six Pants",
release: new Date(1947, 9, 30),
stars: [new String("Larry Fine"), "Shemp Howard"],
minutes: new Number(16),
seconds: 54,
},
};
var b = {
name: new String("Moe Howard"),
age: new Number(77),
stooge: true,
hobbies: ["acting"],
film: {
name: "Sing a Song of Six Pants",
release: new Date(1947, 9, 30),
stars: [new String("Larry Fine"), "Shemp Howard"],
minutes: new Number(16),
seconds: 54,
},
};
eq(a, b); // true
a = Object.create(null, {x: {value: 1, enumerable: true}});
b = {x: 1};
eq(a, b); // true
// Circular Arrays.
(a = []).push(a);
(b = []).push(b);
eq(a, b); // true
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); // true
var buffer = new ArrayBuffer(16);
var view = new DataView(buffer, 0);
view.setInt16(1, 42);
var buffer1 = new ArrayBuffer(16);
var view1 = new DataView(buffer1, 0);
view1.setInt16(1, 42);
eq(view, view1); // true
参考资料: