JS-这两个对象相等吗?

239 阅读7分钟

前言

JS中,并没有直接提供方法帮助开发者判断两个对象是否相等,但在开发中,却绕不开这种判断,本文实现了一种比较全面的、可扩展的对象对比方法。

基本思路

  1. 该方法接受两个参数,作为比较的对象,这两个参数可以是任何值(原始类型引用类型皆可);
  2. 先判断两个参数是否相等,如果相等,则返回true;因为无论是原始类型还是引用类型,如果相等,说明要么值相等,要么是同一个引用对象,那么必然是相等的;
  3. 判断两个参数的数据类型是否相同,如果不相同,直接返回false;
  4. 如果两个参数的数据类型相同,那么判断是否为引用类型,如果不是,返回false;因为如果不是引用类型,那就是原始类型了,而原始类型在第2步中已经判断过了,所以返回false即可;到这一步,已经将全部原始类型排除,接下去处理,都是引用类型
  5. 准备一个映射表(JSON),该映射表记录各种引用类型对比方法,同时,也方便后续数据类型的对比方法扩展修改
  6. 从映射表中获取当前引用类型相应的对比方法,执行该方法,最后返回该方法执行结果。

话不多说,跟着思路开始撸代码。

前置准备

上面思路中提到的记录各种引用类型对比方法的映射表,可以先定义出来,不在方法内部定义:

// 策略映射表
const policyMap = {}

还有两个常用的方法:

/** 判断是否为引用类型 */
function isReferenceValue (target) {
  // 判断是否为引用类型时,需要先排除 null
  return target !== null && typeof target === 'object'
}

/** 获取类型方法 */
const getDataType = Object.prototype.toString

判断函数

根据上面思路写出来的判断函数如下:

/** 接收两个参数 */
function isEqual (target1, target2) {
  /** 值 或者 地址比较,如果相同返回 true,主要是处理原始类型的对比
  * 此处使用`Object.is`进行判断,而不用`===`,是因为它们对待有符号的零和`NaN`不同。
  * 例如,`===` 运算符(也包括 `==` 运算符)将数字 `-0` 和 `+0` 视为相等,而将`NaN` 与 `NaN` 视为不相等;
  * 而`Object.is`将 `-0` 和 `+0` 视为不相等,`NaN` 与 `NaN` 视为相等。
  */
  if (Object.is(target1, target2)) { return true }
  
  // 数据类型进行比较,数据类型不同,或者都不是引用类型,则返回 false
  const typeOfTarget1 = getDataType.call(target1)
  if (typeOfTarget1 !== getDataType.call(target2) || !isReferenceValue(target1)) { return false }
  
  // 根据引用类型,获取比较方法
  const policy = policyMap[typeOfTarget1]
  // 对比的方法接受两个参数,返回方法的比较结果
  return policy ? policy(target1, target2) : String(target1) === String(target2)
}

主要流程的代码都已经准备完毕了,那么接下来,自然就是完善映射表,实现各种引用类型对比方法

实现引用类型的对比方法

本节只记叙实现的思路和代码,测试数据都放在下一节。

Object

判断思路:

  1. 获取比较的两个对象的键;
  2. 判断两个对象的键的数量是否相同,如果不相同返回false;
  3. 如果键的数量相同,则遍历每一个键的值,逐一进行对比,直到发现不相等的值,返回false;如果没有发现,则返回true
/** Object 对比 */
function equalObject (obj1, obj2) {
  // 获取两个对象的键
  const keys1 = Object.keys(obj1)
  const keys2 = Object.keys(obj2)
  // 判断键的长度是否相等
  // 如果相等,则遍历所有键,直到找出不相等的值,或者遍历完成
  return keys1.length === keys2.length && keys1.every(key => isEqual(obj1[key], obj2[key]))
}

// 将方法加入映射表中,键为 Object.prototype.toString.call({}) 的值
policyMap['[object Object]'] = equalObject

Array

判断思路:

  1. 比较两个数组的长度,如果不一致,返回false;
  2. 如果长度相同,则遍历两个数组,直到发现不相等的值,返回false;如果没有发现不相等的值,返回true
/** Array 对比 */
function equalArray (arr1, arr2) {
  return arr1.length === arr2.length && arr1.every((item, index) => isEqual(item, arr2[index]))
}

// 将方法加入映射表中,键为 Object.prototype.toString.call([]) 的值
policyMap['[object Array]'] = equalArray

Date

判断思路:

  1. 时间对象的判断是否相同,其实就是判断是否为同一个时间点
  2. 只需要将两个时间对象,转换为字符串或者数字,就能判断是否为同一个时间点
  3. 获取两个时间对象的毫秒数,进行对比即可
/** Date 对比 */
function equalDate (date1, date2) {
  return date1.getTime() === date2.getTime()
}

// 将方法加入映射表中,键为 Object.prototype.toString.call(new Date()) 的值
policyMap['[object Date]'] = equalDate

RegExp

判断思路:

  1. 将两个正则表达式转换为字符串;
  2. 判断这两个字符串是否相同;
/** RegExp 对比 */
function equalRegExp (reg1, reg2) {
  return reg1.toString() === reg2.toString()
}

// 将方法加入映射表中,键为 Object.prototype.toString.call(new RegExp()) 的值
policyMap['[object RegExp]'] = equalRegExp

Map

Map难以处理的地方有两点:

  1. Map其实是有序的,是按插入的顺序排列的;
  2. Map不只是值可以是任何类型,键也可以是任何类型,包括原始类型引用类型

