Lodash源码阅读-equalArrays

Lodash 源码阅读-equalArrays

功能概述

equalArrays 是 Lodash 库中的一个内部工具函数,专门用于深度比较两个数组是否相等。它是 baseIsEqualDeep 函数的数组专用版本,支持部分深度比较和无序比较,能够处理循环引用等复杂情况。这个函数是 Lodash 中 _.isEqual 方法的核心组件之一,为 Lodash 提供了强大而灵活的深度相等性检查能力。

前置学习

依赖函数

  • arraySome:用于检查数组中是否有元素满足条件
  • cacheHas:检查缓存中是否存在特定键
  • SetCache:一个用于存储唯一值的缓存结构
  • Stack:用于跟踪遍历过的对象,处理循环引用

技术知识

  • 位掩码(Bitmask) :使用二进制位表示多个布尔标志
    • COMPARE_PARTIAL_FLAG(值为 1):表示进行部分比较
    • COMPARE_UNORDERED_FLAG(值为 2):表示进行无序比较
  • 循环引用检测:处理对象间的循环引用问题
  • 深度比较:递归比较嵌套数据结构
  • 自定义比较器:支持用户自定义的比较逻辑

源码实现

javascript 复制代码
/**
 * A specialized version of `baseIsEqualDeep` for arrays with support for
 * partial deep comparisons.
 *
 * @private
 * @param {Array} array The array to compare.
 * @param {Array} other The other array to compare.
 * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.
 * @param {Function} customizer The function to customize comparisons.
 * @param {Function} equalFunc The function to determine equivalents of values.
 * @param {Object} stack Tracks traversed `array` and `other` objects.
 * @returns {boolean} Returns `true` if the arrays are equivalent, else `false`.
 */
function equalArrays(array, other, bitmask, customizer, equalFunc, stack) {
  var isPartial = bitmask & COMPARE_PARTIAL_FLAG,
    arrLength = array.length,
    othLength = other.length;

  if (arrLength != othLength && !(isPartial && othLength > arrLength)) {
    return false;
  }
  // Check that cyclic values are equal.
  var arrStacked = stack.get(array);
  var othStacked = stack.get(other);
  if (arrStacked && othStacked) {
    return arrStacked == other && othStacked == array;
  }
  var index = -1,
    result = true,
    seen = bitmask & COMPARE_UNORDERED_FLAG ? new SetCache() : undefined;

  stack.set(array, other);
  stack.set(other, array);

  // Ignore non-index properties.
  while (++index < arrLength) {
    var arrValue = array[index],
      othValue = other[index];

    if (customizer) {
      var compared = isPartial
        ? customizer(othValue, arrValue, index, other, array, stack)
        : customizer(arrValue, othValue, index, array, other, stack);
    }
    if (compared !== undefined) {
      if (compared) {
        continue;
      }
      result = false;
      break;
    }
    // Recursively compare arrays (susceptible to call stack limits).
    if (seen) {
      if (
        !arraySome(other, function (othValue, othIndex) {
          if (
            !cacheHas(seen, othIndex) &&
            (arrValue === othValue ||
              equalFunc(arrValue, othValue, bitmask, customizer, stack))
          ) {
            return seen.push(othIndex);
          }
        })
      ) {
        result = false;
        break;
      }
    } else if (
      !(
        arrValue === othValue ||
        equalFunc(arrValue, othValue, bitmask, customizer, stack)
      )
    ) {
      result = false;
      break;
    }
  }
  stack["delete"](array);
  stack["delete"](other);
  return result;
}

实现思路

equalArrays 函数的实现思路可以分为以下几个关键步骤:

  1. 长度检查 :首先比较两个数组的长度,如果长度不同且不是部分比较模式(或部分比较模式下第二个数组不长于第一个数组),则直接返回 false

  2. 循环引用检查 :使用 stack 对象检查是否存在循环引用,如果两个数组已经在比较栈中,则直接比较它们的引用关系。

  3. 设置比较标记:将两个数组相互添加到比较栈中,标记它们正在被比较,以处理可能的循环引用。

  4. 元素比较

    • 如果提供了自定义比较器 customizer,则使用它进行比较
    • 如果是无序比较模式,使用 SetCachearraySome 检查每个元素是否在另一个数组中有匹配项
    • 如果是有序比较模式,直接按索引比较对应元素
  5. 清理和返回:从比较栈中移除两个数组,并返回比较结果。

