es-toolkit/compat 的 flatten 深度解析:Lodash 兼容单层数组展平与源码原理
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
导读
flatten是 es-toolkit 兼容层(es-toolkit/compat)中面向 Lodash 用户提供的数组展平函数,它按 Lodash 语义将嵌套数组只展平一层,并完整支持arguments对象、Symbol.isConcatSpreadable对象、类数组(ArrayLike)以及null/undefined等特殊输入。本文将围绕 docs/compat/reference/array/flatten.md 展开,先给出可直接运行的用法示例,再深入 flatten.ts 与 flattenDepth.ts 的源码剖析其判定逻辑,最后对比 es-toolkit 原生flatten的性能差异与取舍,帮助你准确选用合适的 API。
一、flatten是什么:Lodash 兼容版的单层展平
flatten的作用是将一个数组按一层深度展平:只解开最外层的嵌套,更深层的数组原样保留在结果中。
import { flatten } from 'es-toolkit/compat'; flatten([1, [2, [3, [4]], 5]]); // Result: [1, 2, [3, [4]], 5]从输出可以看到,[3, [4]]这一层并没有被继续展开——这正是「单层展平」与flattenDeep(无限深度展平)的核心区别。
与 Lodash 的_.flatten一样,compat 版flatten的定位是逐字兼容 Lodash 的输入输出语义,因此它需要处理大量 Lodash 特有的边界输入(详见下文第三节)。它的完整函数签名定义在 src/compat/array/flatten.ts:
export function flatten<T>(array: ArrayLike<T | readonly T[]> | null | undefined): T[] { return flattenDepth(array as ListOfRecursiveArraysOrValues<T> | null | undefined, 1); }可以看到,flatten本身只是一个薄封装:把深度固定为1后委托给flattenDepth执行真正的展平逻辑。
二、安装与快速上手
安装
es-toolkit 是一个 npm 包,安装后即可使用:
npm install es-toolkit兼容层 API 通过子路径es-toolkit/compat引入,与原生 API(es-toolkit或es-toolkit/array)相互独立:
import { flatten } from 'es-toolkit/compat';基础用法
// 基础单层展平 flatten([1, [2, [3, [4]], 5]]); // Result: [1, 2, [3, [4]], 5]// 空数组与纯一维数组 flatten([]); // [] flatten([1, 2, 3]); // [1, 2, 3]三、核心行为:完整覆盖 Lodash 的边界输入语义
原文档明确强调,compat 版flatten除了基础展平外,还针对以下特殊输入提供了与 Lodash 一致的兼容处理,这也是它区别于原生flatten的关键所在。
1. 支持arguments对象
函数内部的arguments对象会被当作数组一样展平:
import { flatten } from 'es-toolkit/compat'; function example() { return flatten(arguments); } example(1, [2, 3], [[4]]); // Result: [1, 2, 3, [4]]arguments中的元素1、[2, 3]、[[4]]被逐项取出,其中[2, 3]被展开,[[4]]只展开到[4](单层),符合预期。
2. 支持带Symbol.isConcatSpreadable的对象
任何带有真值Symbol.isConcatSpreadable的对象,其索引属性会被当作数组元素展开:
const spreadable = { 0: 'a', 1: 'b', length: 2, [Symbol.isConcatSpreadable]: true }; flatten([1, spreadable, 3]); // Result: [1, 'a', 'b', 3]这与原生Array.prototype.concat的展开规则一致,是 Lodash 兼容行为的重要一环。
3.null与undefined视为空数组
import { flatten } from 'es-toolkit/compat'; flatten(null); // [] flatten(undefined); // [] flatten([]); // []传入null或undefined不会抛错,而是返回空数组,保证了与 Lodash 一致的容错性。
4. 稀疏数组按稠密数组处理
测试用例 flatten.spec.ts 验证了稀疏数组(如Array(3))会被填充为显式的undefined元素:
const array = [[1, 2, 3], Array(3)]; flatten(array); // Expected: [1, 2, 3, undefined, undefined, undefined] // 且结果中 '4' in actual 为 true(索引 4 真实存在)5. 支持类数组(ArrayLike)与字符串
compat 版flatten接受ArrayLike<T>输入,包括字符串和普通类数组对象:
flatten({ 0: [1, 2, 3], length: 1 }); // [1, 2, 3] flatten('123'); // ['1', '2', '3'] flatten(arguments); // [1, 2, 3](函数调用时传入 1,2,3)而非类数组对象(如{ 0: 'a' },没有合法length)则返回空数组:
flatten({ 0: 'a' } as any); // []参数与返回值
| 项目 | 说明 |
|---|---|
array | ArrayLike<T> \| null \| undefined:要展平的数组(或类数组),允许为null/undefined |
| 返回值 | T[]:展平一层后的新数组,原数组不会被修改 |
四、源码剖析:flatten底层到底做了什么
1. 委托链:flatten→flattenDepth
flatten把「展平一层」这一语义翻译为flattenDepth(array, 1),真正的算法实现在 src/compat/array/flattenDepth.ts:
export function flattenDepth<T>(array: ListOfRecursiveArraysOrValues<T> | null | undefined, depth = 1): T[] { if (!isArrayLike(array)) { return []; } const result: T[] = []; const flooredDepth = Math.floor(depth); const recursive = (arr: readonly T[], currentDepth: number) => { for (let i = 0; i < arr.length; i++) { const item = arr[i]; if (isFlattenable(item) && currentDepth < flooredDepth) { recursive(item as T[], currentDepth + 1); } else { result.push(item); } } }; recursive(Array.from(array) as T[], 0); return result; }关键点:
- 非类数组直接返回
[]:这是对null/undefined/普通对象容错的第一道闸门,由isArrayLike判定。 - 深度取整:
Math.floor(depth)意味着flattenDepth(arr, 1.9)等价于深度1。 - 递归展开:只有当当前元素「可被展平」且「未超过深度上限」时才递归,否则原样
push。
2. 可展平判定:isFlattenable
compat 版展平哪些东西,取决于内部的isFlattenable判定(同样位于 src/compat/array/flattenDepth.ts):
function isFlattenable(value: unknown): boolean { return isArray(value) || isArguments(value) || Boolean(value && (value as any)[Symbol.isConcatSpreadable]); }这条判定同时覆盖了第三节中的三类特殊输入:真正的数组(isArray)、arguments对象(isArguments)、以及带真值Symbol.isConcatSpreadable的对象。输入类型声明ListOfRecursiveArraysOrValues<T>定义在 src/compat/_internal/ListOfRecursiveArraysOrValues.ts,即ArrayLike<T | RecursiveArray<T>>。
3. 兼容层内部还有一个针对类数组的辅助函数
在 src/compat/_internal/flattenArrayLike.ts 中还有一个flattenArrayLike,专门把一组类数组对象逐项拼接为普通数组:
export function flattenArrayLike<T>(values: Array<ArrayLike<T>>): T[] { const result: T[] = []; for (let i = 0; i < values.length; i++) { const arrayLike = values[i]; if (!isArrayLikeObject(arrayLike)) { continue; } for (let j = 0; j < arrayLike.length; j++) { result.push(arrayLike[j] as T); } } return result; }从源码结构看,它服务于 compat 层内部对类数组的通用拼接需求,体现了兼容层「处处以类数组为输入基础」的设计取向。
五、对比:compat 版 vs es-toolkit 原生flatten
原文档在开头就给出了一条明确的性能警告:compat 版flatten因需要处理null/undefined与ArrayLike类型而较慢,建议改用 es-toolkit 原生flatten。
原生flatten的实现
es-toolkit 原生flatten位于 src/array/flatten.ts,它是面向现代 JavaScript 的精简实现:
export function flatten<T, D extends number = 1>(arr: readonly T[], depth = 1 as D): Array<FlatArray<T[], D>> { const result: Array<FlatArray<T[], D>> = []; const flooredDepth = Math.floor(depth); const recursive = (arr: readonly T[], currentDepth: number) => { for (let i = 0; i < arr.length; i++) { const item = arr[i]; if (Array.isArray(item) && currentDepth < flooredDepth) { recursive(item, currentDepth + 1); } else { result.push(item as FlatArray<T[], D>); } } }; recursive(arr, 0); return result; }两者的差异可以总结为下表:
| 维度 | compat 版flatten | 原生flatten(es-toolkit/array) |
|---|---|---|
| 入口 | es-toolkit/compat | es-toolkit/es-toolkit/array |
| 输入类型 | ArrayLike<T> \| null \| undefined | readonly T[] |
| 深度参数 | 固定为 1(由flattenDepth支撑) | 可选depth,默认 1,支持任意深度 |
| 可展平判定 | isArray/isArguments/Symbol.isConcatSpreadable | 仅Array.isArray |
| 容错行为 | null/undefined返回[],类数组、arguments 均可处理 | 要求数组输入,逻辑更少 |
| 性能 | 较慢(判定分支多、需Array.from转换) | 更快(仅内建Array.isArray分支) |
原生版本的完整文档见 docs/reference/array/flatten.md,其用法为:
import { flatten } from 'es-toolkit/array'; const array = [1, [2, 3], [4, [5, 6]]]; flatten(array); // [1, 2, 3, 4, [5, 6]] flatten(array, 2); // [1, 2, 3, 4, 5, 6]选择建议:在全新代码中优先使用原生flatten;仅当需要从 Lodash 迁移、或确实依赖 arguments/类数组/Symbol.isConcatSpreadable等兼容语义时,才使用es-toolkit/compat版本。
六、测试验证:compat 语义有据可查
compat 版flatten的行为由 src/compat/array/flatten.spec.ts 中的 Vitest 用例逐一锁定,主要包括:
- arguments 对象展平:
flatten([args, [args]])结果为[1, 2, 3, args]; - 稀疏数组稠密化:
[[1, 2, 3], Array(3)]展平后补齐 3 个显式undefined; Symbol.isConcatSpreadable对象:{ 0: 'a', length: 1, [Symbol.isConcatSpreadable]: true }展平为['a'];- 空数组嵌套:
[[], [[]], [[], [[[]]]]]展平为[[], [], [[[]]]]; - 非类数组返回空数组:
flatten({ 0: 'a' })返回[]; - 类数组与字符串:
flatten({ 0: [1, 2, 3], length: 1 })、flatten('123')、flatten(args)均按预期输出。
这些用例直接印证了本文第三节描述的每一项兼容行为,可作为迁移或回归测试时的参考基线。
七、相关 API:从flatten延伸的展平家族
compat 层围绕展平提供了一组配套函数,全部位于 src/compat/array 目录下:
| 函数 | 说明 | 实现文件 |
|---|---|---|
flatten | 只展平一层,兼容 arguments/类数组/Symbol.isConcatSpreadable | flatten.ts |
flattenDepth | 展平到指定深度,depth默认为 1,是flatten的底层实现 | flattenDepth.ts |
flattenDeep | 无限深度展平(等价于flattenDepth(array, Infinity)) | flattenDeep.ts |
flattenDeep的实现非常简洁:
export function flattenDeep<T>(array: ListOfRecursiveArraysOrValues<T> | null | undefined) { return flattenDepth(array, Infinity) as any; }它们的组合关系是:flatten与flattenDeep都只是flattenDepth在不同深度参数下的特例,理解了flattenDepth的递归算法,就等于理解了整个 compat 展平家族。
结语
es-toolkit 的 compat 版flatten是一份「语义先行」的 Lodash 兼容实现:它牺牲了一部分性能,换来对arguments、类数组、Symbol.isConcatSpreadable、null/undefined等 Lodash 生态边界输入的完整支持。如果你正在从 Lodash 迁移到 es-toolkit,直接替换_.flatten即可得到一致的结果;而如果是在新项目里追求极致性能,则应选择 src/array/flatten.ts 中的原生实现。无论走哪条路径,都建议结合 flatten.spec.ts 中的测试用例验证你的迁移行为,确保边界场景万无一失。
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考