Skip to content
生成循环数组的值
概述
cycleGenerator 利用 Generator 实现一个可按需跳转的循环数组遍历器。与固定步长的迭代器不同,调用方通过 .next(jump) 传入偏移量来控制索引移动,跳转步长由外部决定。
基本概念
Generator 函数(function*)执行后返回一个生成器对象。函数体内的 yield 表达式不仅能向外“产出”值,还能在恢复执行时从外部“接收”值——这种双向通信是整个实现的基础。
生成器执行到 yield arr[currentIndex] 时:
- 将
arr[currentIndex]作为本次next()返回对象的value属性。 - 暂停,等待下一次
next()调用。 - 下一次
next()调用时,传入的参数会成为该yield表达式的返回值。
工作原理
ts
function* cycleGenerator(
arr: number[],
startIndex: number
): Generator<number, void, number> {
const len = arr.length;
let currentIndex = ((startIndex % len) + len) % len;
while (true) {
const jump = yield arr[currentIndex];
if (typeof jump === 'number') {
currentIndex = (((currentIndex + jump) % len) + len) % len;
}
}
}执行流程:
- 创建生成器对象时,函数不立即执行。
- 首次调用
.next(),函数运行至第一个yield,计算并产出arr[currentIndex],然后暂停。此时jump尚未被赋值。 - 第二次及之后的
.next(value)调用从yield处恢复,value成为jump的值。若value为数字,则按jump步长更新currentIndex;否则索引保持不变。 - 无论索引是否更新,控制流都会回到
while顶部,执行下一个yield,产出对应的元素。 while (true)使得生成器永远不会自行结束,除非调用方主动终止迭代。
基本用法
先创建生成器实例,首次调用 .next() 启动并获取起始元素,后续调用 .next(jump) 传入希望前进(或后退)的步数。
ts
const gen = cycleGenerator([10, 20, 30, 40], 1);
console.log(gen.next()); // { value: 20, done: false }
console.log(gen.next(1)); // { value: 30, done: false }
console.log(gen.next(2)); // { value: 10, done: false }
console.log(gen.next(-1)); // { value: 40, done: false }
console.log(gen.next()); // { value: 40, done: false } (jump 为 undefined,索引不变)如果希望按固定步长顺序遍历数组,可以在每次调用时传入步长 1,并在合适的时机主动退出循环。
ts
function getCircularValues(arr: number[], start: number, count: number): number[] {
const gen = cycleGenerator(arr, start);
const result: number[] = [];
// 启动生成器并获取首个值
result.push(gen.next().value);
// 后续 count-1 次每次前进 1
for (let i = 1; i < count; i++) {
result.push(gen.next(1).value);
}
return result;
}
console.log(getCircularValues([1, 2, 3], 2, 5)); // [3, 1, 2, 3, 1]注意 for...of 循环无法向生成器传递参数,因此不能直接用于控制跳转。需要固定步长的无限遍历时,可采用手动迭代并自行终止。
示例
示例 1:基本跳转
ts
const g = cycleGenerator([5, 6, 7], 0);
console.log(g.next().value); // 5 (起始元素)
console.log(g.next(1).value); // 6 (前进 1)
console.log(g.next(2).value); // 5 (从索引 1 前进 2,即 (1+2)%3=0)
console.log(g.next(-1).value); // 7 (后退 1,即 (0-1+3)%3=2)示例 2:忽略非数字参数
ts
const g = cycleGenerator([1, 2, 3], 1);
g.next(); // 产出 2
g.next(undefined); // jump 为 undefined,索引不变
console.log(g.next().value); // 2 (索引仍然指向 1)
g.next('3' as any);// jump 非数字,被忽略
console.log(g.next().value); // 2示例 3:构建有限循环序列
利用生成器的跳转能力,可按任意步长抽取循环片段。
ts
function take<T>(gen: Generator<T, void, number>, steps: number, stepSize: number): T[] {
const results: T[] = [];
// 取第一个元素(启动生成器)
results.push(gen.next().value);
for (let i = 1; i < steps; i++) {
results.push(gen.next(stepSize).value);
}
return results;
}
const gen = cycleGenerator(['a', 'b', 'c', 'd'], 2);
console.log(take(gen, 6, 2)); // ['c', 'a', 'c', 'a', 'c', 'a']注意点
- 负数取模处理:
((x % n) + n) % n是 JavaScript 中确保结果落在[0, n-1]区间的惯用写法。因为%运算符对负数的行为是返回余数而非数学模(例如-1 % 5为-1),该模式在索引更新时可以安全处理负的jump值。 - 无限循环:生成器内部使用了
while (true),永远不会自行结束。如果直接通过for...of或展开运算符(...)消费,将导致死循环。使用时务必在合适的时机手动终止迭代。 - 首个
.next()调用:初次调用.next()时传入的参数会被忽略,因为此时还没有yield表达式来接收值。启动生成器并获取第一个元素只需调用无参的.next()。 - 性能:索引计算中使用了两次取模,在绝大多数场景下开销可忽略。如果处于极端性能敏感路径,可考虑对
startIndex做一次预处理。
应用
- 实现轮询调度算法,根据权重或外部事件跳转到不同节点。
- 游戏开发中的环形菜单或选项循环,允许用户通过方向键(正/负偏移)改变选中项。
- 数据流采样,以非固定步长从环形缓冲区中抽取元素。