这种实现方式既灵活又强大,能够处理各种复杂的数组比较场景,包括嵌套数组、循环引用和自定义比较逻辑。

源码解析

参数解析

javascript 复制代码
function equalArrays(array, other, bitmask, customizer, equalFunc, stack) {

函数接收六个参数:

  • array:要比较的第一个数组
  • other:要比较的第二个数组
  • bitmask:位掩码标志,控制比较的行为
  • customizer:自定义比较函数,允许用户定制比较逻辑
  • equalFunc:用于确定值是否相等的函数,通常是 baseIsEqual
  • stack:跟踪已遍历对象的栈,用于处理循环引用

位掩码处理

javascript 复制代码
var isPartial = bitmask & COMPARE_PARTIAL_FLAG,
  arrLength = array.length,
  othLength = other.length;

这里使用位运算从 bitmask 中提取标志:

  • isPartial:如果设置了 COMPARE_PARTIAL_FLAG(值为 1),则为 true,表示进行部分比较
  • 同时获取两个数组的长度,用于后续比较

长度检查

javascript 复制代码
if (arrLength != othLength && !(isPartial && othLength > arrLength)) {
  return false;
}

这个条件检查两种情况:

  1. 如果两个数组长度不同,且不是部分比较模式,则返回 false
  2. 如果是部分比较模式,但第二个数组长度小于第一个数组,也返回 false

这意味着在部分比较模式下,第二个数组可以比第一个数组长,但反之则不行。

循环引用检查

javascript 复制代码
var arrStacked = stack.get(array);
var othStacked = stack.get(other);
if (arrStacked && othStacked) {
  return arrStacked == other && othStacked == array;
}

这段代码检查两个数组是否已经在比较栈中(即它们是否已经被比较过):

  • 如果两个数组都已经在栈中,则检查它们是否互相引用对方
  • 这是处理循环引用的关键部分,防止无限递归

循环引用详解

循环引用是指数据结构中的元素引用了自身或其祖先元素,形成一个闭环。在 JavaScript 中,这种情况会导致常规的深度比较算法陷入无限递归,最终导致栈溢出错误。equalArrays函数通过巧妙的设计解决了这个问题。

循环引用检测机制

循环引用检测的核心思想是使用一个Stack数据结构来跟踪当前正在比较的对象对。stack对象维护了两个重要的映射:

  1. array -> other: 记录第一个数组映射到第二个数组
  2. other -> array: 记录第二个数组映射到第一个数组

当函数递归比较数组元素时,如果遇到已经在比较过程中的数组,就会触发循环引用检查。

关键的判断条件是:

javascript 复制代码
arrStacked == other && othStacked == array;

这个条件检查:

  • 当前的other是否就是之前为array记录的值
  • 当前的array是否就是之前为other记录的值

只有两个条件同时满足,才能确认这是一个结构相同的循环引用模式。

具体例子
例子 1:数组直接自引用

考虑两个简单的自引用数组:

javascript 复制代码
var arr1 = [];
arr1.push(arr1); // arr1 = [arr1]

var arr2 = [];
arr2.push(arr2); // arr2 = [arr2]

_.isEqual(arr1, arr2); // 返回 true

equalArrays比较这两个数组时:

  1. 初始阶段

    • arr1arr2添加到栈中:stack.set(arr1, arr2)stack.set(arr2, arr1)
  2. 元素比较

    • 比较arr1[0]arr2[0]
    • 因为arr1[0] === arr1arr2[0] === arr2,所以会递归调用equalFunc
  3. 循环引用检测

    • 当再次比较arr1arr2时,发现它们已经在栈中
    • 获取arrStacked = stack.get(arr1) = arr2
    • 获取othStacked = stack.get(arr2) = arr1
    • 检查arr2 === arr2[0]arr1 === arr1[0],两者都为true
    • 确认这是结构相同的循环引用,返回true
例子 2:嵌套数组循环引用

复杂一点的例子:

javascript 复制代码
var arr1 = [1, 2];
var subarr1 = [3, arr1];
arr1.push(subarr1); // arr1 = [1, 2, [3, arr1]]

var arr2 = [1, 2];
var subarr2 = [3, arr2];
arr2.push(subarr2); // arr2 = [1, 2, [3, arr2]]

_.isEqual(arr1, arr2); // 返回 true

比较过程:

  1. 比较头两个元素(1 和 2):完全相等

  2. 比较第三个元素(子数组):

    • 比较subarr1[0]subarr2[0]:两者都是 3,相等
    • 比较subarr1[1]subarr2[1]:递归调用equalFunc比较arr1arr2
  3. 递归比较时:

    • 发现arr1arr2已经在栈中
    • 检查stack.get(arr1) === arr2stack.get(arr2) === arr1
    • 两者都为true,确认循环引用结构相同,返回true
例子 3:不同结构的循环引用
javascript 复制代码
var arr1 = [];
arr1.push(arr1); // arr1 = [arr1]

var arr2 = [[]];
arr2[0].push(arr2); // arr2 = [[arr2]]

_.isEqual(arr1, arr2); // 返回 false

比较过程:

  1. 比较arr1[0]arr2[0]
    • arr1[0] === arr1,但arr2[0]是一个独立的数组,不等于arr2
    • 需要递归比较arr1arr2[0]
  2. 在递归比较过程中:
    • 虽然两者都有循环引用,但结构不同
    • 无法满足循环引用检测条件
    • 最终返回false
为什么设计成双向检查?

双向检查(arrStacked == other && othStacked == array)是为了确保循环引用的结构完全一致。单向检查可能会误判一些情况,例如:

javascript 复制代码
var a = [];
var b = [a];
a.push(b); // a引用b,b引用a

var c = [];
var d = [c];
c.push(d); // c引用d,d引用c

尽管ac都参与了循环引用,但它们的结构实际上是一致的。双向检查能够识别出这种对称性,确保结构相同的循环引用被正确地判定为相等。

Stack 的作用

Stack数据结构不仅用于循环引用检测,还承担了几个重要作用:

  1. 防止无限递归:避免陷入无限深的比较循环
  2. 识别等价结构:确定不同引用但结构相同的循环引用
  3. 提高性能:已比较过的对象对可以直接返回结果,无需重复比较

这种设计使得 Lodash 能够安全、高效地比较包含循环引用的复杂数据结构,是深度相等比较功能的关键部分。

初始化比较状态

javascript 复制代码
var index = -1,
  result = true,
  seen = bitmask & COMPARE_UNORDERED_FLAG ? new SetCache() : undefined;

stack.set(array, other);
stack.set(other, array);

这里初始化了比较所需的变量:

  • index:当前比较的索引,初始为 -1
  • result:比较结果,初始为 true
  • seen:如果是无序比较模式(设置了 COMPARE_UNORDERED_FLAG,值为 2),则创建一个 SetCache 实例用于跟踪已比较的元素

同时,将两个数组互相添加到比较栈中,标记它们正在被比较。

元素比较循环

javascript 复制代码
while (++index < arrLength) {
  var arrValue = array[index],
      othValue = other[index];

这个循环遍历数组的每个元素,逐一进行比较。

自定义比较器处理

javascript 复制代码
if (customizer) {
  var compared = isPartial
    ? customizer(othValue, arrValue, index, other, array, stack)
    : customizer(arrValue, othValue, index, array, other, stack);
}
if (compared !== undefined) {
  if (compared) {
    continue;
  }
  result = false;
  break;
}

如果提供了自定义比较器:

  • 根据是否是部分比较模式,调用比较器时参数顺序会有所不同
  • 如果比较器返回了明确的结果(不是 undefined):
    • 如果结果为真值,继续比较下一个元素
    • 如果结果为假值,设置 resultfalse 并跳出循环
为什么要改变参数顺序?

参数顺序的改变反映了比较的方向或重点:

  1. 完整比较模式isPartial = false):

    • 我们将array作为主要对象,other作为比较对象
    • 判断的是两个数组是否完全相等(双向比较)
    • 因此参数顺序是customizer(arrValue, othValue, ...)
  2. 部分比较模式isPartial = true):

    • 我们将other作为主要对象,array作为要检查的对象
    • 判断的是array是否是other的子集(单向比较)
    • 因此参数顺序是customizer(othValue, arrValue, ...)

