Lodash 源码解读与原理分析 - 数组那些事儿

47 阅读17分钟

Lodash 作为前端开发的核心工具库,其数组函数之所以能兼顾高性能与高鲁棒性,并非简单封装原生 API,而是从算法选型、内存管理、执行效率、边界兼容四个维度进行系统性优化。本文先拆解贯穿所有数组函数的通用优化策略,再逐一分析核心函数的实现细节,完整揭示 Lodash 高性能的底层逻辑。

一、Lodash 数组函数的通用性能优化策略

Lodash 所有数组函数都遵循一套统一的优化范式,这些范式是其高性能的核心基石,具体包含 8 个核心方向:

1. 边界检查与早期返回:避免无效计算

核心逻辑:函数执行初期就对空值、无效输入、边界场景做判断,直接返回结果,避免进入核心逻辑的无效遍历 / 计算。实现方式

  • 优先校验 array 是否为 null/undefined,安全获取数组长度(var length = array == null ? 0 : array.length);

  • 对空数组、无效参数(如 size < 1depth < 0)直接返回空数组 / 原数组,不执行后续逻辑;

  • 对非数组 / 类数组输入,直接返回空数组或原值,避免属性访问报错。

    价值:减少无效的循环、函数调用、内存分配,提升函数冷启动速度,同时增强鲁棒性。

    // 通用边界检查模板
    function baseFunction(array) {
      // 安全获取长度:处理null/undefined
      var length = array == null ? 0 : array.length;
      // 早期返回:空数组直接返回结果
      if (!length) {
        return [];
      }
      // 核心逻辑(省略)
    }
    

2. 内存预分配:避免动态扩容的性能损耗

核心逻辑:对可预知长度的结果数组,提前分配内存空间,替代 push 动态扩容的方式。

实现方式

  • 通过计算目标长度(如分块数 nativeCeil(length / size)、切片长度 end - start)创建固定长度数组;
  • 用索引赋值替代 push,避免数组长度翻倍扩容的内存重分配;
  • 仅在长度不可预知时(如过滤类函数)使用 push,但仍通过双指针优化填充效率。

价值:减少 JavaScript 引擎的内存重分配和垃圾回收(GC)压力,大数组场景下性能提升 20%-30%。

// chunk 中的内存预分配
var result = Array(nativeCeil(length / size)); // 预分配分块结果数组
// baseSlice 中的内存预分配
var result = Array(length); // 预分配切片结果数组

3. 算法分层选型:按数据规模选择最优算法

核心逻辑:根据数组长度、有序性、数据类型选择不同算法,平衡「初始化开销」和「遍历效率」。

实现方式

  • 有序数组:使用二分查找(时间复杂度 O (log n))替代线性查找(O (n)),如 sortedIndex
  • 大数组(≥200):使用 Set/SetCache 缓存(查找 O (1))替代数组查找(O (n)),如 uniq
  • 小数组(<200):使用普通数组查找,避免 Set 初始化的额外开销;
  • 多数组操作(如交集、差集):基于最短数组遍历,减少迭代次数。

价值:不同数据规模下都能达到最优执行效率,大数组场景性能提升 5-50 倍。

// uniq 中的分层策略
if (length >= LARGE_ARRAY_SIZE) { // LARGE_ARRAY_SIZE = 200
  var set = iteratee ? null : createSet(array); // 大数组用Set
  if (set) return setToArray(set);
} else {
  seen = iteratee ? [] : result; // 小数组用普通数组
}

4. 位运算优化:替代算术运算提升执行速度

核心逻辑:用位运算替代 Math.floor、正负判断、整数转换等算术运算,利用 CPU 直接执行的特性提升效率。实现方式

  • 计算中间索引:var mid = (low + high) >>> 1(替代 Math.floor((low+high)/2));
  • 确保正整数:start >>>= 0(替代 Math.max(start, 0));
  • 计算长度:length = (end - start) >>> 0(避免负数,替代 Math.max(end - start, 0))。

