Lodash 工具函数速查表 - Lodash 常用方法与 JavaScript 工具库大全
这张速查给那些与其手写同样的循环、不如调用一个经过充分测试的函数的 JS 开发者。条目覆盖数组、对象、字符串、集合与函数工具,每条都给出能直接解决某个麻烦问题的调用,比如读取一个可能不存在的深层值、深拷贝以避免改到共享状态、或对输入框事件做防抖。与原生方法不同的是,Lodash 对 null/undefined 输入和边界情况更宽容。读完你应能针对去重、分组、防抖和深合并快速选对助手函数,知道哪些场景该用 Lodash 而不是手写。
典型使用场景
面向需要在不引入框架的前提下把数据处理写干净、同时又担心打包体积的 JavaScript 开发者。Lodash 的价值集中在四类场景:安全操作深层嵌套对象(get/set 不会因为中间层不存在而抛错)、集合化的数据处理(groupBy、keyBy、sortBy 多字段排序)、函数频率控制(debounce 用于搜索输入、throttle 用于滚动和 resize)、以及字符串与数值的小工具(camelCase、clamp、round)。本表同时保留了「哪些能被原生方法替代」的判断依据,以及按需单函数引入来压体积的实践方式,避免为了一个 debounce 把整个库打进产物。
数组 7
_.map(arr, (n) => n * 2)_.filter(arr, (n) => n > 0)_.reduce(arr, (sum, n) => sum + n, 0)_.chunk(arr, 2)_.flattenDeep([1, [2, [3]]])_.uniq([1, 2, 1, 3])_.union([1, 2], [2, 3])对象 7
_.get(obj, "a.b.c", "default")_.set(obj, "a.b.c", value)_.pick(obj, ["name", "age"])_.omit(obj, ["password"])_.merge(target, source)_.cloneDeep(obj)_.keys(obj) / _.values(obj)字符串 6
_.camelCase("Foo Bar")_.kebabCase("FooBar")_.template("<%= name %>")({ name: "Vue" })_.escape("<a>")_.trim(" abc ")_.pad("abc", 8)集合 6
_.groupBy(users, "age")_.sortBy(users, ["age", "name"])_.countBy([1, 2, 2, 3], Math.floor)_.keyBy(users, "id")_.sampleSize(arr, 2)_.shuffle(arr)函数 6
_.debounce(fn, 300)_.throttle(fn, 300)_.memoize(fn)_.once(fn)_.partial(fn, _, 2)_.bind(fn, thisArg, arg1)数字与数学 5
_.random(0, 100)_.range(0, 5)_.clamp(num, 0, 100)_.inRange(num, 0, 100)_.round(1.234, 2)实用判断 7
_.isEmpty(value)_.isEqual(a, b)_.isNil(value)_.isArray(value) / _.isObject(value)_.has(obj, "a.b")_.defaults({ a: 1 }, { a: 0, b: 2 })_.noop()参数矩阵
| 参数 | 作用 | 示例 |
|---|---|---|
_.map(arr, fn) | 逐项变换收集为新数组,不改原数组 | _.map(arr, (n) => n * 2) |
_.filter(arr, fn) | 保留回调返回真值的元素 | _.filter(arr, (n) => n > 0) |
_.reduce(arr, fn, init) | 把数组逐项累计为一个值 | _.reduce(arr, (sum, n) => sum + n, 0) |
_.chunk(arr, size) | 按固定大小切分数组,适合分批处理 | _.chunk(arr, 2) |
_.flattenDeep(nested) | 把任意层嵌套一维化 | _.flattenDeep([1, [2, [3]]]) |
_.uniq(arr) | 去重并保留首次出现顺序 | _.uniq([1, 2, 1, 3]) |
_.union([a], [b]) | 合并多数组并按唯一值去重 | _.union([1, 2], [2, 3]) |
_.get(obj, "a.b.c", def) | 安全读深层属性,缺失时回退默认值 | _.get(obj, "a.b.c", "default") |
_.set(obj, "a.b.c", v) | 安全写深层属性,自动创建中间层 | _.set(obj, "a.b.c", value) |
_.pick(obj, keys) | 只取出指定键组成新对象 | _.pick(obj, ["name", "age"]) |
_.omit(obj, keys) | 去掉指定键,常用于剔除敏感字段 | _.omit(obj, ["password"]) |
_.merge(target, source) | 深合并,嵌套对象按层合并而非整体覆盖 | _.merge(target, source) |
_.cloneDeep(obj) | 递归深拷贝,修改副本不污染原引用 | _.cloneDeep(obj) |
_.keys(obj) / _.values(obj) | 取键名数组 / 键值数组 | _.keys(obj) / _.values(obj) |
_.camelCase(str) | 转驼峰命名 | _.camelCase("Foo Bar") |
_.kebabCase(str) | 转短横线命名 | _.kebabCase("FooBar") |
_.template(str)(data) | 编译模板字符串并填入数据 | _.template("<%= name %>")({ name: "Vue" }) |
_.escape(str) | HTML 转义,防止内容被当标签解析 | _.escape("<a>") |
_.trim(str) / _.pad(str, len) | 去首尾空白 / 两端填充到指定长度 | _.trim(" abc ") |
_.groupBy(arr, "age") | 按属性值把数组分组成对象 | _.groupBy(users, "age") |
_.sortBy(arr, ["age", "name"]) | 按多属性序列依次比较排序 | _.sortBy(users, ["age", "name"]) |
_.countBy(arr, fn) | 按规则统计各取值出现频次 | _.countBy([1, 2, 2, 3], Math.floor) |
_.keyBy(arr, "id") | 以指定属性为键把数组转成对象,便于按键取用 | _.keyBy(users, "id") |
_.sampleSize(arr, n) | 不放回随机抽取 n 个元素 | _.sampleSize(arr, 2) |
_.shuffle(arr) | Fisher–Yates 洗牌,返回新数组 | _.shuffle(arr) |
_.debounce(fn, 300) | 防抖:停止触发后才执行一次 | _.debounce(fn, 300) |
_.throttle(fn, 300) | 节流:固定频率内最多执行一次 | _.throttle(fn, 300) |
_.memoize(fn) | 缓存函数结果,相同入参直接复用 | _.memoize(fn) |
_.once(fn) | 包装成只执行一次的函数 | _.once(fn) |
_.partial(fn, _, 2) | 部分应用,预置部分参数 | _.partial(fn, _, 2) |
_.bind(fn, thisArg, arg1) | 绑定 this 与前置参数 | _.bind(fn, thisArg, arg1) |
_.random(0, 100) | 生成区间内的随机整数 | _.random(0, 100) |
_.range(0, 5) | 生成 [0, 1, 2, 3, 4] 这样的数组 | _.range(0, 5) |
_.clamp(num, 0, 100) | 把数值限制在区间内 | _.clamp(num, 0, 100) |
_.inRange(num, 0, 100) | 判断数值是否落在区间内 | _.inRange(num, 0, 100) |
_.round(1.234, 2) | 四舍五入保留指定位数小数 | _.round(1.234, 2) |
_.isEmpty(value) | 判断是否为空(空数组、空对象、空串等) | _.isEmpty(value) |
_.isEqual(a, b) | 深度相等比较 | _.isEqual(a, b) |
_.isNil(value) | 判断是否为 null 或 undefined | _.isNil(value) |
_.isArray / _.isObject | 类型判断 | _.isArray(value) / _.isObject(value) |
_.has(obj, "a.b") | 判断是否拥有该属性路径 | _.has(obj, "a.b") |
_.defaults({ a: 1 }, { a: 0, b: 2 }) | 为未定义的属性填充默认值 | _.defaults({ a: 1 }, { a: 0, b: 2 }) |
_.noop() | 空函数,用作占位默认值 | _.noop() |
易错点与避坑指南
现象搜索框每敲一个字就发一次请求,输入快了直接把后端打满。
原因直接绑在 input 事件上做远程搜索,没有做频率控制。
处置改用 _.debounce(fn, 300):停止触发 300 毫秒后才真正发起一次请求,而不是每键都发。
现象滚动或 resize 时页面卡顿,掉帧明显。
原因在高频事件上执行了重计算或布局操作,且没有限制执行频率。
处置用 _.throttle(fn, 300) 把处理压到固定频率,节流适合这类持续触发而不是等待停止的场景。
现象用户输入的 < 标签被当成真实 HTML 渲染,页面结构被破坏。
原因把未转义的文本直接插入 HTML。
处置插入前用 _.escape(str) 做 HTML 转义,或改用 textContent 写入纯文本。
现象把 lodash 整包 import 进项目,产物体积多出几百 KB。
原因整包引入时打包器难以在所有场景下摇掉未使用的函数。
处置改用 lodash-es 的 ES Module 版本,或按需单函数引入:import cloneDeep from "lodash/cloneDeep",让打包器能摇掉其余代码。
现象_.merge 之后源对象里本该被整体替换的字段反而留在了目标对象里。
原因merge 是按层深合并,嵌套对象不会被整体覆盖,而是逐键递归合并。
处置确实要整体替换时先 _.cloneDeep 再赋值,或改用一次性覆盖语义的处理方式,不要依赖 merge 做替换。
现象_.isEmpty 判断空字符串或数字为 true,业务上的「有值」判断出错。
原因isEmpty 的语义是「自身没有可枚举属性」,对 0 这样的原始值也返回 true。
处置先 _.isNil(value) 排除 null/undefined,再用真假判断或 _.inRange 等针对类型的检查分别处理。
现象_.get(obj, "a.b.c") 明明数据里有值却总拿到默认值。
原因路径是字符串形式,若键名里本身含有点号(如 "a.b" 是一个整体键名),会被当成嵌套路径拆开。
处置改用数组形式路径传参,_.get(obj, ["a.b", "c"]) 明确键名边界,避免点号歧义。
排障路径
1安全读取深层属性并观察回退值
_.get(obj, "a.b.c", "default")a 或 a.b 不存在时不会抛错,而是返回 "default",可直接确认断链位置。
2确认数组是否真的为空
_.isEmpty(value)对数组、对象、空串都适用;对数字返回 true,需配合 _.isNil 判断。
3深拷贝后修改副本验证不污染原对象
_.cloneDeep(obj)在副本上改嵌套字段,再打印原对象,若原对象不变说明深拷贝生效。
4查看去重与合并后的结果
_.union([1, 2], [2, 3])输出 [1, 2, 3],可确认唯一值合并的语义符合预期。
5给高频事件加防抖
_.debounce(fn, 300)把 fn 包一层赋给事件处理器,连续触发结束后只执行最后一次。
6按需引入单个方法控制体积
import cloneDeep from "lodash/cloneDeep"只引入实际用到的那一个函数,其余代码会被打包器摇掉。
命令示例
debounce 防抖搜索 + throttle 节流滚动
const onSearch = _.debounce(function (q) {
$.getJSON("/api/search?q=" + q, render)
}, 300)
const onScroll = _.throttle(function () {
updateParallax()
}, 100)
$("#q").on("input", (e) => onSearch(e.target.value))debounce 等你停止输入后才发请求,适合搜索与自动保存;throttle 固定频率触发,适合滚动、resize 这类持续事件。注意默认配置下首次调用会延后到静默期结束。
omit 剔除敏感字段 + get 读取深层配置
const safe = _.omit(user, ["password", "token"])
const timeout = _.get(config, "http.timeout", 3000)
_.set(config, "http.timeout", 5000)把白名单式序列化(omit)放在统一出口,比在每个接口手写字段列表更不容易漏;get 的默认值只在解析出 undefined 时生效,显式存的 null 会原样返回。
groupBy 分组 + keyBy 建索引 + sortBy 多字段排序
const byAge = _.groupBy(users, "age")
const byId = _.keyBy(users, "id")
const sorted = _.sortBy(users, ["age", "name"])
const counts = _.countBy([1, 2, 2, 3], Math.floor)在循环里反复 users.find(u => u.id === id) 是 O(n) 扫描,keyBy 一次转成对象后都是 O(1);sortBy 的数组形式表示按顺序逐级比较,是最常被漏用的签名。
memoize 缓存昂贵计算 + once 保护单次初始化
const expensive = _.memoize(function (key) {
return heavyLookup(key)
})
const init = _.once(function () {
client.start()
})memoize 以第一个参数为缓存键,对纯函数最安全;传入对象参数会因引用不同而缓存不命中。once 适合模块顶层的初始化,防止被多次 require 或多次点击重复执行。
数据清洗链路:chunk + flattenDeep + uniq
const ids = _.uniq(_.flattenDeep(raw));
const pages = _.chunk(ids, 100);
for (const page of pages) {
upload(page)
}外部接口常返回带 null 与嵌套结构的混合数组,flattenDeep 拉平后 uniq 去掉重复 id,再按固定大小 chunk 分批提交,避免一次性请求体过大被网关拒绝。
常见坑与注意事项
- _.merge(target, source) 会原地改写第一个参数并返回它,多个请求共用的配置对象会被悄悄写脏;先 _.cloneDeep 再 merge。
- _.uniq / _.union 只做 SameValueZero 浅比较,对象数组里的重复对象去不掉;需要对内容去重时改用 _.uniqWith(arr, _.isEqual)。
- _.get(obj, "a.b", 默认值) 只在解析结果为 undefined 时回退默认值:显式存着 null 时返回的是 null 而非默认值,下游按默认值做的判断会全部失效。
- _.template 的 <%= %> 不做 HTML 转义,拼入用户输入即成 XSS;需要输出原始 HTML 时才用 <%- %>,可读文本一律用 <%= %>。
- _.memoize 没有缓存上限且以参数值为键,对每条记录都调用的场景会把整份数据塞进缓存;长生命周期页面上表现为内存只涨不落。
- _.debounce / _.throttle 返回的是新函数,每次绑定都产生一个新实例:事件绑定写 $(el).on("click", _.debounce(fn, 300)) 会在每次绑定时叠加防抖,实际退化成节流甚至多次执行。
提示
- lodash-es 是 ES Module 版本,支持 Tree Shaking,打包体积更小。
- _.get 和 _.set 可以安全操作深层嵌套对象,避免 undefined 报错。
- debounce 适合搜索输入框,throttle 适合滚动/resize 事件。
- 现代 JS 可以用原生方法替代部分 Lodash,如 Array.prototype.flat()、Object.keys()。
- 按需引入单个方法 import cloneDeep from "lodash/cloneDeep" 可进一步减小体积。
常见问题
防抖和节流在时间线上有什么区别?
_.debounce(fn, 300) 以「最后一次触发」为基准:连续触发期间不断重置计时器,只有停止 300 毫秒后才真正执行一次,适合搜索输入框——用户敲完关键词才去请求。_.throttle(fn, 300) 以固定频率执行:不管触发多密集,300 毫秒窗口内最多执行一次,窗口过后若仍有新触发就再执行一次,适合滚动和 resize 这类高频且需要连续反馈的事件。选错的典型症状是搜索框发一串请求(该用防抖却用了节流),或者滚动到底后列表还来不及加载(该用节流却用了防抖)。
深拷贝和深合并,什么时候必须用 Lodash 而不是展开运算符?
展开运算符 {...obj} 是浅拷贝,只复制一层引用:obj.nested 依然与原对象共享同一个引用,改副本的嵌套字段会污染原对象。_.cloneDeep(obj) 递归复制每一层,改副本完全不影响原对象,适合编辑器式「取消编辑就回滚」的场景。_.merge(target, source) 则是深合并,嵌套对象按层合并而不是整体覆盖——传 source 里只有部分字段时,未提及的字段会保留 target 的原值。危险写法是 {...defaults, ...user}:它对顶层有效,一旦有嵌套配置就会整块丢掉默认值,应改成从 defaults 出发的深合并。
把数组转成以 id 为键的对象,用哪个方法最合适?
_.keyBy(users, "id") 直接以指定属性作为键输出对象,得到形如 { "1": user1, "2": user2 } 的结构,后续取用是 O(1),适合「渲染前把列表转成字典反复按 id 查找」的场景。相近但语义不同:_.groupBy(users, "age") 按值分组,输出的是 { 25: [user...], 30: [user...] },同一个值会聚成数组,适合按年龄/分类分堆展示;_.pick(obj, ["name", "age"]) 是对单个对象取子集,得到白名单新对象,常用于剔除 password 这类敏感字段时配合 _.omit。
为什么推荐从 lodash-es 按需引入而不是整包导入?
整包 import _ from "lodash" 会把整个库纳入构建,即便你只用 debounce 和 groupBy。lodash-es 是 ES Module 版本,配合 Tree Shaking,打包器能识别出未使用的具名导出并剔除,配合按需单文件引入 import cloneDeep from "lodash/cloneDeep" 可以做到只打进实际调用的函数。反过来说,Tree Shaking 依赖静态的 import 语句,若把 _ 当成运行时对象做属性访问(如 map 下标遍历),摇树就会失效。因此引入方式要和用法一起设计:用具名导入,别把库对象存起来传。
嵌套对象取值和合并,_.get / _.set / _.has 是怎么配合的?
三者共用「属性路径字符串」这一套约定。_.get(obj, "a.b.c", "default") 在 a 或 a.b 缺失时直接返回默认值而不抛错,适合处理来路不明的响应体;_.set(obj, "a.b.c", value) 写值时会自动创建中间的 a 和 a.b 对象,省掉逐层判空;_.has(obj, "a.b") 则用于判断路径是否存在,常用作「字段必填」的守卫。路径支持数组写法(_.get(obj, ["a", "b"])),在键名本身含点号时必须用数组形式,避免被误拆成多层。注意 _.get 只是读取快照,源对象为空时它返回 undefined 而不会创建节点,要真正建结构得用 _.set。
由 巧匠 维护
公开更新于 2026年7月21日,内容持续校对官方文档。