这种设计使得自定义比较器在不同的比较模式下能保持一致的语义,即第一个参数总是"主要对象"的元素,第二个参数总是"比较对象"的元素。

无序比较模式

javascript 复制代码
if (seen) {
  if (!arraySome(other, function(othValue, othIndex) {
        if (!cacheHas(seen, othIndex) &&
            (arrValue === othValue || equalFunc(arrValue, othValue, bitmask, customizer, stack))) {
          return seen.push(othIndex);
        }
      })) {
    result = false;
    break;
  }
}

如果是无序比较模式(seen 存在):

  • 使用 arraySome 在第二个数组中查找与当前元素相等的元素
  • 如果找到匹配项,将其索引添加到 seen 缓存中,表示该元素已被匹配
  • 如果没有找到匹配项,设置 resultfalse 并跳出循环

这允许数组元素的顺序不同,只要所有元素都能找到对应的匹配项即可。

有序比较模式

javascript 复制代码
else if (!(
      arrValue === othValue ||
        equalFunc(arrValue, othValue, bitmask, customizer, stack)
    )) {
  result = false;
  break;
}

如果是有序比较模式(seen 不存在):

  • 直接比较对应索引位置的元素是否相等
  • 首先尝试使用严格相等(===)进行比较
  • 如果不相等,则使用 equalFunc(通常是 baseIsEqual)进行深度比较
  • 如果两种比较都失败,设置 resultfalse 并跳出循环