价值:位运算比算术运算快 10%-20%,且能避免浮点运算、整数溢出问题。

// sortedIndex 中的位运算优化
var mid = (low + high) >>> 1; // 无浮点、无溢出的中间索引计算

5. 循环优化:最小化迭代器开销

核心逻辑:通过 while 循环 + 双指针替代 for 循环,减少迭代过程中的变量声明、属性访问、冗余计算。

实现方式

  • 优先使用 while 循环:缓存数组长度(仅计算 1 次),避免每次迭代访问 array.length
  • 前置自增(++index):替代后置自增(index++),减少临时变量存储;
  • 双指针遍历:用两个指针分别遍历原数组(index)和填充结果数组(resIndex),减少长度计算;
  • 短路循环:通过 continue outer 跳过无效迭代,减少比较次数。

价值:循环开销降低 10%-30%,高频遍历场景效果显著。

// compact 中的 while 循环 + 双指针
var index = -1, resIndex = 0;
while (++index < length) { // 前置自增,仅比较变量
  var value = array[index];
  if (value) {
    result[resIndex++] = value; // 双指针填充
  }
}

6. 缓存机制:将 O (n) 查找降为 O (1)

核心逻辑:对频繁查找的场景,用缓存结构存储已遍历值,避免重复遍历比较。

实现方式

  • 大数组场景:使用 SetCache(Lodash 封装的 Set 兼容版)缓存已遍历值,查找时间复杂度 O (1);
  • 小数组场景:复用结果数组作为缓存,减少额外内存分配;
  • 多数组操作:缓存第一个数组的所有值,后续数组仅做查找对比。

价值:查找效率提升一个数量级,大数组去重 / 交集场景性能提升数倍。

// baseUniq 中的缓存机制
seen = new SetCache; // O(1) 查找缓存
if (!includes(seen, computed, comparator)) { // 缓存查找
  seen.push(computed);
  result.push(value);
}

7. 原生方法复用:利用引擎底层优化

核心逻辑:对浏览器已高度优化的原生方法,直接封装复用,避免手写逻辑的性能损耗。

实现方式

  • 复用 Array.prototype.push/reverse/slice 等原生方法,如 nativeReverse.call(array)
  • 封装原生方法为内部函数(如 arrayPush 封装 push),统一兼容处理;
  • 仅在原生方法有缺陷时(如 indexOf 不支持 NaN)手写替代逻辑。

价值:原生方法由浏览器引擎用 C++ 实现,执行效率远高于手写 JavaScript 逻辑。

// reverse 中的原生方法复用
function reverse(array) {
  return array == null ? array : nativeReverse.call(array);
}

8. 迭代器归一化:兼顾易用性与性能

核心逻辑:将函数、对象、字符串等多种迭代器格式统一为标准函数,避免多次类型判断。

实现方式

  • 通过 getIterateepredicate 转换为标准迭代函数(支持 _.filter(array, {active: true}) 等易用写法);
  • 迭代器仅初始化一次,复用在整个循环中,避免每次迭代都做类型判断;
  • 统一迭代器参数(value, index, array),保证逻辑一致性。

价值:在不损失性能的前提下,提升 API 易用性,减少用户手写转换逻辑。

// remove 中的迭代器归一化
predicate = getIteratee(predicate, 3); // 统一为标准函数
while (++index < length) {
  if (predicate(value, index, array)) { // 复用迭代器
    // 逻辑处理
  }
}

二、Lodash 核心数组函数的实现与优化细节

基于上述通用策略,Lodash 为不同场景的数组函数设计了针对性的优化方案,以下是核心函数的完整实现与解析:

1. 基础操作函数:易用性与效率的平衡

_.chunk:数组分块的内存优化典范

功能:将数组分割为指定大小的二维数组,空数组 / 无效参数返回空数组。

