lodash 作为前端最具代表的工具库,最初是为了解决UndersCore 的痛点而产生,之后经过多年的发展,历经了函数式、ES6、工程化等阶段,已发展为一个强大的前端基础设施。在第一章中,首先来介绍 lodash 的 发展历程以及设计的核心思想。
Underscore 时代
要理解 Lodash,必先理解它的 "前身" Underscore------2009 年发布的 Underscore 是前端工具库的 "拓荒者",在 ES5 尚未普及的年代,它首次系统化地封装了数组遍历、对象操作、函数增强等基础能力,让开发者摆脱了重复编写 for 循环、手动判断数据类型的繁琐。
Underscore 的设计思路
源码分析: bgithub.xyz/lessfish/un...
官网中所带注释的源码:
具体分析可以看这篇文章:juejin.cn/post/759335...
Lodash 的发展历程
起源:Underscore 的性能与一致性痛点(2012 年)
诞生背景:对 Underscore 的不满与改进诉求
Lodash 由 John-David Dalton(GitHub 用户名 jdalton)于 2012 年 4 月 23 日创建,最初是 Underscore.js 的一个 fork。当时 Underscore 已成为前端开发的标配工具库,但存在两大核心痛点:
- 性能不足 :核心方法复用原生高阶函数(如
Array.prototype.forEach),闭包与函数调用栈开销大,大数据量处理时卡顿明显; - API 不一致 :参数顺序混乱(部分方法
(fn, arr),部分(arr, fn)),边界处理无统一标准,容易出错; - 跨浏览器兼容性:对老旧浏览器(如 IE6+)的适配不够完善,部分方法存在边缘行为差异。
Dalton 的初衷很明确:打造一个性能更优、API 更一致、兼容性更强的 Underscore 替代方案,同时保持 API 兼容性,让开发者可以无缝迁移。
命名由来:低调的 "下划线" 变体
项目名称 "Lodash" 源自 "low dash"(低破折号)的谐音,形象地表达了它与 Underscore(下划线)的渊源 ------ 既相似又有区别,就像破折号(-)比下划线(_)位置更低一样。
早期探索:从兼容到差异化(2012-2014 年,v0.x - v2.x)
v0.x 阶段(2012 年 4-12 月):快速迭代与核心能力构建
- 首个版本 v0.1.0 于 2012 年 4 月 23 日发布,仅包含基础的数组 / 对象 / 函数操作方法;
- 核心改进:重写
_.each/_.map等遍历方法,用原生for循环替代原生高阶函数,性能提升 2-5 倍; - 保持 API 兼容:完全复刻 Underscore 的核心方法,确保开发者 "零成本迁移";
- 新增差异化功能:如
_.cloneDeep(深拷贝)、_.get(对象路径取值)等 Underscore 缺失的高频能力。
v1.x 阶段(2013 年):模块化与跨环境适配
- 2013 年加入 Dojo 基金会,获得社区资源支持,开始规范化发展;
- 核心突破:引入模块化设计,支持 AMD(RequireJS)、CommonJS 等模块化规范,适配 Node.js 与浏览器双环境;
- 性能优化升级:为不同数据类型(数组 / 对象 / 类数组)提供专用遍历逻辑,避免通用逻辑的冗余判断;
- 新增功能:函数防抖节流(
_.debounce/_.throttle)、对象深度比较(_.isEqual升级)等。
v2.x 阶段(2014 年):链式调用与惰性求值
- 核心特性:引入惰性求值 (lazy evaluation)机制,优化链式调用性能 ------ 仅在调用
value()时才执行所有链式操作,而非每一步都立即计算; - API 一致性强化:统一所有方法的参数顺序为 "数据在前,操作在后",彻底解决 Underscore 的参数混乱问题;
- 边界处理完善:对
null/undefined等空值做统一兼容,如_.map(null)返回空数组,避免报错; - 社区影响力扩大:开始被 React、Angular 等主流框架推荐,成为前端项目的 "标配依赖"。
爆发期:性能革命与生态扩张(2015 年,v3.x)
2015 年是 Lodash 发展的关键转折点,v3.x 版本的发布使其彻底超越 Underscore,成为前端工具库的绝对领导者。
v3.0.0(2015 年 1 月):性能巅峰与功能全覆盖
- 性能突破:核心方法性能再提升 30-50%,部分场景(如大数据量
_.filter)性能是 Underscore 的 10 倍以上; - 新增 47 个新方法:如数学工具(
_.add/_.multiply)、字符串操作(_.startsWith/_.endsWith)、集合高级处理(_.flatMap)等; - 链式调用增强:支持隐式链式调用(
_(value).map(...).filter(...)),简化语法; - 工具链升级:推出
lodash-cli,支持定制化构建(如仅保留数组操作方法,减小打包体积)。
2015 年重大里程碑
- 成为 npm 上依赖最多的包,超过 Express、React 等核心框架;
- npm 下载量突破 10 亿次,全球超过 300 万项目直接依赖;
- 与 Underscore 开始 "合并讨论",部分 Underscore 贡献者开始向 Lodash 迁移,甚至推动 Underscore 向 Lodash 看齐;
- 推出
lodash-fp分支,提供函数式编程风格的 API(自动柯里化、参数顺序反转),适配 Ramda 等函数式库的用户习惯。
重构期:API 革新与生态分化(2015 年 6 月,v4.0.0)
v4.0.0 是 Lodash 历史上最具争议也最具远见的版本,它放弃了对 Underscore 的 API 兼容,进行了彻底的 API 重构,奠定了现代 Lodash 的基础。
核心重构内容
| 重构方向 | 具体变化 | 影响 |
|---|---|---|
| 移除嵌套命名空间 | 如 _.collection.map → _.map,所有方法直接挂载到 _ 上 |
简化调用,降低学习成本 |
| API 去冗余 | 移除 _.pluck(用 _.map 替代)、_.compose(用 _.flowRight 替代)等模糊方法 |
统一功能入口,减少方法数量 |
| 参数规范化 | 所有方法严格遵循 "数据在前,操作在后",如 _.sortBy(collection, iteratee) |
彻底解决参数混乱问题 |
| 模块化深度优化 | 支持 ES6 Module,推出独立的 lodash-es 包,适配现代打包工具(Webpack/Rollup) |
支持按需导入,大幅减小打包体积 |
| 性能再优化 | 重写底层工具函数,引入 "类型预判缓存",进一步提升执行效率 | 保持性能领先优势 |
争议与适应
- 短期影响:部分依赖 Underscore 兼容 API 的项目无法直接升级,出现 "Lodash 3 兼容版" 分支;
- 长期价值:API 一致性达到新高度,学习成本降低 50%,吸引更多新开发者使用;
- 生态分化:
lodash-es成为现代前端项目的首选,而lodash保持 CommonJS 规范,适配 Node.js 与老旧项目。
稳定期:持续迭代与基础设施化(2016-2025 年,v4.x 长期维护)
v4.0.0 之后,Lodash 进入 "功能完善 + 安全维护" 的稳定期,版本号从 v4.0.0 逐步迭代到 v4.17.21(2020 年 2 月),期间未引入破坏性变更。
核心发展方向
-
生态完善:
lodash-es成为主流:适配 ES6 模块化,支持 Tree-shaking,解决 "Lodash 打包体积大" 的痛点;- 推出
lodash.cli/lodash-webpack-plugin等工具,支持定制化构建,最小化打包体积; - 适配 ES6+ 新特性:支持
Map/Set/Symbol等原生类型,新增_.isMap/_.isSymbol等判断方法。
-
安全与兼容性:
- 持续修复安全漏洞(如原型污染),成为 npm 生态中最安全的核心依赖之一;
- 兼容范围扩大:支持 IE9+、Node.js 6+ 等主流环境,同时为现代环境提供原生 API 复用能力;
- 2019 年通过 jQuery 基金会、JS 基金会,最终加入 OpenJS 基金会,获得长期社区支持。
里程碑事件(2020-2025 年)
- 2020 年 8 月发布 v4.17.20,之后进入 "安全维护模式",仅修复严重漏洞;
- 2025 年 10 月,Sovereign Tech Agency 宣布投资 Lodash,支持其 "功能完成 + 长期稳定" 战略,确保其作为前端基础设施的可持续性;
- 截至 2025 年,Lodash 每周 npm 下载量超过 2.57 亿次,全球 930 万 + 网站使用,包括三分之一的全球 Top10000 网站;
- v5.0.0 规划:计划移除对老旧环境的支持(如 IE),全面采用 ES2015+ 语法,进一步优化性能与体积,预计 2026 年后发布。
Lodash 发展的核心驱动力与启示
四大核心驱动力
| 驱动力 | 具体体现 |
|---|---|
| 性能优先 | 从 v0.x 到 v4.x,每一次核心方法重写都以性能为第一目标,这是其超越 Underscore 的关键 |
| API 一致性 | 坚持 "数据在前,操作在后" 的参数顺序,统一返回值规则,降低学习与使用成本 |
| 跨环境适配 | 最早支持浏览器 / Node.js 双环境,适配模块化规范,成为 "前后端通用工具库" |
| 社区协同 | 加入基金会、与 Underscore 贡献者协作、接受社区反馈,形成良性发展循环 |
对前端生态的启示
- 工具库设计范式:Lodash 的 "无侵入式设计"(不修改原生对象原型)、"边界容错" 等原则成为现代工具库的设计标准;
- 性能优化方法论:为不同数据类型提供专用逻辑、惰性求值、缓存优化等策略,被广泛应用于 React/Vue 等框架的源码中;
- 模块化与按需加载 :
lodash-es与 Tree-shaking 的结合,推动前端生态向 "轻量化" 方向发展; - 生态协作模式:从 "竞争" 到 "融合"(与 Underscore 的合并讨论),体现了开源社区的协作精神。
Lodash 的核心设计原则
性能优先
Lodash 把 "性能" 作为第一优先级,所有核心方法的实现都围绕 "最小化执行开销、最大化执行效率" 展开,甚至为了性能牺牲部分 "语法优雅性"(比如放弃原生高阶函数,改用底层循环)。
核心优化策略
| 优化方向 | 具体实现逻辑 |
|---|---|
| 底层遍历:放弃原生高阶函数 | 核心遍历方法(如 _.each/_.map)不用 Array.prototype.forEach/map,改用原生 for 循环(减少函数调用栈开销、避免闭包损耗); |
| 类型预判:提前分支处理 | 执行核心逻辑前,先精准判断数据类型(数组 / 类数组 / 对象 / Map/Set),为不同类型选择最优执行路径,避免通用逻辑的冗余判断; |
| 惰性求值:延迟计算 | _.chain 链式调用采用 "惰性执行"------ 仅在调用 value() 时才执行所有链式操作,而非每一步都立即计算,减少中间步骤的性能损耗; |
| 缓存优化:避免重复计算 | 对高频调用 / 高开销方法(如 _.memoize/_.cloneDeep)提供缓存能力,或提前缓存常用常量(如空对象、空数组); |
| 减少冗余操作:精准边界判断 | 避免无意义的遍历 / 计算(如对空数组直接返回,对长度为 1 的数组跳过循环); |
源码实例:_.each 的性能优化(对比 Underscore)
Underscore 的 _.each(低效实现)
js
// Underscore 仅做通用遍历,无类型预判,直接复用原生逻辑
_.each = function(obj, iteratee) {
iteratee = optimizeCb(iteratee);
if (Array.isArray(obj)) {
for (var i = 0, l = obj.length; i < l; i++) iteratee(obj[i], i, obj);
} else {
for (var key in obj) {
if (_.has(obj, key)) iteratee(obj[key], key, obj);
}
}
return obj;
};
Lodash 的 _.each(性能优化版)
js
// Lodash 先做精细类型预判,为不同类型选最优遍历方式
function each(collection, iteratee) {
const func = Array.isArray(collection)
? arrayEach // 数组专用遍历(纯 for 循环,无额外判断)
: isPlainObject(collection)
? objectEach // 纯对象专用遍历(Object.keys + for 循环)
: isMap(collection)
? mapEach // Map 专用遍历(原生 forEach,适配 Map 特性)
: isSet(collection)
? setEach // Set 专用遍历
: baseEach; // 兜底通用遍历
return func(collection, iteratee);
}
// 数组专用遍历:极致精简的 for 循环(无冗余判断)
function arrayEach(array, iteratee) {
let index = -1;
const length = array.length;
// 提前判断长度,空数组直接返回(减少循环开销)
while (++index < length) {
// 迭代器返回 false 时立即终止循环(额外性能优化)
if (iteratee(array[index], index, array) === false) {
break;
}
}
return array;
}
该原则解决的核心问题
- 解决 Underscore 等库 "通用逻辑导致的性能损耗";
- 适配大数据量场景(如上万条数据的遍历 / 处理);
- 平衡 "功能丰富性" 与 "执行效率",避免 "大而慢"。
API 一致性
Lodash 重新定义了工具库的 API 设计规范,通过 "统一的规则" 让开发者无需记忆复杂的参数、返回值逻辑,大幅降低学习和使用成本 ------ 这也是 Lodash 比 Underscore 更易用的核心原因。
核心统一规则
参数顺序统一:"数据在前,操作在后"
所有方法遵循 (collection/value, iteratee/options, [thisArg]) 的参数顺序:
- 第一参数:待处理的 "数据"(数组 / 对象 / 函数等);
- 第二参数:对数据的 "操作"(迭代器 / 配置项等);
- 可选第三参数:迭代器的
this上下文;
反例(Underscore) :部分方法参数混乱(如 _.sortBy 是 (collection, iteratee),而早期 _.curry 是 (fn, arity),无统一逻辑);正例(Lodash) :_.map(array, fn)、_.filter(object, fn)、_.each(set, fn) 均遵循 "数据在前"。
返回值规则统一
| 方法类型 | 返回值规则 |
|---|---|
| 修改类方法 | 返回新对象 / 新数组 (如 _.assign/_.map),不修改原数据(无副作用); |
| 判断类方法 | 返回布尔值(如 _.isEmpty/_.isEqual),且判断逻辑 "全域统一"; |
| 遍历类方法 | 返回原数据(如 _.each),支持链式调用; |
| 取值类方法 | 返回目标值(如 _.get),无值时返回默认值(避免返回 undefined); |
边界处理统一
所有方法对 null/undefined/ 空值等边界情况做统一兼容,避免开发者手动加判断:
_.get(obj, 'a.b.c', 'default'):即使obj.a不存在,也不会报错,返回默认值;_.map(null, fn):直接返回空数组,而非抛出 TypeError;_.each(undefined, fn):直接返回undefined,无报错。
命名规则统一
- 语义化命名:方法名直接体现功能(如
_.cloneDeep明确是 "深拷贝",而非 Underscore 仅有的_.clone浅拷贝); - 扁平化命名:v4 版本移除嵌套命名空间 (如
_.collection.map→_.map),所有方法直接挂载到_上; - 前缀统一:判断类方法均以
is开头(_.isArray/_.isPlainObject),操作类方法无冗余前缀。
源码实例:_.get 的边界处理一致性
js
// Lodash _.get 核心逻辑:统一处理 null/undefined,返回默认值
function get(object, path, defaultValue) {
// 先判断对象是否为 null/undefined,直接返回默认值(统一边界)
const result = object == null ? undefined : baseGet(object, path);
// 结果为 undefined 时返回默认值,否则返回结果(统一返回规则)
return result === undefined ? defaultValue : result;
}
该原则解决的核心问题
- 解决 Underscore API 混乱导致的 "记忆成本高、易出错";
- 降低团队协作成本(所有人遵循同一套 API 规则);
- 提升代码可维护性(方法行为可预判)。
跨环境兼容
Lodash 从 v1 开始就瞄准 "全环境适配",是最早同时支持浏览器、Node.js 的工具库之一,其兼容逻辑并非 "简单的降级",而是 "在不同环境下提供一致的 API 行为"。
兼容范围与实现策略
| 兼容维度 | 具体实现逻辑 |
|---|---|
| 浏览器兼容(IE6+) | 对老旧浏览器的缺失 API 做降级实现(如 Array.isArray/Object.keys);抹平不同浏览器的 DOM / 事件 API 差异; |
| Node.js 兼容 | 适配 Node.js 特有数据类型(Buffer/Stream/process);支持 CommonJS 模块化(require('lodash')); |
| 模块化规范兼容 | 支持 AMD(RequireJS)、CommonJS、ES6 Module(lodash-es)三大模块化规范; |
| 原生 API 兼容 | 适配 ES6+ 新特性(Map/Set/Symbol),新增 _.isMap/_.isSymbol 等方法;兼容原生方法的行为(如 _.assign 对齐 Object.assign); |
| 运行时环境兼容 | 适配 Electron、React Native 等跨端环境,抹平端侧 API 差异; |
源码实例:_.isArray 的跨环境兼容
js
// Lodash _.isArray 兼容逻辑:优先复用原生 API,无则降级实现
const isArray = Array.isArray || function(value) {
// 老旧浏览器降级:通过 toString 判断类型(IE6+ 兼容)
return typeof value === 'object' &&
value !== null &&
Object.prototype.toString.call(value) === '[object Array]';
};
关键设计:"渐进式兼容" 而非 "一刀切"
Lodash 不会为了兼容老旧环境牺牲现代环境的性能:
- 现代环境(Chrome/Firefox/Node.js 14+):优先复用原生 API(如
Array.prototype.flat); - 老旧环境(IE6/Node.js 6-):自动降级为纯 JS 实现;
- 开发者可通过
lodash-cli定制构建,移除无需的兼容逻辑(如仅支持现代浏览器时,剔除 IE 兼容代码)。
该原则解决的核心问题
- 避免开发者为不同环境编写 "环境适配层";
- 让同一套代码可运行在浏览器、Node.js、跨端环境;
- 降低工具库的 "环境迁移成本"(如从浏览器项目迁移到 Node.js 项目,无需替换工具库)。
功能全覆盖
Lodash 遵循 "一站式" 设计理念,覆盖前端开发从 "基础数据处理" 到 "工程化优化" 的所有高频场景,避免开发者混用多个工具库(如 Underscore + jQuery + 自定义工具函数)。
功能分层
| 功能层 | 覆盖场景 | 核心方法示例 |
|---|---|---|
| 基础层 | 数组 / 对象 / 字符串 / 数字 / 日期的通用操作 | _.each/_.map/_.keys/_.trim |
| 增强层 | 函数控制、数据深度操作、集合高级处理 | _.debounce/_.cloneDeep/_.get |
| 工程层 | 类型判断、数据验证、惰性求值、模块化导出 | _.isPlainObject/_.chain/_.memoize |
核心功能覆盖
Lodash 不仅复刻 Underscore 的核心功能,还补充了大量高频且原生 JS 缺失的能力:
| 缺失的原生能力 | Lodash 解决方案 | 解决的核心问题 |
|---|---|---|
| 深拷贝 | _.cloneDeep |
原生仅能浅拷贝(Object.assign); |
| 对象路径取值 / 赋值 | _.get/_.set |
原生需多层 && 判断(obj?.a?.b 是 ES2020 后才支持); |
| 防抖节流(高级配置) | _.debounce/_.throttle |
原生需手动实现,且无 "立即执行""取消" 等配置; |
| 数据深度比较 | _.isEqual |
原生仅能比较引用(===),无法比较对象内容; |
| 函数柯里化 / 组合 | _.curry/_.flow |
原生无内置函数式编程工具; |
| 集合惰性遍历 | _.chain |
原生链式调用需手动拼接,无惰性求值; |
设计细节
Lodash 的功能设计遵循 "符合开发者直觉":
- 方法行为与命名高度一致(如
_.map对数组 / 对象 / Map 的处理逻辑 "语义一致"); - 新增方法时,优先复用已有参数规则(如
_.filter与_.map的参数顺序完全一致); - 避免 "隐藏行为"(如修改原数据、静默失败),所有副作用均明确告知。
该原则解决的核心问题
- 减少项目依赖数量(无需引入多个工具库);
- 降低 "工具函数碎片化"(避免团队每个人都写自己的深拷贝 / 防抖函数);
- 提升代码一致性(全项目使用同一套工具方法)。