es-toolkit 函数式编程:使用 fp 版sampleSize在 pipe 流水线中随机抽样
【免费下载链接】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 的fp子包将工具函数改造为柯里化形式,使其能无缝接入pipe等函数组合工具。本篇指南以docs/ja/fp/reference/sampleSize.md为骨架,讲解 fp 版sampleSize的用法、参数与错误行为,并深入到src/fp/array/sampleSize.ts与src/array/sampleSize.ts的源码,揭示其底层基于 Floyd 算法的无重复抽样原理。读完本文,你将掌握如何在 es-toolkit 的函数式流水线中正确、高效地完成随机抽样,并理解它与普通版sampleSize的取舍。
一、fp 版sampleSize是什么
fp 版sampleSize的作用是创建一个从数组中返回随机值的函数,用于与函数式编程的pipe一起使用:
const result = pipe(array, sampleSize(size));从调用形式上可以直观看出它与普通版的关键区别:
- 普通版
sampleSize(array, size)一次性接收数组和数量两个参数; - fp 版
sampleSize(size)先接收数量size,返回一个"等待数组输入"的函数,再由pipe把上游数据喂给它。
import { pipe, sampleSize } from 'es-toolkit/fp'; const values = pipe([1, 2, 3, 4], sampleSize(2)); // values 的长度为 2,元素来自输入数组从源码看,这种柯里化只是薄薄一层包装。src/fp/array/sampleSize.ts的实现是:
export function sampleSize<T>(size: number): (array: readonly T[]) => T[] { return function (array: readonly T[]): T[] { return sampleSizeToolkit(array, size); }; }它直接委托给src/array/sampleSize.ts导出的普通版sampleSize(源码中通过import { sampleSize as sampleSizeToolkit } from '../../array/sampleSize.ts'引入),因此 fp 版与普通版在抽样行为、返回的新数组以及抛错规则上完全一致,区别仅在于参数的组织方式。
二、与普通版sampleSize如何选择
::: info
在不使用管道组合的普通代码中,推荐使用原版 es-toolkit 的sampleSize;只有当你需要用pipe串联变换时,才使用 fp 版。
:::
这一建议背后是清晰的工程逻辑:
- 普通代码里直接
sampleSize(array, size)更直观、参数更少; - 在
pipe(array, fn1, fn2, ...)的流水线里,每一步操作符都约定为"接收一个值,返回一个新函数",fp 版sampleSize(size)恰好满足这一约定,从而可以与filter、map、take等 fp 操作符自由组合。
普通版sampleSize的完整用法可以参考docs/reference/array/sampleSize.md,例如:
import { sampleSize } from 'es-toolkit/array'; const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const randomNumbers = sampleSize(numbers, 3); // 例如 [2, 7, 9],实际结果随机三、参数、返回值与错误行为
参数
size(number):要从管道传入的数组中返回的随机值个数。
返回值
(array: readonly T[]) => T[]:一个将readonly T[]映射为随机值数组的函数。也就是说,sampleSize(size)的结果仍然是一个函数,它等待pipe将上游数组传入后才真正执行抽样。
错误
当size大于管道传入数组的长度时,会抛出错误。
错误信息与普通版一致,来自src/array/sampleSize.ts:
if (size > array.length) { throw new Error('Size must be less than or equal to the length of array.'); }对应的测试src/array/sampleSize.spec.ts验证了这一行为:
it('throws an error if the size is greater than the array length', () => { expect(() => sampleSize([1, 2, 3], 4)).toThrowErrorMatchingInlineSnapshot( `[Error: Size must be less than or equal to the length of array.]` ); });四、边界情况:size 为 0 与 size 等于数组长度
虽然 fp 版文档只规定了"大于数组长度时抛错",但深入源码与测试可以发现两个值得留意的边界行为:
size为 0 时返回空数组。测试src/array/sampleSize.spec.ts明确断言sampleSize([1, 2, 3], 0)的结果为[]。size等于数组长度时,返回包含全部元素的新数组(相当于洗牌效果),但不会复用原数组引用。测试src/array/sampleSize.spec.ts同时断言result与array内容相等(toEqual)但引用不同(not.toBe),即返回的是新数组,原数组不被修改。
这两个特性同样适用于 fp 版,因为 fp 版只是委托调用同一个底层实现。
五、源码原理:Floyd 算法与无重复抽样
sampleSize的核心实现位于src/array/sampleSize.ts:
export function sampleSize<T>(array: readonly T[], size: number): T[] { if (size > array.length) { throw new Error('Size must be less than or equal to the length of array.'); } const result = new Array(size); const selected = new Set(); for (let step = array.length - size, resultIndex = 0; step < array.length; step++, resultIndex++) { let index = randomInt(0, step + 1); if (selected.has(index)) { index = step; } selected.add(index); result[resultIndex] = array[index]; } return result; }从中可以提炼出几个关键实现事实:
- 采用 Floyd 算法:源码注释明确标注其算法来源为 Robert Floyd 的经典抽样算法,可以在只遍历一次、只调用
size次随机数的前提下,从n个元素中无重复地抽取k个元素,时间复杂度为 O(k),空间上用一个Set记录已选下标。 - 保证"同一数组位置不重复":每次抽样用
selected.has(index)判断下标是否已被选中;若已选中,则回退为当前step位置(这正是 Floyd 算法的精髓),从而保证结果中不会出现重复的数组下标。 - 随机数来源:算法依赖
src/math/randomInt.ts生成[0, step + 1)区间内的随机整数,而randomInt内部又调用src/math/random.ts,基于Math.random()实现minimum + Math.random() * (maximum - minimum)的区间映射。因此抽样结果的随机性最终来源于引擎的Math.random()。
这也解释了 fp 版文档中"不重复同一数组位置"(同じ配列位置は繰り返しません)这一行为承诺的来源:它不是约定俗成,而是由 Floyd 算法的Set去重逻辑在源码层面保证的。
六、在 pipe 流水线中的组合实战
fp 版sampleSize的价值体现在长流水线中。下面是一个把"从用户列表中随机抽取 3 名、再映射出姓名"的完整组合示例:
import { pipe, sampleSize, map } from 'es-toolkit/fp'; interface User { id: number; name: string; } const users: User[] = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Carol' }, { id: 4, name: 'Dave' }, { id: 5, name: 'Eve' }, ]; // 从用户数组中随机抽取 3 人,再取出姓名 const winners = pipe(users, sampleSize(3), map(user => user.name)); // winners: 例如 ['Carol', 'Alice', 'Eve'],实际结果随机pipe的从左到右数据流定义在src/fp/pipe.ts,sampleSize等 fp 操作符(见src/fp/array/index.ts中的导出)都遵循"先收参数、再收数据"的柯里化签名,这正是它们能在pipe中直接作为操作符使用的原因。
需要留意的限制是:由于size在创建操作符时已经固定,sampleSize(size)要求调用方事先知道数组的大致规模;一旦传入的数组长度小于size,流水线会在该步骤抛出错误(错误信息与第五节所示一致)。因此在流水线中使用时,建议先通过filter等操作完成裁剪,再根据裁剪后的规模选择合理的size,或在外部对数组长度做前置校验。
七、小结
- fp 版
sampleSize(size)返回(array: readonly T[]) => T[]形式的柯里化函数,专为pipe流水线设计; - 普通代码应优先使用
sampleSize(array, size),二者底层实现完全相同; - 抽样基于 Floyd 算法,保证结果无重复下标、返回新数组且不修改原数组;
size > array.length时抛错;size === 0返回空数组;size === array.length时相当于一次洗牌;- 随机性来源是
Math.random()(经randomInt→random调用链),适用于抽奖、问卷抽样、游戏随机选人等场景。
【免费下载链接】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),仅供参考