Lodash 源码解读与原理分析 - 集合那些事儿

18 阅读24分钟

Lodash 作为 JavaScript 生态最经典的工具库之一,其集合函数(遍历、过滤、聚合、排序等)的高性能并非偶然,而是基于对 JavaScript 语言特性、引擎优化机制、数据结构与算法的深度理解,通过「分层设计、精准适配、极致抠细节」实现的。本文先系统性梳理集合函数通用的优化策略(覆盖所有底层手段),再逐一拆解每个函数的实现逻辑与优化落地,兼顾源码细节与设计思考。

第一部分:Lodash 集合函数通用优化策略

Lodash 所有集合函数(forEach、filter、groupBy 等)均基于一套统一的优化方法论构建,这些策略贯穿底层到上层,是性能优势的核心来源,可分为 10 大维度:

一、底层架构优化

核心思路:通过「高阶函数封装骨架、底层函数实现核心、上层函数暴露 API」的分层设计,减少代码冗余,同时保证逻辑一致性,降低维护与执行开销。

  1. 高阶函数封装聚合骨架:针对聚合类函数(groupBy、countBy、keyBy、partition),提取通用聚合逻辑到 createAggregator,仅对外暴露「聚合规则回调」,实现「一处修改,多处复用」。避免为每个聚合函数重复编写遍历、边界检查、迭代器处理逻辑,减少代码体积与执行时的编译开销。

  2. 底层基础函数复用:提取所有集合函数共用的核心逻辑为基础函数(baseEach、baseMap、baseReduce、baseIndexOf 等),上层函数仅根据场景做「类型分支」和「参数适配」。例如,arrayEach、objectEach 均基于 baseEach 扩展,避免重复实现遍历逻辑。

  3. 迭代器标准化封装:通过 getIteratee 统一处理用户传入的迭代器(支持函数、字符串、对象、数组等形式),将非函数迭代器转为标准化函数(如 _.forEach(arr, 'name') 转为 item => item.name)。既提升 API 易用性,又避免在每个函数中重复处理迭代器类型判断,减少冗余计算。

二、类型适配优化

核心思路:JavaScript 中数组、对象、类数组(arguments、NodeList)、字符串的遍历/操作效率差异极大,且 V8 引擎对数组有专门优化(如快数组、慢数组区分),Lodash 通过精准类型判断,为不同数据类型选择最优执行路径。

  1. 类型分支优先:所有集合函数第一步均做类型判断,拆分「数组专属逻辑」与「对象/类数组逻辑」。例如 forEach 中,数组用 arrayEach(基于 for 循环,V8 可优化为快速遍历),对象用 baseEach(基于 for...in + hasOwnProperty,避免原型链污染),而非用统一逻辑兼容所有类型。

  2. 类数组特殊处理:对 arguments、NodeList 等类数组对象,先通过 isArrayLike 判断,再转为数组或直接按数组逻辑处理(避免用 for...in 遍历类数组,效率极低)。例如 includes 中,先将非数组类数组转为数组,再用 baseIndexOf 查找。

  3. 字符串单独适配:字符串虽属于类数组,但遍历和查找逻辑与普通数组不同(如字符串 indexOf 是引擎优化方法),Lodash 为字符串单独设计分支。例如 includes 中,字符串直接调用原生 indexOf,而非转为数组处理,减少类型转换开销。

三、内存管理优化

核心思路:JavaScript 数组动态扩容、临时对象创建会触发内存分配与垃圾回收(GC),高频场景下会严重影响性能。Lodash 通过预分配、复用容器、减少临时对象等手段,优化内存使用效率。

  1. 结果数组预分配:对可预估结果长度的函数(map、filter、invokeMap 等),根据原集合长度提前创建结果数组,避免动态 push 导致的扩容。例如 invokeMap 中,result = isArrayLike(collection) ? Array(collection.length) : [],后续直接通过索引赋值,再截断多余长度,比动态 push 快 20%-40%(大数组场景)。

  2. 容器复用与初始化优化:聚合类函数(groupBy、countBy)初始化结果容器时,直接创建空对象/数组,避免临时变量;partition 函数预分配两个空数组作为结果,而非中途创建,减少内存分配次数。

  3. 避免不必要的类型转换:对无需修改原集合的函数,尽量直接操作原集合(或浅拷贝),避免深拷贝带来的内存开销。例如 shuffle 中,仅浅拷贝原集合到新数组,再进行打乱,而非深拷贝元素。

  4. 减少临时变量创建:核心遍历逻辑中,尽量复用变量(如缓存集合长度、索引值),避免在循环内创建临时变量。例如 for 循环中,提前缓存 len = collection.length,而非每次循环都读取 collection.length;前置自增(++index)代替后置自增(index++),减少临时变量存储。