function chunk(array, size, guard) {
  // 1. 边界兼容:处理参数异常(防误传、未传size)
  if ((guard ? isIterateeCall(array, size, guard) : size === undefined)) {
    size = 1; // 默认分块大小为1
  } else {
    // 转为整数并确保最小值为0(避免负数分块)
    size = nativeMax(toInteger(size), 0);
  }

  // 2. 安全获取长度 + 早期返回:空数组/无效size直接返回
  var length = array == null ? 0 : array.length;
  if (!length || size < 1) {
    return [];
  }

  // 3. 内存预分配:计算分块数并创建固定长度数组
  var index = 0,
      resIndex = 0,
      result = Array(nativeCeil(length / size)); // 向上取整确定分块数

  // 4. 循环优化:while循环 + 指针更新,减少迭代开销
  while (index < length) {
    // 复用baseSlice切片,同时更新索引(index += size)
    result[resIndex++] = baseSlice(array, index, (index += size));
  }
  return result;
}

专属优化点

  • 分块数预计算:通过 nativeCeil(length / size) 精准预分配结果数组长度;
  • 索引联动更新:index += size 一次操作完成指针移动,避免重复计算;
  • 底层逻辑复用:基于 baseSlice 实现切片,避免重复造轮子。

_.compact:假值过滤的极致简洁

功能:移除数组中的假值(false、null、0、""、undefined、NaN),返回新数组。

function compact(array) {
  // 1. 安全初始化:处理null/undefined,缓存长度
  var index = -1,
      length = array == null ? 0 : array.length,
      resIndex = 0,
      result = [];

  // 2. 循环优化:while + 双指针,减少属性访问
  while (++index < length) {
    var value = array[index];
    // 3. 快速判空:直接判断值,比Boolean(value)更高效
    if (value) {
      result[resIndex++] = value; // 双指针填充,避免push扩容
    }
  }
  return result;
}

专属优化点

  • 快速假值判断:if (value) 直接校验,省去 Boolean() 函数调用开销;
  • 双指针填充:resIndex 独立控制结果数组索引,避免 result.length 访问。

_.concat:多参数拼接的灵活实现

功能:拼接多个数组 / 值,返回新数组,支持类数组、单值输入。

function concat() {
  // 1. 早期返回:无参数直接返回空数组
  var length = arguments.length;
  if (!length) {
    return [];
  }

  // 2. 参数预处理:批量处理arguments,避免多次类数组操作
  var args = Array(length - 1),
      array = arguments[0],
      index = length;

  // 反向遍历:高效将arguments转为数组
  while (index--) {
    args[index - 1] = arguments[index];
  }

  // 3. 核心逻辑:原生复用 + 扁平化控制
  // - isArray判断:数组拷贝,非数组转为数组
  // - baseFlatten:仅扁平化1层,平衡灵活性与性能
  // - arrayPush:封装原生push,利用引擎优化
  return arrayPush(
    isArray(array) ? copyArray(array) : [array],
    baseFlatten(args, 1)
  );
}

专属优化点

  • 反向遍历参数:while (index--) 高效处理 arguments,避免 Array.from 开销;
  • 扁平化控制:仅扁平化 1 层,避免过度遍历,同时兼容多参数输入。

2. 查找与索引函数:算法层面的性能突破

_.findIndex:条件查找的精准控制

功能:查找第一个满足条件的元素索引,支持起始位置,无匹配返回 -1。

function findIndex(array, predicate, fromIndex) {
  // 1. 安全获取长度 + 早期返回:空数组直接返回-1
  var length = array == null ? 0 : array.length;
  if (!length) {
    return -1;
  }

  // 2. 起始索引校准:处理负数、非数字索引
  var index = fromIndex == null ? 0 : toInteger(fromIndex);
  if (index < 0) {
    // 负数索引转为合法正索引,最小为0
    index = nativeMax(length + index, 0);
  }

  // 3. 迭代器归一化 + 底层复用:
  // - getIteratee:统一迭代器格式(函数/对象/字符串)
  // - baseFindIndex:封装核心查找,支持正序/倒序复用
  return baseFindIndex(array, getIteratee(predicate, 3), index);
}

