☰
just-compose 函数组合实战:零依赖 compose 的安装、求值原理与类型安全用法
2026/10/15 3:32:01 网站建设 项目流程
  • 开发工具

【免费下载链接】just

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

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

本文以just开源库中的just-compose(源码位于 packages/function-compose)为讲解主体,带你从安装引入、基础用法一路深入到 CJS 源码级求值原理、TypeScript 类型重载设计以及双模块格式发布机制,读完后你可以准确理解"从右向左"的函数组合语义,并能在自己的项目里安全、高效地使用这一零依赖工具。

一、什么是 just-compose

just-compose是just库中负责函数组合(function composition)的独立子包。整个just库的定位是"零依赖(zero-dependency)、每个模块只做一件事"的 JavaScript 工具集合,而just-compose只承担唯一职责:返回一个由两个或更多函数组合而成的新函数。

它的核心语义非常贴近数学中的复合函数记号f ∘ g:组合后的函数先执行右侧函数,再把结果依次向左传递,最终得到整体结果。由于每个子包都被发布为独立的 npm 模块,你完全不需要为引入一个组合函数而背负整个工具库的体积。

二、安装与引入

与just库的其他子包一样,just-compose以独立 npm 包发布,支持 npm 与 yarn 两种安装方式:

npm install just-compose
yarn add just-compose

安装完成后,在支持 ESM 的环境中以默认导出方式引入:

import compose from 'just-compose';

在 CommonJS 环境下(如传统 Node.js 模块或老项目)同样可用:

const compose = require('just-compose');

包自身不依赖任何第三方运行时库,引入后可直接使用。如果你在仓库根目录使用 monorepo 开发(本仓库通过 lerna.json 管理),也可以直接引用 packages/function-compose 下的本地构建产物进行验证。

三、快速上手:组合两个以上函数

这是官方 README 给出的标准示例,也是理解just-compose语义的最短路径:

import compose from 'just-compose'; const sqRootBiggest = compose(Math.max, Math.sqrt, Math.trunc); sqRootBiggest(10, 5); // 3 sqRootBiggest(7, 0, 16); // 4

compose(Math.max, Math.sqrt, Math.trunc)返回一个新函数,求值时从右向左依次执行:

  1. 先调用最右侧的Math.trunc?——不,注意顺序:组合的约定是右侧函数先作用于原始入参之前的数据流,而在本例中,compose的第一参数Math.max才是先执行的函数(详见下一节)。为了准确理解,我们直接以第一个参数为起点:
    • 第 1 步:调用Math.max,接收全部原始参数。sqRootBiggest(10, 5)时得到Math.max(10, 5) === 10;sqRootBiggest(7, 0, 16)时得到Math.max(7, 0, 16) === 16;
    • 第 2 步:把上一步结果交给Math.sqrt,即Math.sqrt(10) ≈ 3.1623、Math.sqrt(16) === 4;
    • 第 3 步:再交给Math.trunc截断小数,得到3与4。

因此compose(fn1, fn2, fn3)(x)的求值顺序是fn1(x) → fn2(结果) → fn3(结果)。写成数学记号就是fn3 ∘ fn2 ∘ fn1——参数排列顺序与执行顺序恰好相反,这正是函数式编程中"组合(compose)"的通行约定,与pipe流水线方向正好相反(见第七节对比)。

四、源码级原理剖析

just-compose的实现极其精简,完整逻辑只有 index.cjs 中约 25 行代码:

function compose(fn1, fn2 /*, fn3, etc */) { if (!arguments.length) { throw new Error( 'expected at least one (and probably more) function arguments' ); } var fns = arguments; return function() { var result = fns[0].apply(this, arguments); var len = fns.length; for (var i = 1; i < len; i++) { result = fns[i].call(this, result); } return result; }; }

逐段拆解,可以提炼出三个关键设计:

1. 空参数校验。组合至少需要一个函数。若调用compose()时不传任何参数,立即抛出Error: expected at least one (and probably more) function arguments,避免生成无意义甚至运行时崩溃的"空组合"。这一行为有专门的测试用例锁定(见第六节)。