四、算法选择优化

核心思路:不同场景下,算法的时间复杂度差异显著。Lodash 为每个函数选择适配场景的最优算法,避免用通用算法牺牲性能。

  1. 洗牌算法优化:shuffle 采用 Fisher-Yates 洗牌算法(时间复杂度 O(n)),而非原生 sort + 随机数(O(n log n)),且保证随机无偏。该算法通过一次遍历交换元素,无需额外空间,效率远超通用排序洗牌。

  2. 查找算法适配:数组查找用 baseIndexOf(基于 for 循环,支持从指定索引开始),字符串查找用原生 indexOf(引擎优化方法),对象查找用 for...in + hasOwnProperty,避免全场景用同一查找逻辑。

  3. 排序算法优化:sortBy、orderBy 基于归并排序实现(稳定排序,时间复杂度 O(n log n)),而非原生 sort(不稳定,且不同引擎实现差异大)。同时支持多字段排序,通过扁平化迭代器参数,减少排序前的预处理开销。

五、执行流程优化

核心思路:对无需遍历全部元素即可得到结果的场景,通过短路逻辑提前终止执行,避免无效遍历,最大程度减少计算量。

  1. 条件短路优化:every(全量满足)、some(部分满足)、find、findLast 均实现短路逻辑。every 只要遇到不满足条件的元素立即返回 false;some 只要遇到满足条件的元素立即返回 true;find/findLast 找到目标元素后立即返回,不遍历剩余元素(大集合场景下性能提升显著,极端情况可减少 99% 遍历量)。

  2. 早期返回处理:所有函数均优先处理边界情况(空集合、null/undefined 输入),直接返回默认结果(空数组、0、undefined),避免进入后续遍历逻辑。例如 size 函数中,if (collection == null) return 0,无需后续类型判断与长度计算。

  3. 深度控制终止:flatMapDepth 中,通过 depth 参数递减控制扁平化深度,当 depth=0 时停止扁平化,避免过度遍历嵌套数组,减少无效递归。

六、属性操作优化

核心思路:对象属性操作需兼顾「避免原型链污染」与「执行效率」,Lodash 通过精准的属性检查与赋值方法,优化对象操作逻辑。

  1. 安全属性检查:用 hasOwnProperty.call(obj, key) 代替 obj[key] !== undefined,避免遍历对象时访问原型链上的属性(如 toString、valueOf),同时比 in 运算符更高效(in 会遍历原型链)。例如 groupBy、countBy 中,均用此方法检查键是否存在。

  2. 高效属性赋值:用 baseAssignValue 代替普通赋值(obj[key] = value),兼容 Symbol 键、不可枚举键等特殊场景,同时避免赋值时触发对象属性拦截器(如 Proxy),比直接赋值更稳定高效。

  3. 键生成优化:聚合类函数中,提前生成键并缓存,避免在循环内重复生成相同键(如 groupBy 中,一次迭代仅生成一次 key,而非多次调用 iteratee)。

七、参数处理优化

核心思路:用户传入的参数存在多样性(可选参数、默认值、异常值),Lodash 通过参数规范化处理,避免执行时出错,同时减少参数判断的冗余逻辑。

  1. 参数类型转换:对数值型参数(如 fromIndex、depth),通过 toInteger 转为整数,处理负数、小数、字符串等异常输入。例如 includes 中,fromIndex 转为整数后,再处理负索引(fromIndex = nativeMax(length + fromIndex, 0)),避免越界。

  2. 可选参数适配:用 guard 参数区分「参数省略」与「参数传 undefined」,避免逻辑误判。例如 every、some 中,guard 参数用于判断是否省略 iteratee,确保参数处理的准确性。

  3. 可变参数封装:对支持可变参数的函数(如 sortBy、invokeMap),通过 baseRest 封装可变参数处理,将参数转为数组,避免手动处理 arguments 对象(arguments 是类数组,操作效率低)。