清理和返回结果

javascript 复制代码
stack["delete"](array);
stack["delete"](other);
return result;

最后,从比较栈中移除两个数组,并返回比较结果。这一步很重要,它确保了栈的正确状态,防止内存泄漏和错误的循环引用检测。

总结

equalArrays 是 Lodash 中一个强大而复杂的内部工具函数,专门用于深度比较数组是否相等。它支持多种比较模式,能够处理循环引用、嵌套结构和自定义比较逻辑,是 _.isEqual 方法的核心组件之一。

这个函数的实现体现了几个重要的编程原则和技术:

  1. 位掩码技术:使用二进制位表示多个布尔标志,提高参数传递的效率
  2. 循环引用处理:使用栈跟踪已比较的对象,防止无限递归
  3. 短路求值:一旦发现不相等的元素,立即返回结果,避免不必要的比较
  4. 灵活的比较策略:支持有序比较、无序比较和部分比较等多种模式
  5. 可扩展性:通过自定义比较器支持用户定制的比较逻辑

理解 equalArrays 的工作原理,有助于我们更好地使用 Lodash 的 _.isEqual 方法,并在自己的代码中实现更高效、更可靠的深度比较逻辑。

相关推荐
大土豆的bug记录1 小时前
鸿蒙进行视频上传,使用 request.uploadFile方法
开发语言·前端·华为·arkts·鸿蒙·arkui
maybe02091 小时前
前端表格数据导出Excel文件方法,列自适应宽度、增加合计、自定义文件名称
前端·javascript·excel·js·大前端
HBR666_1 小时前
菜单(路由)权限&按钮权限&路由进度条
前端·vue
A-Kamen2 小时前
深入理解 HTML5 Web Workers:提升网页性能的关键技术解析
前端·html·html5
锋小张3 小时前
a-date-picker 格式化日期格式 YYYY-MM-DD HH:mm:ss
前端·javascript·vue.js
鱼樱前端4 小时前
前端模块化开发标准全面解析--ESM获得绝杀
前端·javascript
yanlele4 小时前
前端面试第 75 期 - 前端质量问题专题(11 道题)
前端·javascript·面试
就是有点傻5 小时前
C#中Interlocked.Exchange的作用
java·javascript·c#
前端小白۞5 小时前
el-date-picker时间范围 编辑回显后不能修改问题
前端·vue.js·elementui
拉不动的猪5 小时前
刷刷题44(uniapp-中级)
前端·javascript·面试