2. 首函数接收全部参数,后续函数只接收单一结果。返回的新函数被调用时,fns[0].apply(this, arguments)把调用方传入的所有参数原样透传给第一个函数;随后在for循环中,fns[i].call(this, result)把前一步的返回值作为唯一参数依次交给后续每个函数。这正是"第一参数是入口、其余参数是管道"的设计——例如compose(Math.max, ...)可以接受(10, 5)这种多参数调用,而中间的转换函数只需要处理一个值。

3.this上下文全程透传。无论是首函数的apply(this, arguments)还是后续函数的call(this, result),都显式绑定了组合函数被调用时的this。这意味着组合内部可以安全调用依赖this的方法函数,不会被意外改绑到undefined或全局对象。

从循环结构还可以推断出两个行为特征:

  • 结果重赋值语义:result变量被逐步覆盖,前一个函数的返回值成为下一个函数的输入,形成严格串行的数据管道;
  • 参数数量无上限:fns直接取自arguments类数组对象,循环遍历全部传入函数,因此可以组合任意多个函数,不局限于示例中的三个。

在 ESM 侧,仓库通过 rollup.config.js 调用通用构建脚本 config/createRollupConfig.js,把同一份 CJS 实现转译为 index.mjs,两份产物逻辑完全一致:

export {functionCompose as default};

五、类型系统的安全保证

just-compose为 TypeScript 用户提供了非常完整的类型声明,见 index.d.ts。其设计思路是为 1 至 6 个函数分别生成精确重载,每一层都严格约束"前一个函数的输出类型必须是后一个函数的输入类型":

declare function compose<A extends unknown[], B>(f0: (...a: A) => B): (...a: A) => B; declare function compose<A extends unknown[], B, C>( f0: (...a: A) => B, f1: (b: B) => C ): (...a: A) => C; declare function compose<A extends unknown[], B, C, D>( f0: (...a: A) => B, f1: (b: B) => C, f2: (c: C) => D ): (...a: A) => D; // ... 依次递增,直到 f5

以两个函数为例:compose(fa, fb)要求fa的返回类型B恰好能作为fb的入参,组合后的函数签名是(...a: A) => C,即入参类型继承自第一个函数、出参类型来自最后一个函数。

对于超过 6 个函数的情况,声明文件提供了一个变通重载:第 7 个及以后的函数必须是(g: G) => G的同型转换(...fs: ((g: G) => G)[]),并最终返回(...a: A) => G;另有一个更宽松的兜底重载返回(...a: A) => any。

这一套类型约束并非纸面声明,而是由类型测试文件 index.test.ts 在仓库根目录执行npm run test-types(即tsc,见根目录 package.json 的 scripts)时强制验证。测试中既有大量"合法组合"的正例,也有用@ts-expect-error标注的负例,例如:

// OK:类型链正确,组合结果类型精确推导 let ma: (s: string, n: number) => [] = compose(fa, fb); let mb: (s: string, n: number) => 0 = compose(fa, fb, fc); // Not OK:类型不匹配会被编译器拒绝 // @ts-expect-error compose(fa, fc); // @ts-expect-error compose(fa, fb, 0);

对类型用户来说,这意味着一旦中间函数类型不衔接,编译期就会立即报错,组合错误无法静默流入运行时。

六、测试驱动验证:三个核心行为

just-compose的运行时行为由 tape 测试用例锁定,见 test/function-compose/index.cjs(测试辅助工具位于 test/util/test.js)。三个用例分别覆盖:

1. 正常组合:compose(String, str => str.split(2))作用于52423得到['5', '4', '3'];README 中的compose(Math.max, Math.sqrt, Math.round)验证(10, 5) → 3、(7, 0, 16) → 4。