八、原生方法复用

核心思路:V8 引擎对原生方法(如 push、indexOf、slice)有深度优化,执行效率远超手写逻辑。Lodash 在保证兼容性的前提下,尽量复用原生方法,而非完全手写实现。

  1. 原生数组方法复用:arrayPush 封装 Array.prototype.push,批量添加元素时比手动循环赋值更高效;baseIndexOf 在条件允许时,复用原生 indexOf 方法。

  2. 字符串方法复用:字符串查找、截取直接调用原生方法(indexOf、slice),避免将字符串转为数组后处理,减少类型转换与遍历开销。

  3. 数学方法复用:随机数生成复用 Math.random,并通过 baseRandom 封装,保证生成区间的准确性,同时借力引擎优化的数学方法提升效率。

九、递归优化

核心思路:纯递归处理深层嵌套数据(如 flatMapDeep)易触发栈溢出,Lodash 通过「迭代+递归」混合模式,控制递归深度,平衡效率与稳定性。

  1. 混合遍历模式:baseFlatten(支撑 flatMap 系列)外层用 for 循环遍历,内层仅对需要继续扁平化的子数组递归,递归深度=扁平化深度,而非数组长度,避免栈溢出。例如 flatMapDeep 中,递归深度由 INFINITY 控制,但实际递归次数取决于嵌套层级,而非元素数量。

  2. 递归终止条件强化:递归函数中明确终止条件(如 depth=0、非可扁平化元素),避免无限递归;同时通过参数传递复用结果数组,减少递归过程中的内存分配。

十、兼容性优化

核心思路:Lodash 需兼容 IE 等老浏览器,这些浏览器不支持 ES6+ 方法(如 Array.prototype.includes),但适配过程中不牺牲性能。

  1. 原生方法降级封装:对老浏览器不支持的原生方法,封装自定义实现(如 baseIndexOf 替代 ES6 includes),但在现代浏览器中优先复用原生方法,兼顾兼容性与性能。

  2. 类数组适配老浏览器:老浏览器中,arguments、NodeList 等类数组不支持数组方法,Lodash 通过 isArrayLike 判断后,转为数组或用兼容逻辑处理,避免报错。

第二部分:具体集合函数实现与优化落地

一、遍历类函数:forEach / each / eachRight

1. 功能定位

遍历集合(数组/对象/类数组),对每个元素执行迭代器函数,无返回值(仅执行副作用);eachRight 从右向左遍历,逻辑与 forEach 相反。

2. 完整核心实现


// 数组遍历(最优路径,for 循环+引擎优化)
function arrayEach(array, iteratee) {
  var index = -1,
      length = array.length;

  while (++index < length) {
    // 迭代器执行,支持中断(返回 false 终止遍历)
    if (iteratee(array[index], index, array) === false) {
      break;
    }
  }
  return array;
}

// 对象/类数组遍历(兼容路径,for...in + 安全检查)
function baseEach(collection, iteratee) {
  var iterable = Object(collection),
      keys = baseKeys(iterable),
      index = -1,
      length = keys.length;

  while (++index < length) {
    var key = keys[index];
    // 执行迭代器,支持中断
    if (iteratee(iterable[key], key, iterable) === false) {
      break;
    }
  }
  return collection;
}

// 上层暴露 API(类型分支+迭代器标准化)
function forEach(collection, iteratee) {
  // 早期返回:空集合直接返回
  if (collection == null) {
    return collection;
  }
  // 类型分支:数组用 arrayEach,其他用 baseEach
  var func = isArray(collection) ? arrayEach : baseEach;
  // 迭代器标准化:处理字符串/对象等迭代器形式
  return func(collection, getIteratee(iteratee, 3));
}

// each 是 forEach 的别名
var each = forEach;

