我有一个发电机:
function* source() {
yield "hello"; yield "world";
}
我使用循环创建迭代,迭代,然后在迭代器完全完成之前脱离循环(返回完成)。
function run() {
for (let item of source()) {
console.log(item);
break;
}
}
问题:我如何从迭代方面找出迭代器早期终止?
如果您直接在发电机本身中进行此操作,似乎没有任何反馈:
function* source2() {
try {
let result = yield "hello";
console.log("foo");
} catch (err) {
console.log("bar");
}
}
...既没有记录" foo"one_answers" bar"。
编辑:请参阅更新的接受答案。我会像做/确实工作一样保持这一点,并且在我能够破解解决方案时感到非常高兴。但是,正如您在接受的答案中所看到的那样,最终的解决方案是如此简单,现在已经确定了。
我注意到Typescript将Iterator
(lib.es2015)定义为:
interface Iterator<T> {
next(value?: any): IteratorResult<T>;
return?(value?: any): IteratorResult<T>;
throw?(e?: any): IteratorResult<T>;
}
我拦截了这些方法和已记录的调用,并且确实看来,如果迭代器提早终止 - 至少是通过for-loop
终止的,则调用return
方法。,如果消费者引发错误,也将调用。如果允许循环完全迭代迭代器 return
是 not 调用的。
Return
hack
所以,我做了一些骇客,以允许捕获另一个 iToser-所以我不必重新实现迭代器。
function terminated(iterable, cb) {
return {
[Symbol.iterator]() {
const it = iterable[Symbol.iterator]();
it.return = function (value) {
cb(value);
return { done: true, value: undefined };
}
return it;
}
}
}
function* source() {
yield "hello"; yield "world";
}
function source2(){
return terminated(source(), () => { console.log("foo") });
}
for (let item of source2()) {
console.log(item);
break;
}
它有效!
你好
foo
删除break
,然后得到:
你好
世界
在每个yield
之后检查在键入此答案时,我意识到一个更好的问题/解决方案是在原始生成器方法。
中找到我看到的唯一方法是将信息传递回原始峰值,就是使用next(value)
。因此,如果我们选择了一些独特的值(例如Symbol.for("terminated")
)来发出终止的信号,并且我们将上述返回式挑战以调用it.next(Symbol.for("terminated"))
:
function* source() {
let terminated = yield "hello";
if (terminated == Symbol.for("terminated")) {
console.log("FooBar!");
return;
}
yield "world";
}
function terminator(iterable) {
return {
[Symbol.iterator]() {
const it = iterable[Symbol.iterator]();
const $return = it.return;
it.return = function (value) {
it.next(Symbol.for("terminated"));
return $return.call(it)
}
return it;
}
}
}
for (let item of terminator(source())) {
console.log(item);
break;
}
成功!
你好
Foobar!
链接级联Return
如果您链接一些额外的转换迭代器,则return
通过它们呼叫级联:
function* chain(source) {
for (let item of source) { yield item; }
}
for (let item of chain(chain(terminator(source())))) {
console.log(item);
break
}
你好
Foobar!
软件包
我将上述解决方案包裹为包装。它支持[Symbol.iterator]
和[Symbol.asyncIterator]
。我特别感兴趣的异步迭代案例,尤其是当需要正确处理某些资源时。
有一种更简单的方法:使用最后一个块。
function *source() {
let i;
try {
for(i = 0; i < 5; i++)
yield i;
}
finally {
if(i !== 5)
console.log(' terminated early');
}
}
console.log('First:')
for(const val of source()) {
console.log(` ${val}`);
}
console.log('Second:')
for(const val of source()) {
console.log(` ${val}`);
if(val > 2)
break;
}
...屈服:
First:
0
1
2
3
4
Second:
0
1
2
3
terminated early
我遇到了类似的需求,以弄清迭代器何时终止。公认的答案确实很聪明,可能是一般解决问题的最佳方法,但我认为该解决方案也可能有助于其他用途。
说,例如,您具有无限的峰值,例如MDN的迭代器和发电机中描述的斐波那契序列 docs。
在任何形式的循环中,都需要有一个条件设置,就像已经给出的解决方案一样,需要提早脱离循环。但是,如果您想破坏峰值以创建一系列价值观怎么办?在这种情况下,您需要限制迭代次数,从本质上设置了最大长度。
为此,我编写了一个名为 limitIterable
的函数,该功能以参数为迭代,一个迭代限制和一个可选回调函数,以便在迭代器及早终止的情况下执行。返回值是使用立即调用(Generator)函数表达式创建的生成器对象(既是迭代器又是迭代)。
当执行发电机时,无论是在循环中,带有破坏性或调用next()方法时,它将检查以查看iterator.next().done === true
还是iterationCount < iterationLimit
。如果像斐波那契序列这样的无限峰值,后者总是会导致循环退出。但是,请注意,一个人还可以设置一个比某些有限峰值的长度大的迭代限制,并且一切仍然可以起作用。
在任何一种情况下,一旦暂停循环退出,将检查最新结果,以查看是否完成了迭代器。如果是这样,将使用原始峰值的返回值。如果不是,则执行可选回调函数并用作返回值。
请注意,此代码还允许用户将值传递给next()
,然后将其传递给原始峰值(请参见使用MDN的fibonacci序列,在附加的代码片段中使用MDN的fibonacci序列)。它还允许在回调函数内的集合iTerationLimit之外向next()
进行其他调用。
运行代码片段以查看一些可能的用例的结果!这是limitIterable
功能代码:
function limitIterable(iterable, iterationLimit, callback = (itCount, result, it) => undefined) {
// callback will be executed if iterator terminates early
if (!(Symbol.iterator in Object(iterable))) {
throw new Error('First argument must be iterable');
}
if (iterationLimit < 1 || !Number.isInteger(iterationLimit)) {
throw new Error('Second argument must be an integer greater than or equal to 1');
}
if (!(callback instanceof Function)) {
throw new Error('Third argument must be a function');
}
return (function* () {
const iterator = iterable[Symbol.iterator]();
// value passed to the first invocation of next() is always ignored, so no need to pass argument to next() outside of while loop
let result = iterator.next();
let iterationCount = 0;
while (!result.done && iterationCount < iterationLimit) {
const nextArg = yield result.value;
result = iterator.next(nextArg);
iterationCount++;
}
if (result.done) {
// iterator has been fully consumed, so result.value will be the iterator's return value (the value present alongside done: true)
return result.value;
} else {
// iteration was terminated before completion (note that iterator will still accept calls to next() inside the callback function)
return callback(iterationCount, result, iterator);
}
})();
}
function limitIterable(iterable, iterationLimit, callback = (itCount, result, it) => undefined) {
// callback will be executed if iterator terminates early
if (!(Symbol.iterator in Object(iterable))) {
throw new Error('First argument must be iterable');
}
if (iterationLimit < 1 || !Number.isInteger(iterationLimit)) {
throw new Error('Second argument must be an integer greater than or equal to 1');
}
if (!(callback instanceof Function)) {
throw new Error('Third argument must be a function');
}
return (function* () {
const iterator = iterable[Symbol.iterator]();
// value passed to the first invocation of next() is always ignored, so no need to pass argument to next() outside of while loop
let result = iterator.next();
let iterationCount = 0;
while (!result.done && iterationCount < iterationLimit) {
const nextArg = yield result.value;
result = iterator.next(nextArg);
iterationCount++;
}
if (result.done) {
// iterator has been fully consumed, so result.value will be the iterator's return value (the value present alongside done: true)
return result.value;
} else {
// iteration was terminated before completion (note that iterator will still accept calls to next() inside the callback function)
return callback(iterationCount, result, iterator);
}
})();
}
// EXAMPLE USAGE //
// fibonacci function from:
//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators#Advanced_generators
function* fibonacci() {
let fn1 = 0;
let fn2 = 1;
while (true) {
let current = fn1;
fn1 = fn2;
fn2 = current + fn1;
let reset = yield current;
if (reset) {
fn1 = 0;
fn2 = 1;
}
}
}
console.log('String iterable with 26 characters terminated early after 10 iterations, destructured into an array. Callback reached.');
const itString = limitIterable('abcdefghijklmnopqrstuvwxyz', 10, () => console.log('callback: string terminated early'));
console.log([...itString]);
console.log('Array iterable with length 3 terminates before limit of 4 is reached. Callback not reached.');
const itArray = limitIterable([1,2,3], 4, () => console.log('callback: array terminated early?'));
for (const val of itArray) {
console.log(val);
}
const fib = fibonacci();
const fibLimited = limitIterable(fibonacci(), 9, (itCount) => console.warn(`Iteration terminated early at fibLimited. ${itCount} iterations completed.`));
console.log('Fibonacci sequences are equivalent up to 9 iterations, as shown in MDN docs linked above.');
console.log('Limited fibonacci: 11 calls to next() but limited to 9 iterations; reset on 8th call')
console.log(fibLimited.next().value);
console.log(fibLimited.next().value);
console.log(fibLimited.next().value);
console.log(fibLimited.next().value);
console.log(fibLimited.next().value);
console.log(fibLimited.next().value);
console.log(fibLimited.next().value);
console.log(fibLimited.next(true).value);
console.log(fibLimited.next().value);
console.log(fibLimited.next().value);
console.log(fibLimited.next().value);
console.log('Original (infinite) fibonacci: 11 calls to next(); reset on 8th call')
console.log(fib.next().value);
console.log(fib.next().value);
console.log(fib.next().value);
console.log(fib.next().value);
console.log(fib.next().value);
console.log(fib.next().value);
console.log(fib.next().value);
console.log(fib.next(true).value);
console.log(fib.next().value);
console.log(fib.next().value);
console.log(fib.next().value);