yliu

时来天地皆同力,运去英雄不自由

判断对象全等


JavaScript 自带了=====两种判断方式,前者会隐式转换类型导致代码出现问题,而后者则是根据指针地址进行判断。

在绝大多数情况这两种已经足够使用了,不过延伸下想判断两个对象或数组元素是否相同,则会显得不太友好,而且在 JavaScript 中有一些特殊的规则:NaNNaN不相同、+0-0相同,这就导致有的场景使用起来不便,下面动手实现一个equal函数,它具备以下功能:

  • +0-0不相同
  • NaNNaN相同
  • {}{}相同
  • [][]相同
  • new Set()new Set()相同
  • new Map()new Map()相同
  • new Date(111)new Date(111)相同
  • new String(1)new String(1)之类的包装对象相同
  • /a//a/相同 -() => {}() => {}不相同
  • 其他情况一律===判断

第一版

在这一版中,我们先实现+0-0NaN的判断

function 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 / 0Infinity,而1/-0-Infinity

第二版

第一版中我们已经实现了基础的功能,下面就来实现一下怎么深层次判断对象和数组是否相同

function 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有可能被改写,例如:

var foo = {
  hasOwnProperty: function () {
    return false;
  },
  bar: 'Here be dragons',
};

foo.hasOwnProperty('bar'); // 始终返回 false

第三版

上面的代码基本实现了一个雏形,下面加入SetMap的对比,SetMap是 es6 是引入的新的数据结构,Set是一组不重复的成员,Map则是object的升级版:重点解决了键名循环不稳定以及只能插入 string 键名的问题。

它们两者都是有序的,且都遵循Symbol.iterator,所以我们不仅要对比namevalue也要对比所在的位置是否相同,这里为了简化后面的工作提前写了一个函数

const iteration = (value, j) => {
  let i = 0;
  for (const iterator of value) {
    if (j === i++) {
      return iterator;
    }
  }
  return undefined;
};

它的作用就是根据j指定下标来运行迭代器的next方法,例如:

iteration(new Set([1, 2, 3]), 1); // 2
// 判断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;
}

可以发现SetMap判断是否相似,首先对比成员数量,其次对比每一次当前位置的键名和键值。

第四版

写到这里我们已经实现了 80%的功能,剩下的一些判断还有

  • new Date(111)new Date(111)相同
  • new String(1)new String(1)之类的包装对象相同
  • /a//a/相同
// 判断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可以valueOftoString方法实现,每个对象都会从Object.prototype继承vlaueOftoString方法。

通常情况下valueOf返回包装对象传递的参数,而有的对象会更改valueOf方法,例如Date返回number类型,所以上面判断valueOf一则是为了Date另外一方则是初始过滤传递的参数。

至于为什么也判断toString则是为了考虑边界情况,例如:

var a = new Boolean(true);
var b = new Boolean(1);
a === b ?

如果没有toString这个就相同了

最后

完整的把代码贴一下,如果对你有帮助也可以star支持一下,有什么错误也欢迎指出。

const 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;
}

参考