// eachRight(从右向左遍历,类型分支+反向逻辑)
function forEachRight(collection, iteratee) {
  if (collection == null) {
    return collection;
  }
  var func = isArray(collection) ? arrayEachRight : baseEachRight;
  return func(collection, getIteratee(iteratee, 3));
}

// 数组反向遍历(for 循环反向迭代,高效)
function arrayEachRight(array, iteratee) {
  var length = array.length,
      index = length;

  while (index--) {
    if (iteratee(array[index], index, array) === false) {
      break;
    }
  }
  return array;
}

3. 优化策略落地

  • 类型分支优化:数组用 for 循环(V8 可优化为快速遍历,比 forEach 原生方法快),对象用 baseKeys 提取键后遍历(避免 for...in 直接遍历对象,减少原型链检查开销)。

  • 执行流程优化:支持迭代器返回 false 中断遍历(自定义短路逻辑),同时早期返回空集合,避免无效遍历。

  • 迭代器标准化:getIteratee 处理迭代器,支持多种形式,提升 API 易用性,减少冗余判断。

  • 内存优化:缓存集合长度与索引,避免循环内重复读取;无临时对象创建,仅复用变量。

4. 场景适配

适用于无需返回结果、仅执行副作用的场景(如修改元素属性、打印日志);eachRight 适用于需从末尾开始处理的场景(如栈结构遍历)。大集合遍历优先用 forEach,而非 for...of(for 循环底层更高效)。

二、查找类函数:find / findLast

1. 功能定位

查找集合中第一个(find)/最后一个(findLast)满足迭代器条件的元素,找到后立即返回,无匹配元素返回 undefined。

2. 完整核心实现


// 高阶函数创建查找函数,复用 findIndex 逻辑
function createFind(findIndexFunc) {
  return function(collection, predicate, fromIndex, guard) {
    // 早期返回:空集合直接返回 undefined
    if (collection == null) {
      return undefined;
    }
    // 标准化迭代器
    var iteratee = getIteratee(predicate, 3);
    // 调用 findIndex/findLastIndex 获取索引
    var index = findIndexFunc(collection, iteratee, fromIndex, guard);
    // 索引有效则返回元素,否则返回 undefined
    return index > -1 ? collection[index] : undefined;
  };
}

// 数组查找索引(正向,短路逻辑)
function findIndex(array, predicate, fromIndex, guard) {
  var length = array == null ? 0 : array.length;
  if (!length) {
    return -1;
  }
  // 标准化起始索引
  var index = fromIndex == null ? 0 : toInteger(fromIndex);
  if (index < 0) {
    index = nativeMax(length + index, 0);
  }
  // 正向遍历,短路返回
  return baseFindIndex(array, predicate, index, false);
}

// 数组反向查找索引(逆向,短路逻辑)
function findLastIndex(array, predicate, fromIndex, guard) {
  var length = array == null ? 0 : array.length;
  if (!length) {
    return -1;
  }
  var index = fromIndex == null ? length - 1 : toInteger(fromIndex);
  if (index < 0) {
    index = nativeMax(length + index, 0);
  }
  return baseFindIndex(array, predicate, index, true);
}

// 底层查找索引逻辑(支持正向/反向,短路)
function baseFindIndex(array, predicate, fromIndex, fromRight) {
  var length = array.length,
      index = fromRight ? nativeMax(fromIndex, 0) : nativeMin(fromIndex, length - 1);

  while (fromRight ? index-- : ++index < length) {
    if (predicate(array[index], index, array)) {
      return index; // 短路:找到后立即返回索引
    }
  }
  return -1; // 无匹配元素
}

// 上层暴露 API
var find = createFind(findIndex);
var findLast = createFind(findLastIndex);

3. 优化策略落地

  • 底层逻辑复用:通过 createFind 高阶函数,复用 findIndex/findLastIndex 的索引查找逻辑,仅新增「索引转元素」的逻辑,减少代码冗余。

  • 执行流程优化:baseFindIndex 中实现短路逻辑,找到匹配元素后立即返回索引,不遍历剩余元素;早期返回空集合,避免无效计算。

  • 参数处理优化:标准化 fromIndex 参数,处理负数、小数等异常输入,避免越界;guard 参数区分参数省略与 undefined。

  • 类型适配优化:findIndex/findLastIndex 对数组和类数组分别处理,数组用 for 循环遍历,效率最优。

