☰
just-flatten-it 版本演进全解析:从 CHANGELOG 到零依赖数组扁平化的源码实现
2026/10/15 2:09:20 网站建设 项目流程
  • 开发工具

【免费下载链接】just

A library of dependency-free JavaScript utilities that do just one thing.

项目地址:https://gitcode.com/gh_mirrors/jus/just
点击查看免费下载

本文以 packages/array-flatten/CHANGELOG.md 为主线,逐条解析just-flatten-it从 5.1.0 到 5.2.0 四个版本的技术变更,并结合源码、类型定义、打包配置与测试用例,完整还原这个零依赖数组扁平化工具的实现原理与发布演进。读完本文,你将能理解其双模块(CJS/ESM)导出机制、递归类型设计、基于栈的展平算法,以及版本变更背后对应的真实代码改动。

一、工具概览:一个只做一件事的数组展平器

just-flatten-it是 just 库(A library of dependency-free JavaScript utilities that do just one thing)中的一个零依赖 npm 模块,功能只有一个:返回展平后的数组。其 package.json 中dependencies为空、keywords明确标注了no-dependencies,安装方式与其他 npm 包一致:

npm install just-flatten-it
yarn add just-flatten-it

基本用法(README.md):

import flatten from 'just-flatten-it'; flatten([[1, [2, 3]], [[4, 5], 6, 7, [8, 9]]]); // [1, 2, 3, 4, 5, 6, 7, 8, 9] flatten([[1, [2, 3]], [[4, 5], 6, 7, [8, 9]]], 1); // [1, [2, 3], [[4, 5], 6, 7, [8, 9]]]

省略depth参数时数组被完全展平;传入depth时只展平到指定层数。这两个语义正是后文版本演进与实现解析的落脚点。

二、版本脉络:CHANGELOG 记录的四个关键节点

CHANGELOG 完整记录了 4 个版本(当前最新版本 5.2.0 与 package.json 中version: "5.2.0"一致):

