请实践 lodash 的深比较 (isEqual)

2022年12月26日

💎 加入 E+ 成長計畫 與超過 350+ 位軟體工程師一同在社群中成長,並且獲得更多的軟體工程學習資源

在写程式时,很常会需要比较两个不同的物件是否有相同的值,这种比较又被称为深比较 (deep comparison)。举例来说,lodash 函式库就有提供 isEqual 这个协助深比较的函式。以下面的例子来说,objectother   是两个不同的物件,所以直接比较 object === other 会是 false 。但因为两个物件里面的值是一样的,所以透过 isEqual 来比较则会回传 ture

const object = { a: 1, b: [2, 3] };
const other = { a: 1, b: [2, 3] };

_.isEqual(object, other);
// => true

object === other;
// => false

虽然在平时工作,可以透过 lodash 等函式库,轻松引用这个方法。但是在面试中,只会用是不够的,因为面试官会问你如何自己手写来实践深比较。以下让我们一起来看如何手写吧!

手写深比较

下面是深比较的手写,假如不确定为什么是这样写,往下滑会有针对代码详细说明的版本

function isEqual(value, other) {
  if (typeof value !== "object" && typeof other !== "object") {
    const isValueNaN = Number.isNaN(value);
    const isOtherNaN = Number.isNaN(other);

    if (isValueNaN && isOtherNaN) {
      return true;
    }

    return value === other;
  }

  if (value === null && other === null) {
    return true;
  }

  if (typeof value !== typeof other) {
    return false;
  }

  if (value === other) {
    return true;
  }

  if (Array.isArray(value) && Array.isArray(other)) {
    if (value.length !== other.length) {
      return false;
    }

    for (let i = 0; i < value.length; i++) {
      if (!isEqual(value[i], other[i])) {
        return false;
      }
    }

    return true;
  }

  if (Array.isArray(value) || Array.isArray(other)) {
    return false;
  }

  if (Object.keys(value).length !== Object.keys(other).length) {
    return false;
  }

  for (const [k, v] of Object.entries(value)) {
    if (!(k in other)) {
      return false;
    }

    if (!isEqual(v, other[k])) {
      return false;
    }
  }

  return true;
}

以下是有注解详细说明的版本

function isEqual(value, other) {
  // 先处理两个值都是原始型别 (primitive type) 的状况
  if (typeof value !== "object" && typeof other !== "object") {
    const isValueNaN = Number.isNaN(value);
    const isOtherNaN = Number.isNaN(other);

    // 在 JavaScript 中,NaN 是唯一不等于自己的值,所以要特别处理
    if (isValueNaN && isOtherNaN) {
      return true;
    }

    return value === other;
  }

  // 虽然 null 在 JavaScript 是原始型别,但因为一些历史因素,
  // 导致 null 的型别是 object,所以我们需要额外处理 null 的状况
  if (value === null && other === null) {
    return true;
  }

  // 处理完两个都是原始型别,接着判断只有一个是原始型别,
  // 假如一个是原始型别,另一个是物件型别,那就回传 false
  if (typeof value !== typeof other) {
    return false;
  }

  // 假如通过以上的条件,代表两个值都是物件型别,
  // 所以我们先比较两个物件,如果是来自同个址 (reference) 则回传 true
  if (value === other) {
    return true;
  }

  // 接着,当这两个物件都是数组时的状况
  if (Array.isArray(value) && Array.isArray(other)) {
    // 如果两个数组长度不同,则回传 false
    if (value.length !== other.length) {
      return false;
    }
    // 迭代数组中的每个值,然后递回地用 isEqual 比较两个值
    for (let i = 0; i < value.length; i++) {
      if (!isEqual(value[i], other[i])) {
        return false;
      }
    }

    return true;
  }

  // 只有一个是数组,但另一个不适的状况,则回传 false
  // 因为上面的 && 没有成立,代表不会两个都是数组,这边用 || 就能判断是否有一个是数组
  if (Array.isArray(value) || Array.isArray(other)) {
    return false;
  }

  // 如果上面的条件中都没有回传,剩下的可能性是两个值都是物件
  // 先检查两个物件的 key 一样多,不一样多代表两个物件一定不同
  if (Object.keys(value).length !== Object.keys(other).length) {
    return false;
  }

  // 假如两个物件有一样多的 key 透过 Object.entires 迭代过第一个物件
  for (const [k, v] of Object.entries(value)) {
    // 如果第一个物件中的某个 key 不存在于第二个物件,代表两者不同
    if (!(k in other)) {
      return false;
    }

    // 如果第一个物件中的键值对与第二个的不同,也代表两个物件不同
    // 切记,因为值可能也是某个物件,所以要用 isEqual 递回地检查是否两个值一样
    if (!isEqual(v, other[k])) {
      return false;
    }
  }

  return true;
}
🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們