4. 场景适配

适用于需查找单个匹配元素的场景(如查找用户信息、匹配条件数据);大集合场景下比 filter[0] 高效(filter 遍历全部元素,find 短路返回)。

三、聚合类函数:groupBy / countBy / keyBy

1. 功能定位

聚合类函数核心是按迭代器生成的键,对集合元素进行聚合:groupBy 分组(值为元素数组)、countBy 计数(值为数量)、keyBy 建索引(值为单个元素,覆盖重复键)。

2. 完整核心实现


// 聚合函数骨架,统一遍历、边界检查、迭代器处理
function createAggregator(accumulator, initResult) {
  return function(collection, iteratee) {
    // 早期返回:空集合返回初始化结果
    if (collection == null) {
      return initResult ? initResult() : {};
    }
    // 初始化结果容器(groupBy/countBy/keyBy 容器不同,由 initResult 控制)
    var result = initResult ? initResult() : {};
    // 标准化迭代器
    var iterateeFunc = getIteratee(iteratee, 3);
    // 遍历集合,执行聚合逻辑
    baseEach(collection, function(value, key, collection) {
      // 生成聚合键(仅生成一次,缓存结果)
      var groupKey = iterateeFunc(value, key, collection);
      // 执行自定义聚合逻辑(分组/计数/建索引)
      accumulator(result, value, groupKey);
    });
    return result;
  };
}

// 1. groupBy:按键分组,值为元素数组
var groupBy = createAggregator(function(result, value, key) {
  // 安全属性检查:避免原型链污染
  if (hasOwnProperty.call(result, key)) {
    result[key].push(value); // 已有键,直接 push(复用数组,避免新建)
  } else {
    baseAssignValue(result, key, [value]); // 新键,初始化数组并赋值
  }
});

// 2. countBy:按键计数,值为数量
var countBy = createAggregator(function(result, value, key) {
  if (hasOwnProperty.call(result, key)) {
    ++result[key]; // 前置自增,减少临时变量
  } else {
    baseAssignValue(result, key, 1); // 新键,赋值为 1
  }
});

// 3. keyBy:按键建索引,值为单个元素(覆盖重复键)
var keyBy = createAggregator(function(result, value, key) {
  baseAssignValue(result, key, value); // 直接赋值,覆盖重复键
});

3. 优化策略落地

  • 底层架构优化:createAggregator 封装聚合通用逻辑(遍历、迭代器处理、边界检查),三个函数仅差异聚合回调,代码复用率达 80%+。

  • 属性操作优化:hasOwnProperty.call 安全检查键存在性,baseAssignValue 高效赋值,兼容特殊键,避免原型链污染。

  • 内存优化:groupBy 中已有键直接 push 元素,复用数组;无临时对象创建,聚合键仅生成一次,减少冗余计算。

  • 执行流程优化:早期返回空集合,baseEach 遍历支持中断(迭代器返回 false 可终止聚合)。

4. 场景适配

groupBy 适用于数据分组场景(如按性别分组用户);countBy 适用于统计场景(如统计各类型数据数量);keyBy 适用于快速索引场景(如按 ID 建立用户映射,便于后续查找)。

四、过滤类函数:filter / reject / partition

1. 功能定位

filter 筛选满足条件的元素;reject 筛选不满足条件的元素(filter 取反);partition 将集合分为两组,一组满足条件,一组不满足。

2. 完整核心实现


// 数组过滤(内存预分配+高效遍历)
function arrayFilter(array, predicate) {
  var index = -1,
      length = array.length,
      result = Array(length), // 预分配结果数组,长度与原数组一致
      resIndex = 0;

  while (++index < length) {
    var value = array[index];
    if (predicate(value, index, array)) {
      result[resIndex++] = value; // 直接索引赋值,避免扩容
    }
  }
  result.length = resIndex; // 截断多余长度,得到最终结果
  return result;
}

// 对象/类数组过滤(兼容路径)
function baseFilter(collection, predicate) {
  var result = [];
  baseEach(collection, function(value, key, collection) {
    if (predicate(value, key, collection)) {
      result.push(value);
    }
  });
  return result;
}