专属优化点

  • 起始位置校准:自动处理负数索引,无需用户手动转换;
  • 底层逻辑抽象:baseFindIndex 同时支撑 findIndex/findLastIndex,避免重复代码。

_.sortedIndex:有序数组的二分查找

功能:在有序数组中查找值的插入位置,保证数组有序性,仅支持数字 / 可比较类型。

// 对外简化接口
function sortedIndex(array, value) {
  return baseSortedIndex(array, value);
}

// 底层核心实现
function baseSortedIndex(array, value, retHighest) {
  // 1. 初始化二分边界:处理null/undefined
  var low = 0,
      high = array == null ? low : array.length;

  // 2. 数字类型特化:跳过无效值,提升效率
  if (typeof value == 'number' && value === value && high <= HALF_MAX_ARRAY_LENGTH) {
    // 3. 二分查找核心:位运算优化 + 短路判断
    while (low < high) {
      // 位运算计算中间索引:无浮点、无溢出
      var mid = (low + high) >>> 1,
          computed = array[mid];

      // 短路判断:跳过Symbol/null,快速比较
      if (computed !== null && !isSymbol(computed) &&
          (retHighest ? (computed <= value) : (computed < value))) {
        low = mid + 1; // 目标在右半部分
      } else {
        high = mid; // 目标在左半部分
      }
    }
    return high;
  }

  // 非数字/超大数组:通用实现兜底
  return baseSortedIndexBy(array, value, identity, retHighest);
}

专属优化点

  • 二分查找选型:时间复杂度 O (log n),大数组比线性查找快 5-10 倍;
  • 数字特化处理:跳过 Symbol/null 等无效值,减少判断开销;
  • 长度限制:HALF_MAX_ARRAY_LENGTH 避免超大数组的性能问题。

_.indexOf:值查找的兼容性优化

功能:查找值的第一个索引,支持起始位置,兼容 NaN 查找。

function indexOf(array, value, fromIndex) {
  // 1. 安全获取长度 + 早期返回:空数组直接返回-1
  var length = array == null ? 0 : array.length;
  if (!length) {
    return -1;
  }

  // 2. 起始索引校准:处理负数、非数字
  var index = fromIndex == null ? 0 : toInteger(fromIndex);
  if (index < 0) {
    index = nativeMax(length + index, 0);
  }

  // 3. 底层复用:兼容NaN(原生indexOf不支持)
  return baseIndexOf(array, value, index);
}

专属优化点

  • NaN 兼容:baseIndexOf 专门处理 NaN 查找,弥补原生 API 缺陷;
  • 索引校准:自动处理负数起始位置,提升易用性。

3. 修改与操作函数:原地 / 非原地的平衡

_.pullAll(支撑 _.pull):原地移除指定值

功能:从原数组中移除指定值,直接修改原数组,返回修改后的数组。

function pullAll(array, values) {
  // 1. 早期返回:空数组/空值列表直接返回原数组
  if (!(array && array.length && values && values.length)) {
    return array;
  }

  // 2. 批量处理:调用底层逻辑,避免多次遍历
  return basePullAll(array, values);
}

专属优化点

  • 原地修改:直接操作原数组,减少新数组创建的内存开销;
  • 批量处理:一次性移除多个值,避免多次遍历数组。

_.remove:条件移除的高效实现

功能:移除满足条件的元素,返回被移除的数组,原数组被修改。

function remove(array, predicate) {
  // 1. 初始化 + 早期返回:空数组返回空结果
  var result = [];
  if (!(array && array.length)) {
    return result;
  }

  // 2. 缓存长度 + 迭代器归一化
  var index = -1,
      indexes = [],
      length = array.length;
  predicate = getIteratee(predicate, 3);

  // 3. 双数组记录:先收集索引/值,避免边遍历边删除的索引错乱
  while (++index < length) {
    var value = array[index];
    if (predicate(value, index, array)) {
      result.push(value); // 收集被移除值
      indexes.push(index); // 收集被移除索引
    }
  }

  // 4. 批量移除:一次性处理所有索引,减少数组重排次数
  basePullAt(array, indexes);
  return result;
}