判断思路:

  1. 判断两个Map的大小是否相同,如果不相同,返回false;如果相同,执行以下步骤;
  2. 获取两个Map迭代器,逐个遍历相应位置的键值对
  3. 对于每一个键与值,都进行对比;
/** Map 对比 */
function equalMap (map1, map2) {
  // 判断两个 Map 的大小是否校相同,不相同返回 false
  if (map1.size !== map2.size) { return false }
  
  // 获取 Map 的迭代器
  const iterator1 = map1.entries()
  const iterator2 = map2.entries()
  // 记录下第一组对比的键值对,是一个数组的形式
  let value1 = iterator1.next().value
  let value2 = iterator2.next().value
  
  // 通过循环遍历,将 Map 中每一个键、每一个值进行对比
  while(value1 && value2) {
    for (let i = 0; i < 2; i++) {
      // 调用对比,递归,如果有一个不相同,则返回 false,结束循环
      if (!isEqual(value1[i], value2[i])) {
        return false
      }
    }
    // 获取下一组对比的键值对
    value1 = iterator1.next().value
    value2 = iterator2.next().value
  }
  
  // 遍历完成之后,说明没有不同的键值对,返回 true
  return true
}

// 将方法加入映射表中,键为 Object.prototype.toString.call(new Map()) 的值
policyMap['[object Map]'] = equalMap

Set

判断思路:
基本上与 Map 相同;
区别在于 Set 迭代器返回的是 [value, value] 形式的数组,因为没有Key的存在,为了与 Map 对象的 API 形式保持一致,故使得每一个 entrykeyvalue 都拥有相同的值,因而最终返回一个 [value, value] 形式的数组,详细
所以最终只需对比迭代器返回的数组中第一个值即可。

/** Set 对比 */
function equalSet (set1, set2) {
  // 判断两个 Set 的大小是否校相同,不相同返回 false
  if (set1.size !== set2.size) { return false }
  
  // 获取 Map 的迭代器
  const iterator1 = set1.entries()
  const iterator2 = set2.entries()
  // 记录下第一组对比的值,是一个数组的形式
  let value1 = iterator1.next().value
  let value2 = iterator2.next().value
  
  // 通过循环遍历,将 Map 中每一个值进行对比
  while (value1 && value2) {
    // 只对比第一个值
    if (!isEqual(value1[0], value2[0])) { return false }
    
    // 获取下一组对比数据
    value1 = iterator1.next().value
    value2 = iterator2.next().value
  }
  
  // 遍历完成之后,说明没有不同的键值对,返回 true
  return true
}

// 将方法加入映射表中,键为 Object.prototype.toString.call(new Set()) 的值
policyMap['[object Set]'] = equalSet

Error

判断思路:

  1. Error对象的相关属性不是很多,本文主要对比错误名称Error.prototype.name、错误描述Error.prototype.message;
  2. 可以调用Error.prototype.toString()同时将错误名称错误描述转换为字符串,进行对比即可;
  3. 除了通用的 Error 构造函数外,JavaScript 还有其它类型的错误构造函数,但是通过Object.prototype.toString.call() 获取类型,最终都是 [object Error],所以不必特殊处理;
/** Error 对比 */
function equalError (err1, err2) {
  return err1.toString() === err2.toString()
}

// 将方法加入映射表中,键为 Object.prototype.toString.call(new Error()) 的值
policyMap['[object Error]'] = equalError

最终对比方法映射表

// 映射表
const policyMap = {
  '[object Object]': equalObject,
  '[object Array]': equalArray,
  '[object Date]': equalDate,
  '[object RegExp]': equalRegExp,
  '[object Map]': equalMap,
  '[object Set]': equalSet,
  '[object Error]': equalError
}

如果还需要添加其他数据类型对比方法,直接在映射表中添加即可。

测试

测试数据如下:

const sym = Symbol('Sym')
// 测试数据 a
const mapA = new Map()
mapA.set({ b: 2 }, true)
mapA.set(false, { a: 1 })
mapA.set('key', [ 'value' ])
const setA = new Set()
setA.add(NaN)
setA.add('string')
setA.add([1, 2, 3, { c: 3 }])
setA.add(false)
const errorA = new Error('test error !')
errorA.name = 'Test'
const a = {
  num: 1,
  nan: NaN,
  str: 'abc',
  bool: true,
  arr: [1, 2, 3, { a: 1 }, setA, errorA],
  date: new Date(),
  obj: {
    bool: false,
    arr: [
      new Date(),
      'abc',
      '123',
      { reg: /^(,?[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?)$/g }
    ],
    map: mapA,
    sym
  },
  reg: /\./g,
  nul: null,
  undef: undefined
}

// 测试数据 b
const mapB = new Map()
mapB.set({ b: 2 }, true)
mapB.set(false, { a: 1 })
mapB.set('key', [ 'value' ])
const setB = new Set()
setB.add(NaN)
setB.add('string')
setB.add([1, 2, 3, { c: 3 }])
setB.add(false)
const errorB = new Error('test error !')
errorB.name = 'Test'
const b = {
  num: 1,
  nan: NaN,
  str: 'abc',
  bool: true,
  arr: [1, 2, 3, { a: 1 }, setB, errorB],
  date: new Date(),
  obj: {
    bool: false,
    arr: [
      new Date(),
      'abc',
      '123',
      { reg: /^(,?[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?)$/g }
    ],
    map: mapB,
    sym
  },
  reg: /\./g,
  nul: null,
  undef: undefined
}
console.log('isEqual:', isEqual(a, b))
// true

可以更改任意数据来测试false的情况,不再一一列举。

最终代码

请查看script部分 !

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