// 1. filter 上层 API
function filter(collection, predicate) {
  if (collection == null) {
    return []; // 早期返回:空集合返回空数组
  }
  // 类型分支:数组用 arrayFilter(预分配优化),其他用 baseFilter
  var func = isArray(collection) ? arrayFilter : baseFilter;
  return func(collection, getIteratee(predicate, 3));
}

// 2. reject:filter 取反,复用过滤逻辑
function reject(collection, predicate) {
  // negate 反转迭代器结果
  return filter(collection, negate(getIteratee(predicate, 3)));
}

// 3. partition:分组过滤,复用聚合骨架
var partition = createAggregator(function(result, value, key) {
  result[key ? 0 : 1].push(value); // 键为布尔值,0 组满足,1 组不满足
}, function() {
  return [[], []]; // 预分配两个结果数组,避免中途创建
});

3. 优化策略落地

  • 内存管理优化:arrayFilter 预分配结果数组,索引赋值后截断长度,比动态 push 快 20%-40%;partition 预分配两个数组,减少内存分配次数。

  • 逻辑复用优化:reject 复用 filter 逻辑,仅通过 negate 反转迭代器结果,无需重复编写过滤逻辑;partition 复用 createAggregator 骨架,减少代码冗余。

  • 类型分支优化:数组用专用过滤逻辑(for 循环+预分配),对象用 baseEach 遍历,适配不同类型效率差异。

  • 执行流程优化:早期返回空数组,避免无效遍历;数组过滤无临时对象,仅复用索引变量。

4. 场景适配

filter 适用于筛选符合条件的数据(如筛选活跃用户);reject 适用于排除不符合条件的数据(如排除无效订单);partition 适用于需同时获取满足/不满足条件数据的场景(如拆分通过/未通过审核的数据)。

五、排序类函数:sortBy / orderBy

1. 功能定位

sortBy 按迭代器结果升序排序(稳定排序);orderBy 支持多字段排序与排序方向(升序/降序),功能更灵活。

2. 完整核心实现


// 底层排序逻辑(稳定排序,支撑 sortBy/orderBy)
function baseOrderBy(collection, iteratees, orders) {
  // 处理迭代器:扁平化嵌套迭代器,标准化为函数
  iteratees = iteratees.map(function(iteratee) {
    return getIteratee(iteratee, 3);
  });

  // 生成排序所需的「键数组」(每个元素对应集合元素的迭代器结果)
  var indexed = baseMap(collection, function(value, index) {
    var criteria = iteratees.map(function(iteratee) {
      return iteratee(value, index, collection);
    });
    return { criteria: criteria, index: index, value: value };
  });

  // 稳定排序:基于归并排序,优先按迭代器结果排序,再按原索引排序(保证稳定)
  indexed.sort(function(a, b) {
    var length = iteratees.length,
        index = 0;

    while (index < length) {
      var order = orders ? orders[index] : 'asc';
      var compareResult = baseCompare(a.criteria[index], b.criteria[index]);
      if (compareResult) {
        return order === 'desc' ? -compareResult : compareResult;
      }
      index++;
    }
    // 迭代器结果相同时,按原索引排序(稳定排序关键)
    return a.index - b.index;
  });

  // 提取排序后的值,返回结果数组
  return baseMap(indexed, function(item) {
    return item.value;
  });
}

// 比较函数:处理不同类型值的比较(数字、字符串、对象)
function baseCompare(value, other) {
  if (value === other) {
    return 0;
  }
  var valIsDefined = value !== undefined,
      otherIsDefined = other !== undefined;

  if (!valIsDefined && otherIsDefined) {
    return -1;
  } else if (valIsDefined && !otherIsDefined) {
    return 1;
  }

  var valIsNull = value === null,
      otherIsNull = other === null;

  if (valIsNull && !otherIsNull) {
    return -1;
  } else if (!valIsNull && otherIsNull) {
    return 1;
  }

  // 字符串按 Unicode 编码比较,数字直接比较
  var valIsString = typeof value === 'string',
      otherIsString = typeof other === 'string';

  if (valIsString && otherIsString) {
    return value.localeCompare(other);
  }

  var valIsNumber = typeof value === 'number',
      otherIsNumber = typeof other === 'number';

  if (valIsNumber && otherIsNumber) {
    return value - other;
  }

  // 对象按 toString 结果比较
  return String(value).localeCompare(String(other));
}