2. 单函数退化行为:compose(Math.sqrt)(19)与直接调用Math.sqrt(19)结果一致;compose(Math.max)(4, 7, 99)等价于Math.max(4, 7, 99)。这印证了源码中"循环从索引 1 开始"的设计——只有一个函数时循环体不执行,原样返回首函数的多参数调用结果。

3. 零函数报错:compose()不传参数时抛出Error,与源码中的空参数校验一一对应。

根目录npm test会依次执行 lint、全部test/**/index.cjs的 tape 用例(经 tap-spec 格式化)以及tsc类型测试,运行时与类型层都被持续守护。

七、compose 与 pipe:方向相反的孪生兄弟

just库同时提供了 function-pipe(源码见 index.js),二者互为镜像,极易混淆:

  • compose(本包):参数是compose(fn1, fn2, ...),从左到右书写、从右到左执行,即先fn1后fn2,数学上等价于fn2 ∘ fn1;
  • pipe:写法是pipe(value, fn1, fn2, ...),直接对初始值从左到右流水执行,例如pipe(5, a => a * 2, b => b + 1) // 11。

选择依据很简单:如果你已经持有"入口函数 + 一串后续变换",希望生成一个可复用的组合函数,用compose;如果你只是要对一个具体值做链式变换,用pipe更直白。两者在just中均为零依赖实现,可放心混用。

八、版本演进与发布形态

CHANGELOG.md 记录了该包从 2.1.1 到 2.3.0 的演进脉络:

  • 2.1.1:补齐并修正 TypeScript 类型声明;
  • 2.2.0:更新package.json以修复模块解析问题(对应仓库 issue #467、#483);
  • 2.2.1:调整exports字段顺序,将默认导出放在最后(issue #488);
  • 2.3.0:Node 模块文件由.js重命名为.cjs,明确 CommonJS 身份。

当前版本(2.3.0)的发布形态在 package.json 中一目了然:

{ "name": "just-compose", "version": "2.3.0", "description": "return a function composed of 2 or more functions", "main": "index.cjs", "types": "index.d.ts", "module": "index.mjs", "exports": { ".": { "require": "./index.cjs", "types": "./index.d.ts", "default": "./index.mjs" }, "./package.json": "./package.json" } }

exports字段实现了双格式分发:CommonJS 环境命中require→index.cjs,现代打包器/浏览器 ESM 环境命中default→index.mjs,TypeScript 命中types→index.d.ts。无论你的项目运行在哪种模块体系下,都能拿到与实现完全同步的产物。

九、最佳实践与注意事项

结合源码语义与类型声明,使用just-compose时有几点值得注意:

  1. 永远至少传一个函数:空组合会立即抛错。若场景是"可能没有变换",请在调用前自行判断,而不是依赖组合函数去兜底。
  2. 只有第一个函数能接收多参数:后续函数一律只接收前一步的单个返回值。需要"多参数 → 单值"的汇聚逻辑,请写在第一个函数里(如示例中的Math.max)。
  3. 利用类型链在编译期纠错:在 TypeScript 项目中,类型不衔接的组合会在tsc阶段被拒绝,无需等到运行时。
  4. 关注this透传:若组合中的函数是依赖this的方法,调用组合结果时请保持期望的this上下文,源码会将其原样透传。
  5. 与pipe区分使用:需要"组合成可复用函数"用compose,需要"对值做一次性流水"用 just-pipe,两者数据流向完全相反。

总而言之,just-compose用 25 行零依赖代码交付了标准的"从右向左"函数组合能力,并配套了精确到参数级别的 TypeScript 重载、双格式发布产物与完整的运行时/类型测试。无论是写中间件编排、数据转换管道,还是构建可复用的逻辑切片,它都是一个"只做一件事、做得干净利落"的可靠选择。

  • 开发工具

【免费下载链接】just

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

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

相关推荐

上一篇:idea-claude-code-gui 前端性能优化:拆分合并的 Hook 计算(Split Combined Hook Computations)
下一篇:RIOT OS 中 nRF52 板卡固件烧录完全指南:J-Link 与 OpenOCD 双路径配置详解

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

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

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

立即咨询