es-toolkit 的 Lodash 兼容版 `flatten`:单层扁平化的行为细节与实现原理
2026/9/16 12:25:24 网站建设 项目流程

es-toolkit 的 Lodash 兼容版flatten:单层扁平化的行为细节与实现原理

【免费下载链接】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

本文围绕 es-toolkit 的 Lodash 兼容模块es-toolkit/compat中的flatten函数展开,说明它与核心es-toolkit版本flatten的差异、完整的参数语义(ArrayLikeArgumentsSymbol.isConcatSpreadablenull/undefined等),并结合 src/compat/array/flatten.ts、src/compat/array/flattenDepth.ts 与 src/compat/array/flatten.spec.ts 的源码与测试,讲透兼容版flatten为什么"慢一点"、在什么场景下值得使用,以及如何平滑迁移到核心 API。

先看结论:为什么文档建议优先使用es-toolkitflatten

es-toolkit/compat是为了与 Lodash 接口、行为 1:1 对齐而存在的兼容层(见 docs/compat/intro.md)。官方文档在flatten的日语参考页(即 docs/ja/compat/reference/array/flatten.md)开头给出了明确警告:

请使用es-toolkitflatten。compat 版本的flatten因为要处理null/undefinedArrayLike类型等,运行会更慢。

也就是说:

  • 核心版es-toolkit/arrayflatten):只接受真正的数组(readonly T[]),行为等价于 JavaScript 原生的Array#flat,但更快;
  • 兼容版es-toolkit/compatflatten):为对齐 Lodash,额外支持Arguments对象、任意ArrayLikeSymbol.isConcatSpreadable以及null/undefined输入,因此多了一层类型判断逻辑,性能略低、包体略大。

两者共享同一个思想:把嵌套数组展开一层(depth = 1)。兼容版flatten的实现只有一行,本质上是把工作委托给了flattenDepth

// 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的完整行为规范

基本用法:单层扁平化

flatten只展开一层嵌套,更深层的数组保持原样:

import { flatten } from 'es-toolkit/compat'; // 基本的扁平化(1 层) flatten([1, [2, [3, [4]], 5]]); // 结果: [1, 2, [3, [4]], 5]

[3, [4]]这一层只被解开一次,内层的[4]不会被继续展开。

支持Arguments对象

与 Lodash 一致,兼容版flatten可以把函数内部的arguments对象当作数组处理:

import { flatten } from 'es-toolkit/compat'; function example() { return flatten(arguments); } example(1, [2, 3], [[4]]); // 结果: [1, 2, 3, [4]]

支持带Symbol.isConcatSpreadable的对象

JavaScript 的Array.prototype.concat会检查对象的Symbol.isConcatSpreadable属性来决定是否将其展开;Lodash 的flatten也复用了这一约定,兼容版同样支持:

import { flatten } from 'es-toolkit/compat'; const spreadable = { 0: 'a', 1: 'b', length: 2, [Symbol.isConcatSpreadable]: true }; flatten([1, spreadable, 3]); // 结果: [1, 'a', 'b', 3]

nullundefined视为空数组

这是 Lodash 兼容行为中最常见的一个差异点:传入nullundefined不会抛错,而是返回空数组。

import { flatten } from 'es-toolkit/compat'; flatten(null); // [] flatten(undefined); // [] flatten([]); // []

参数与返回值

  • arrayArrayLike<T> | null | undefined):需要扁平化的数组(或类数组对象)。
  • 返回值(T[]):扁平化后的新数组,不会修改原数组。

注意返回值类型是T[]:由于只展平一层,元素类型在类型层面不会像flattenDeep那样被递归推导。

源码级原理:flattenDepthisFlattenable

兼容版flatten内部委托给 src/compat/array/flattenDepth.ts,其中depth = 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; } function isFlattenable(value: unknown): boolean { return isArray(value) || isArguments(value) || Boolean(value && (value as any)[Symbol.isConcatSpreadable]); }