// 1. sortBy:升序稳定排序,复用 baseOrderBy
var sortBy = baseRest(function(collection, iteratees) {
  if (collection == null) {
    return []; // 早期返回
  }
  // 处理参数:判断是否为迭代器调用,标准化迭代器
  var length = iteratees.length;
  if (length > 1 && isIterateeCall(collection, iteratees[0], iteratees[1])) {
    iteratees = [];
  } else if (length > 2 && isIterateeCall(iteratees[0], iteratees[1], iteratees[2])) {
    iteratees = [iteratees[0]];
  }
  // 扁平化迭代器,调用 baseOrderBy(默认升序)
  return baseOrderBy(collection, baseFlatten(iteratees, 1), []);
});

// 2. orderBy:多字段排序,支持方向
function orderBy(collection, iteratees, orders, guard) {
  if (collection == null) {
    return []; // 早期返回
  }
  // 标准化参数:迭代器转为数组,排序方向转为数组
  if (!isArray(iteratees)) {
    iteratees = iteratees == null ? [] : [iteratees];
  }
  orders = orders == null ? [] : castArray(orders);
  // 调用 baseOrderBy,传入排序方向
  return baseOrderBy(collection, baseFlatten(iteratees, 1), orders);
}

3. 优化策略落地

  • 算法选择优化:基于归并排序实现稳定排序,避免原生 sort 的不稳定性;同时通过「迭代器结果预计算」,减少排序过程中的重复计算(排序前一次性生成所有元素的迭代器结果,排序时直接比较)。

  • 底层逻辑复用:sortBy 与 orderBy 均复用 baseOrderBy 核心逻辑,仅差异参数处理与排序方向,代码复用率高。

  • 参数处理优化:baseRest 处理可变参数,baseFlatten 扁平化迭代器,支持多字段排序;orders 参数标准化,兼容字符串与数组形式。

  • 执行流程优化:早期返回空集合;排序后仅提取元素值,避免返回冗余数据;稳定排序通过原索引兜底,保证相同优先级元素的相对顺序。

4. 场景适配

sortBy 适用于单字段升序排序场景(如按年龄排序用户);orderBy 适用于复杂排序场景(如按年龄降序、姓名升序排序用户),稳定排序特性适用于需保留原顺序的场景(如分页排序后合并数据)。

六、随机类函数:shuffle / sample / sampleSize

1. 功能定位

shuffle 随机打乱集合元素(返回新数组,不修改原集合);sample 随机抽取一个元素;sampleSize 随机抽取指定数量的元素(无重复)。

2. 完整核心实现


// 底层随机数生成(均匀无偏,适配区间)
function baseRandom(lower, upper) {
  return lower + Math.floor(Math.random() * (upper - lower + 1));
}

// 1. shuffle:Fisher-Yates 洗牌算法
function shuffle(collection) {
  // 早期返回:空集合返回空数组
  if (collection == null) {
    return [];
  }
  // 浅拷贝集合:避免修改原集合,类数组转为数组
  var array = isArrayLike(collection) ? copyArray(collection) : values(collection);
  var length = array.length;
  var index = -1;
  var lastIndex = length - 1;

  // Fisher-Yates 洗牌:一次遍历,交换元素
  while (++index < length) {
    // 生成 [index, lastIndex] 区间的随机索引
    var rand = baseRandom(index, lastIndex);
    // 交换当前元素与随机元素(无临时变量,直接交换)
    var value = array[rand];
    array[rand] = array[index];
    array[index] = value;
  }
  return array;
}

// 2. sample:随机抽取一个元素
function sample(collection) {
  // 早期返回:空集合返回 undefined
  if (collection == null) {
    return undefined;
  }
  // 类数组转为数组,便于获取随机索引
  var array = isArrayLike(collection) ? collection : values(collection);
  var length = array.length;
  return length ? array[baseRandom(0, length - 1)] : undefined;
}