专属优化点

  • 双数组记录:先收集后移除,避免边遍历边删除导致的元素漏判;
  • 批量移除:basePullAt 一次性处理索引,减少数组元素移动的开销。

_.reverse:原生能力的安全封装

功能:反转数组,直接修改原数组,兼容 null/undefined 输入。

function reverse(array) {
  // 1. 安全兼容:null/undefined直接返回,避免报错
  // 2. 原生复用:调用浏览器优化的reverse方法
  return array == null ? array : nativeReverse.call(array);
}

专属优化点

  • 原生方法复用:直接调用 Array.prototype.reverse,利用引擎 C++ 实现的高性能;
  • 无额外开销:仅做安全判断,无多余计算。

4. 集合操作函数:数据结构驱动的性能提升

_.uniq:数组去重的分层优化

功能:数组去重,返回新数组,兼容 NaN、0/-0 等特殊值。

// 对外简化接口
function uniq(array) {
  // 早期返回:空数组/非数组直接返回空数组
  return (array && array.length) ? baseUniq(array) : [];
}

// 底层核心去重
function baseUniq(array, iteratee, comparator) {
  // 1. 初始化变量:缓存长度,默认查找函数
  var index = -1,
      includes = arrayIncludes,
      length = array.length,
      isCommon = true,
      result = [],
      seen = result;

  // 2. 比较器处理:自定义比较器切换查找逻辑
  if (comparator) {
    isCommon = false;
    includes = arrayIncludesWith;
  }
  // 3. 分层策略:大数组用Set,小数组用普通数组
  else if (length >= LARGE_ARRAY_SIZE) { // LARGE_ARRAY_SIZE = 200
    // 无迭代器时直接用Set(最快)
    var set = iteratee ? null : createSet(array);
    if (set) {
      return setToArray(set);
    }
    isCommon = false;
    includes = cacheHas;
    seen = new SetCache; // O(1) 查找缓存
  } else {
    // 小数组:复用结果数组作为缓存
    seen = iteratee ? [] : result;
  }

  // 4. 核心遍历:短路逻辑 + 特殊值处理
  outer:
  while (++index < length) {
    var value = array[index],
        computed = iteratee ? iteratee(value) : value;

    // 特殊值处理:0和-0视为相同
    value = (comparator || value !== 0) ? value : 0;

    // 普通场景:反向遍历缓存,减少比较次数
    if (isCommon && computed === computed) {
      var seenIndex = seen.length;
      while (seenIndex--) {
        if (seen[seenIndex] === computed) {
          continue outer; // 短路:跳过重复值
        }
      }
      if (iteratee) {
        seen.push(computed);
      }
      result.push(value);
    }
    // 非普通场景/NaN:缓存查找
    else if (!includes(seen, computed, comparator)) {
      if (seen !== result) {
        seen.push(computed);
      }
      result.push(value);
    }
  }
  return result;
}

专属优化点

  • 分层去重:200 为阈值,平衡 Set 初始化开销和查找效率;
  • 特殊值兼容:处理 NaN、0/-0,弥补原生 Set 的部分缺陷;
  • 反向遍历缓存:减少缓存比较次数,提升小数组去重效率。

_.intersection:多数组交集的缓存优化

功能:计算多个数组的交集,返回新数组,仅保留所有数组都包含的元素。

var intersection = baseRest(function(arrays) {
  // 1. 参数归一化:转为数组/类数组,过滤无效输入
  var mapped = arrayMap(arrays, castArrayLikeObject);
  
  // 2. 早期返回:无有效输入返回空数组
  return (mapped.length && mapped[0] === arrays[0])
    ? baseIntersection(mapped)
    : [];
});

专属优化点

  • 最短数组优先:基于最短数组遍历,减少迭代次数;
  • 缓存复用:对大数组创建 SetCache,查找效率从 O (n) 降为 O (1);
  • 参数归一化:统一处理类数组,提升兼容性。

