判断对象全等
JavaScript 自带了==和===两种判断方式,前者会隐式转换类型导致代码出现问题,而后者则是根据指针地址进行判断。
在绝大多数情况这两种已经足够使用了,不过延伸下想判断两个对象或数组元素是否相同,则会显得不太友好,而且在 JavaScript 中有一些特殊的规则:NaN和NaN不相同、+0和-0相同,这就导致有的场景使用起来不便,下面动手实现一个equal函数,它具备以下功能:
+0和-0不相同NaN和NaN相同{}和{}相同[]和[]相同new Set()和new Set()相同new Map()和new Map()相同new Date(111)和new Date(111)相同new String(1)和new String(1)之类的包装对象相同/a/和/a/相同 -() => {}和() => {}不相同- 其他情况一律
===判断
第一版🔗
在这一版中,我们先实现+0和-0、NaN的判断
JSfunction equal(a, b) {
// 判断NaN
if (a === b) {
return 1 / a === 1 / b;
}
// 判断对象
if (a && b && typeof a === 'object' && typeof b === 'object') {
//... 留空
}
// 判断NaN
return a !== a && b !== b;
}
上面判断NaN的思路为1 / 0为Infinity,而1/-0为-Infinity
第二版🔗
第一版中我们已经实现了基础的功能,下面就来实现一下怎么深层次判断对象和数组是否相同
JSfunction equal(a, b) {
// 判断NaN
if (a === b) {
return 1 / a === 1 / b;
}
// 判断对象
if (a && b && typeof a === 'object' && typeof b === 'object') {
if (a.constructor !== b.constructor) {
return false;
}
// 判断array
if (Array.isArray(a) && Array.isArray(b)) {
const len = a.length;
if (len !== b.length) {
return false;
}
for (let i = 0; i < len; i++) {
if (!equal(a[i], b[i])) {
return false;
}
}
return true;
}
// 默认为对象,进行key和长度对比
const keys = Object.keys(a);
const len = keys.length;
if (Object.keys(b).length !== len) {
return false;
}
for (let i = 0; i < len; i++) {
const key = keys[i];
if (!Object.prototype.hasOwnProperty.call(b, key)) {
return false;
}
if (!equal(a[key], b[key])) {
return false;
}
}
return true;
}
}
上面代码中首先判断constructor是否相同,constructor可以从实例指向原型的构造函数,上面代码首先的意思是首先判断构造函数是否相同,如果不相同直接返回。
后面的话判断数组的成员数量、每个子属性是否相同,而判断对象也是判断length之后判断b下是否存在name,之后统一对比值是否相同。
至于执行Object.prototype.hasOwnProperty的原因则是hasOwnProperty有可能被改写,例如:
JSvar foo = {
hasOwnProperty: function () {
return false;
},
bar: 'Here be dragons',
};
foo.hasOwnProperty('bar'); // 始终返回 false
第三版🔗
上面的代码基本实现了一个雏形,下面加入Set、Map的对比,Set和Map是 es6 是引入的新的数据结构,Set是一组不重复的成员,Map则是object的升级版:重点解决了键名循环不稳定以及只能插入 string 键名的问题。
它们两者都是有序的,且都遵循Symbol.iterator,所以我们不仅要对比name、value也要对比所在的位置是否相同,这里为了简化后面的工作提前写了一个函数
JSconst iteration = (value, j) => {
let i = 0;
for (const iterator of value) {
if (j === i++) {
return iterator;
}
}
return undefined;
};
它的作用就是根据j指定下标来运行迭代器的next方法,例如:
JSiteration(new Set([1, 2, 3]), 1); // 2
JS// 判断set
if (a instanceof Set && b instanceof Set) {
const len = a.size;
if (len !== b.size) {
return false;
}
for (let i = 0; i < len; i++) {
if (!equal(iteration(a, i), iteration(b, i))) {
return false;
}
}
return true;
}
// 判断Map
if (a instanceof Map && b instanceof Map) {
const len = a.size;
if (len !== b.size) {
return false;
}
for (let i = 0; i < len; i++) {
const [nameA, valueA] = iteration(a.entries(), i);
const [nameB, valueB] = iteration(b.entries(), i);
if (!equal(nameA, nameB) || !equal(valueA, valueB)) {
return false;
}
}
return true;
}
可以发现Set和Map判断是否相似,首先对比成员数量,其次对比每一次当前位置的键名和键值。
第四版🔗
写到这里我们已经实现了 80%的功能,剩下的一些判断还有
new Date(111)和new Date(111)相同new String(1)和new String(1)之类的包装对象相同/a/和/a/相同
JS// 判断regexp
if (a.constructor === RegExp) {
return a.source === b.source && a.flags === b.flags;
}
if (a.toString !== Object.prototype.toString) {
return a.toString() === b.toString();
}
if (a.valueOf !== Object.prototype.valueOf) {
return a.valueOf() === b.valueOf();
}
判断包装类型和Date可以valueOf和toString方法实现,每个对象都会从Object.prototype继承vlaueOf和toString方法。
通常情况下valueOf返回包装对象传递的参数,而有的对象会更改valueOf方法,例如Date返回number类型,所以上面判断valueOf一则是为了Date另外一方则是初始过滤传递的参数。
至于为什么也判断toString则是为了考虑边界情况,例如:
JSvar a = new Boolean(true);
var b = new Boolean(1);
a === b ?
如果没有toString这个就相同了
最后🔗
完整的把代码贴一下,如果对你有帮助也可以star支持一下,有什么错误也欢迎指出。
JSconst iteration = (value, j) => {
let i = 0;
for (const iterator of value) {
if (j === i++) {
return iterator;
}
}
return undefined;
};
function equal(a, b) {
// 判断NaN
if (a === b) {
return 1 / a === 1 / b;
}
// 判断对象
if (a && b && typeof a === 'object' && typeof b === 'object') {
// 判断对象
if (a && b && typeof a === 'object' && typeof b === 'object') {
if (a.constructor !== b.constructor) {
return false;
}
// 判断array
if (Array.isArray(a) && Array.isArray(b)) {
const len = a.length;
if (len !== b.length) {
return false;
}
for (let i = 0; i < len; i++) {
if (!equal(a[i], b[i])) {
return false;
}
}
return true;
}
// 判断set
if (a instanceof Set && b instanceof Set) {
const len = a.size;
if (len !== b.size) {
return false;
}
for (let i = 0; i < len; i++) {
if (!equal(iteration(a, i), iteration(b, i))) {
return false;
}
}
return true;
}
// 判断Map
if (a instanceof Map && b instanceof Map) {
const len = a.size;
if (len !== b.size) {
return false;
}
for (let i = 0; i < len; i++) {
const [nameA, valueA] = iteration(a.entries(), i);
const [nameB, valueB] = iteration(b.entries(), i);
if (!equal(nameA, nameB) || !equal(valueA, valueB)) {
return false;
}
}
return true;
}
// 判断regexp
if (a.constructor === RegExp) {
return a.source === b.source && a.flags === b.flags;
}
if (a.toString !== Object.prototype.toString) {
return a.toString() === b.toString();
}
if (a.valueOf !== Object.prototype.valueOf) {
return a.valueOf() === b.valueOf();
}
// 默认为对象,进行key和长度对比
const keys = Object.keys(a);
const len = keys.length;
if (Object.keys(b).length !== len) {
return false;
}
for (let i = 0; i < len; i++) {
const key = keys[i];
if (!Object.prototype.hasOwnProperty.call(b, key)) {
return false;
}
if (!equal(a[key], b[key])) {
return false;
}
}
return true;
}
}
// 判断NaN
return a !== a && b !== b;
}