// 3. sampleSize:随机抽取指定数量元素
function sampleSize(collection, n) {
  if (collection == null) {
    return [];
  }
  var array = isArrayLike(collection) ? copyArray(collection) : values(collection);
  var length = array.length;
  // 处理 n 的边界:不超过数组长度,最小为 0
  n = nativeMin(n < 0 ? 0 : n, length);
  var index = -1;
  var lastIndex = length - 1;
  var result = Array(n); // 预分配结果数组

  // 简化版 Fisher-Yates:抽取 n 个元素后终止
  while (++index < n) {
    var rand = baseRandom(index, lastIndex);
    result[index] = array[rand];
    // 交换已抽取元素与当前索引元素,避免重复抽取
    array[rand] = array[index];
  }
  return result;
}

3. 优化策略落地

  • 算法选择优化:shuffle 与 sampleSize 采用 Fisher-Yates 算法,时间复杂度 O(n),比原生 sort + 随机数(O(n log n))高效,且随机无偏,无重复抽取。

  • 内存管理优化:shuffle 浅拷贝原集合,避免深拷贝开销;sampleSize 预分配结果数组,减少动态 push 扩容。

  • 执行流程优化:早期返回空集合/undefined;sampleSize 抽取 n 个元素后立即终止,无需遍历全部元素(短路优化)。

  • 随机数优化:baseRandom 生成均匀区间随机数,避免 Math.random 直接使用导致的偏置;无临时变量交换元素,减少内存开销。

4. 场景适配

shuffle 适用于打乱数据顺序(如抽奖打乱名单);sample 适用于随机抽取单个元素(如随机推荐商品);sampleSize 适用于随机抽取多个不重复元素(如随机选取 5 个用户做调研)。

七、其他核心集合函数优化要点

剩余集合函数均基于上述通用优化策略实现,核心优化要点如下:

1. every / some

  • 短路优化:every 遇不满足元素立即返回 false,some 遇满足元素立即返回 true;

  • 类型分支:数组用专用逻辑(for 循环),对象用 baseEach 遍历;

  • 迭代器标准化:getIteratee 处理多种迭代器形式,早期返回空集合。

2. map / flatMap 系列

  • 内存预分配:数组 map 预分配结果数组,索引赋值后截断;

  • 逻辑复用:flatMap = map + flatten(深度 1),flatMapDepth = map + flattenDepth,无需重复编写逻辑;

  • 递归优化:flatMapDeep 基于 baseFlatten,迭代+递归混合模式,避免栈溢出。

3. includes

  • 类型分支:字符串直接调用原生 indexOf,数组用 baseIndexOf,对象转为数组后处理;

  • 边界处理:标准化 fromIndex 参数,处理负索引与越界;

  • 早期返回:空集合直接返回 false,避免无效查找。

4. reduce / reduceRight

  • 类型分支:数组用 arrayReduce(for 循环),对象用 baseReduce;

  • 累加器优化:支持省略初始累加器(用第一个元素作为初始值);

  • 迭代器标准化:getIteratee 处理迭代器,早期返回空集合。

5. size

  • 类型分支:类数组直接取 length,对象用 baseKeys 提取键后取长度;

  • 早期返回:null/undefined 直接返回 0,无需后续处理;

  • 高效计算:避免遍历对象,直接通过键数组长度获取大小。

第三部分:总结与落地启示

Lodash 集合函数的高性能,本质是「对细节的极致追求」与「对 JavaScript 特性的深度理解」,其优化思路并非孤立技巧,而是一套可复用的方法论:

  1. 分层设计是基础:通过高阶函数、底层函数拆分逻辑,实现复用与解耦,同时减少执行开销;

  2. 类型适配是关键:不同数据类型的操作效率差异极大,精准分支能显著提升性能;

  3. 内存与算法是核心:减少内存分配、复用容器、选择最优算法,是大集合场景性能提升的核心;

  4. 提前终止是捷径:短路逻辑、早期返回,能最大程度减少无效计算,极端场景下性能提升显著。

日常开发中,可借鉴这些思路优化业务代码:如大数组操作前预分配长度、用短路逻辑避免全量遍历、按数据类型选择遍历方式等,在保证代码可读性的同时,实现性能提升。