_.xor:对称差集的高效计算

功能:计算多个数组的对称差集,返回仅出现在一个数组中的元素。

var xor = baseRest(function(arrays) {
  // 1. 过滤无效输入:仅保留数组/类数组
  // 2. 底层复用:基于baseDifference实现,避免重复逻辑
  return baseXor(arrayFilter(arrays, isArrayLikeObject));
});

专属优化点

  • 差集复用:基于 baseDifference 实现核心逻辑,减少代码冗余;
  • 批量计算:一次性处理所有输入,减少中间数组创建。

5. 其他高频函数:细节处的性能打磨

_.head/_.last:首尾元素的直接访问

功能:快速获取数组第一个 / 最后一个元素,兼容空数组、非数组输入。

// 获取第一个元素
function head(array) {
  // 直接访问:无函数调用开销,空数组返回undefined
  return (array && array.length) ? array[0] : undefined;
}

// 获取最后一个元素
function last(array) {
  // 安全获取长度:处理null/undefined
  var length = array == null ? 0 : array.length;
  // 直接访问:避免slice(-1)[0]的额外开销
  return length ? array[length - 1] : undefined;
}

专属优化点

  • 直接索引访问:比 array.slice(0,1)[0] 快数倍,无额外内存分配;
  • 无冗余计算:仅做必要的长度判断,无循环 / 函数调用。

_.flatten/_.flattenDepth:数组扁平化的深度控制

功能:将嵌套数组扁平化为一维(flatten)或指定深度(flattenDepth),返回新数组。

// 扁平化1层(默认)
function flatten(array) {
  var length = array == null ? 0 : array.length;
  // 早期返回:空数组直接返回,否则调用底层逻辑(深度1)
  return length ? baseFlatten(array, 1) : [];
}

// 支持指定深度
function flattenDepth(array, depth) {
  var length = array == null ? 0 : array.length;
  if (!length) {
    return [];
  }
  // 深度校准:未传depth默认1,转为整数确保合法
  depth = depth === undefined ? 1 : toInteger(depth);
  // 底层复用:迭代实现,避免递归栈溢出
  return baseFlatten(array, depth);
}

专属优化点

  • 深度控制:避免过度扁平化,减少无效遍历;
  • 迭代代替递归:baseFlatten 用迭代实现,避免深层嵌套导致的栈溢出;
  • 类型校验:仅扁平化数组 / 类数组,跳过普通对象。

_.without:排除指定值的差集实现

功能:创建排除指定值的新数组,原数组不变,支持多值排除。

var without = baseRest(function(array, values) {
  // 1. 类型校验:仅处理数组/类数组,否则返回空数组
  // 2. 底层复用:基于baseDifference实现差集计算
  return isArrayLikeObject(array)
    ? baseDifference(array, values)
    : [];
});

专属优化点

  • 差集复用:直接使用 baseDifference 核心逻辑,避免重复代码;
  • 可变参数:通过 baseRest 支持多值输入,提升易用性;
  • 非破坏性:返回新数组,不修改原数组,兼顾不可变需求。

三、核心总结

Lodash 数组函数的高性能,是「通用优化策略 + 场景化定制」的双重结果,核心关键点可总结为:

  1. 性能分层设计:根据数组大小、有序性选择算法(二分 / Set / 线性),避免「一刀切」的低效;
  2. 内存效率优先:预分配内存、原地修改、缓存复用,减少 GC 压力和内存重分配;
  3. 细节极致打磨:位运算、while 循环、快速判空等小优化,叠加后产生显著效果;
  4. 兼容与性能平衡:在鲁棒性(兼容 null/NaN/ 负数索引)和性能之间找到最优解,不牺牲易用性。

这些优化思路不仅适用于数组操作,更是前端高性能编程的通用准则 —— 掌握这些逻辑,既能更高效地使用 Lodash,也能在手写代码时写出兼具性能与鲁棒性的逻辑。