版本变更类型变更内容
5.2.0Minor Changes将 Node 模块文件由.js重命名为.cjs
5.1.2Patch Changes类型改进(Type improvements)
5.1.1Patch Changes修复:调整导出顺序,将默认导出置于最后(issue #488)
5.1.0Minor Changespackage.json 更新,修复 issue #467、#483

虽然每一条 changelog 都只有一句话,但结合仓库中的实际产物,可以一一还原每条变更对应的真实代码改动。下面按时间倒序逐条深挖。

三、5.2.0:Node 模块从.js重命名为.cjs,确立双模块导出

这是最近一次发布,也是最能体现包结构设计的一次变更:Node 侧的 CommonJS 入口文件由index.js重命名为index.cjs。在 packages/array-flatten 目录下,可以看到当前包同时维护三个入口文件:

  • index.cjs:CommonJS 实现,文件末尾module.exports = flatten;
  • index.mjs:ESM 实现,文件末尾export {arrayFlatten as default};
  • index.d.ts:TypeScript 类型声明

与之配套的是 package.json 中完整的exports条件导出映射:

{ "name": "just-flatten-it", "version": "5.2.0", "type": "module", "exports": { ".": { "types": "./index.d.ts", "require": "./index.cjs", "import": "./index.mjs" }, "./package.json": "./package.json" }, "main": "index.cjs", "types": "index.d.ts" }

这里的关键点在于:

  • "type": "module"让整个包以 ESM 语义解析,因此 CommonJS 入口必须使用.cjs扩展名以显式声明自身模块格式——这正是 5.2.0 重命名的直接原因;
  • exports字段为require与import分别指向index.cjs与index.mjs,Node 与打包工具可以按模块系统自动选择正确入口;
  • main与types保留兼容旧解析逻辑的兜底字段。

有趣的是,index.mjs并不是手写的,而是由 Rollup 从 CJS 源文件构建生成的。每个包的 rollup.config.js 统一调用根目录的 config/createRollupConfig.js:

module.exports = function createRollupConfig(path) { return { input: resolve(path, './index.cjs'), output: { file: resolve(path, './index.mjs'), format: 'esm', }, plugins: [cjs()], }; };

也就是说:手写维护一份 CJS 源码(index.cjs),通过@rollup/plugin-commonjs转译出 ESM 版本(index.mjs),构建命令为npm run build(根 package.json 中为lerna run build --stream,各包内为rollup -c)。两个文件的实现体完全一致,仅入口导出语句不同,可以互相印证。

四、5.1.1:修复导出顺序,默认导出置于最后(#488)

CHANGELOG 记录这条修复为“reorder exports to set default last”,即将默认导出语句调整到文件末尾。对照当前构建产物,确实可以看到该约定已被落实:

  • index.mjs 中,文件开头先声明var arrayFlatten = flatten;,实现函数定义在前,最后一行为export {arrayFlatten as default};
  • index.cjs 中,module.exports = flatten;同样位于文件最前,而实现主体在后。

这种“导出放最后”的顺序在混用多种模块系统、或部分工具链按顺序解析导出声明时,能减少默认导出被提前截断/覆盖的兼容性问题。虽然 changelog 只给出 issue 编号 #488,但当前产物中“默认导出出现在文件末尾”的形态,正是这次修复落地后可以观察到的结果。

五、5.1.2:类型改进——用递归类型描述任意层嵌套

5.1.2 的类型改进,对应到当前 index.d.ts,核心是引入了一个递归数组类型来精确描述可展平输入:

type RecursiveList<T> = readonly (T | readonly T[] | RecursiveList<T>)[]; declare function flatten<T>(arr: RecursiveList<T>, depth?: number): T[]; export default flatten;

这个类型定义的表达能力体现在三方面:

  1. 递归引用自身(RecursiveList<T>内再次出现RecursiveList<T>),因此任意深度的嵌套数组(如[[[[[4]]]]])都能被类型系统接受;
  2. 支持 readonly 数组:readonly修饰使flatten(numbers)(其中numbers: readonly number[])合法,而不强制要求可变数组;
  3. 返回值类型为T[]:无论嵌套多深,只要完全展平,输出就是元素类型T的一维数组。

配套的类型测试 index.tests.ts 用@ts-expect-error明确圈定了非法调用,比如:

// OK const numbers: readonly number[] = [1, 2, 3] flatten(numbers); flatten([1, [2, numbers], 4]); flatten([1, [2, [3]], [[[[[4]]]]]], 2); // Not OK(以下调用在编译期必须报错) // @ts-expect-error flatten(5); // @ts-expect-error flatten({ a: 5 }); // @ts-expect-error flatten([1], true); // @ts-expect-error flatten(new Set());

这些用例覆盖了“非数组输入”“depth 为布尔值/字符串”“Map/Set 容器”等错误场景,保证类型定义在编译期就拦住非法调用(根 package.json 的test-types脚本通过tsc执行这一检查)。

六、核心实现:输入校验 + 基于栈的迭代展平

看完版本演进,再看实现本体。index.cjs 的入口函数先做严格的输入校验:

function flatten(arr, depth) { if (!Array.isArray(arr)) { throw new Error('expected an array'); } if (depth !== undefined && typeof depth !== 'number') { throw new Error('depth expects a number'); } return flattenHelper(arr, typeof depth === 'number' ? depth : Infinity); }

两个关键语义:

  • 非数组输入直接抛错(expected an array),且不允许Map、Set等类数组容器;
  • depth必须显式传undefined或数字;传null、{}、[]、函数等都会抛错(depth expects a number);省略时按Infinity处理,即完全展平。

真正执行展平的是 flattenHelper,一个值得玩味的栈式迭代实现(而非递归):

function flattenHelper(arr, depth) { var stack = arr.slice(); var result = []; while (stack.length) { var item = stack.pop(); if (Array.isArray(item) && depth > 0) { stack.push.apply(stack, item); depth--; } else { result.push(item); } } return result.reverse(); }

算法要点:

  1. 用arr.slice()复制一份输入,避免污染原数组(测试中“flattened arrays are unchanged”验证了这一点);
  2. 每次pop栈顶元素:若它还是数组且depth > 0,就把它展开推回栈中并把当前层的 depth 减一;否则直接收进result;
  3. 由于从栈尾弹出、按“后进先出”顺序收集,最终结果与输入顺序相反,所以末尾用result.reverse()还原原始顺序。

整个展平过程不依赖任何原生 API,也不修改输入数组。这一点在 test/array-flatten/index.cjs 中体现得尤其彻底:测试先保存原生Array.prototype.flat,将其临时置为null,跑完用例后再用Object.defineProperty恢复——从源码结构看,这是刻意验证实现不依赖原生 flat、即使原生方法缺失也能独立工作。

七、测试与质量保障:用例如何锁定版本承诺

test/array-flatten/index.cjs 用 tape 框架覆盖了五个维度的行为,恰好与上文实现一一对应:

测试用例验证点
flattened arrays are unchanged已展平数组原样返回,且不修改原数组
unflattened arrays are flattened混合数组与对象数组均完全展平
throws on non-arrays{n: 4}、2、undefined均抛错
flatten to specific depthdepth为 0/1/2/3/4 时逐层展平,结果逐一断言
throws when depth is not a number'depth'、null、{}、[]、函数均抛错

其中深度语义的测试最为直观:

var arr = [1, [2, [3, [4, [5]]]]]; flatten(arr, 0); // [1, [2, [3, [4, [5]]]]] flatten(arr, 1); // [1, 2, [3, [4, [5]]]] flatten(arr, 2); // [1, 2, 3, [4, [5]]] flatten(arr, 3); // [1, 2, 3, 4, [5]] flatten(arr, 4); // [1, 2, 3, 4, 5]

可见depth表示“允许展开的嵌套层数”:0表示完全不展平,层数越大展得越深,直到完全扁平。根 package.json 的test脚本将 lint、tape 行为测试与tsc类型测试串成一条流水线,确保行为与类型两方面的回归都在发布前被拦截。

八、小结:一条 changelog 背后的完整工程链路

回顾这 4 条 changelog,可以串联出just-flatten-it完整的工程链路:

  • 5.1.0更新 package.json 导出相关配置(#467、#483),为后续模块格式调整铺路;
  • 5.1.1规范默认导出的书写位置(#488),保障多模块系统下的兼容性;
  • 5.1.2升级类型系统,用递归类型与 readonly 支持覆盖任意嵌套输入;
  • 5.2.0最终将 Node 模块重命名为.cjs,与"type": "module"的包级 ESM 语义解耦,形成 CJS/ESM 双入口并存的稳定形态。

配套的 config/createRollupConfig.js 负责从 CJS 源自动生成 ESM 产物,index.tests.ts 与 test/array-flatten/index.cjs 分别守住类型与行为边界。对于一个“只做一件事”的工具包而言,这份 changelog 记录的不只是版本号,而是一套从实现、类型到打包与测试的完整质量闭环——这也正是阅读 changelog 并结合源码回溯版本变更的价值所在。

  • 开发工具

【免费下载链接】just

A library of dependency-free JavaScript utilities that do just one thing.

项目地址:https://gitcode.com/gh_mirrors/jus/just
点击查看免费下载
上一篇:Dependency-Check终极指南:如何快速保护Web应用依赖安全
下一篇:昇腾原生盘古Ultra-MoE模型:与其他主流大模型的终极差异对比

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询