由此可以归纳出兼容版flatten的几个关键实现事实:

  1. 入口先做isArrayLike检查:非类数组对象(如{ 0: 'a' })直接返回[]。这是flatten(null)flatten(undefined)返回[]的根源。
  2. "可扁平化"判定有三条isArray(真数组)、isArgumentsarguments对象)、或对象上存在真值的Symbol.isConcatSpreadable属性。这也解释了文档示例中spreadable对象能被展开的原因。
  3. 深度受Math.floor控制depth会被向下取整,flatten1即只递归一层。
  4. 稀疏数组会被"压实":测试 src/compat/array/flatten.spec.ts 验证了flatten([[1, 2, 3], Array(3)])的结果是[1, 2, 3, undefined, undefined, undefined],即空洞位置会被补成undefined(这也是 Lodash 行为之一)。

isArrayLikeObject的逐项拷贝逻辑还可参考 src/compat/_internal/flattenArrayLike.ts,它展示了类数组元素如何被逐个push进结果数组。

与核心版flatten的对比

核心 API 的实现在 src/array/flatten.ts,两者差异集中体现在"接受什么输入、怎么判定嵌套元素"上:

维度核心版es-toolkit/array兼容版es-toolkit/compat
签名flatten<T, D>(arr: readonly T[], depth = 1)flatten<T>(array: ArrayLike<T \| readonly T[]> \| null \| undefined)
嵌套元素判定Array.isArray(item)isArray/isArguments/Symbol.isConcatSpreadable三者其一
是否接受null/undefined不接受(类型上不合法)接受,返回[]
是否接受类数组 /arguments不接受接受
深度参数支持,泛型D会反映到返回类型Array<FlatArray<T[], D>>固定为 1(flatten层面),类型为T[]
性能判定路径最短,更快多类型判断,略慢、包体略大

因此文档的建议非常明确:如果你的代码不依赖 Lodash 的null/undefinedarguments、类数组等兼容语义,直接用核心版

import { flatten } from 'es-toolkit/array'; flatten([1, [2, 3], [4, [5, 6]]]); // [1, 2, 3, 4, [5, 6]] flatten([1, [2, 3], [4, [5, 6]]], 2); // [1, 2, 3, 4, 5, 6]

核心版的行为与原生Array#flat一致,但由于内部用显式循环而非concat展开,实际运行更快。

迁移路径:从兼容版到核心版

按照 docs/compat/intro.md 推荐的迁移流程:

  1. 第一步:把lodash/lodash-es的导入直接换成es-toolkit/compat,调用点一行不改,行为保持一致:
    import { flatten } from 'es-toolkit/compat';
  2. 第二步:逐步清理调用点,确认代码中没有依赖arguments、类数组、null/undefined等 Lodash 特殊语义后,改用核心 API:
    import { flatten } from 'es-toolkit/array';

    换来的是更小的打包体积和更快的运行速度。

如果你确实需要"展平任意深度"的兼容行为,兼容层还提供flattenDeepflattenDepth(见 src/compat/array/flattenDeep.ts,内部以Infinity为深度调用flattenDepth),可按需选用。

测试佐证:行为由测试用例锁定

src/compat/array/flatten.spec.ts 用 Vitest 覆盖了以下行为,可作为兼容性契约的参考:

  • 扁平化arguments对象:flatten([args, [args]])得到[1, 2, 3, args]
  • 稀疏数组按稠密数组处理(空洞补undefined);
  • 带真值Symbol.isConcatSpreadable的对象被展开;
  • 空数组嵌套([[], [[]], [[], [[[]]]]])只展平一层;
  • 多层嵌套数组只展平一层;
  • 非类数组对象({ 0: 'a' })返回[]
  • 类数组支持:flatten({ 0: [1, 2, 3], length: 1 })flatten('123')flatten(args)均按预期展开。

这些测试同时兼容 Lodash 自身的测试套件,是es-toolkit/compat实现 100% 兼容目标的直接证据。

小结

  • 兼容版flatten1 层深度展开嵌套数组,完整支持ArgumentsSymbol.isConcatSpreadable、类数组对象,并将null/undefined视为空数组;
  • 它内部委托给flattenDepth,通过isFlattenableisArray/isArguments/isConcatSpreadable三选一)判定可展开元素;
  • 这些额外语义带来了可感知的性能与包体开销,因此官方文档明确建议:新代码优先使用es-toolkit/array的核心版flatten;只有迁移 Lodash 存量代码、且依赖其特殊行为时,才使用es-toolkit/compat版本,并在迁移完成后逐步切换回核心 API。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询