Как я могу узнать, завершается ли итератор javascript раньше?
Допустим, у меня есть генератор:
function* source() {
yield "hello"; yield "world";
}
Я создаю итерацию, выполняю итерацию с циклом for, а затем вырываюсь из цикла, пока итератор полностью не завершит (возврат завершен).
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", ни "bar" не зарегистрированы.
3 ответа
Я заметил, что машинопись определяет 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
не называется.
Return
мотыга
Итак, я немного взломал, чтобы позволить захватить другую итерацию - поэтому мне не нужно повторно реализовывать итератор.
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]
, Случай асинхронного итератора представляет особый интерес для меня, особенно когда необходимо правильно утилизировать некоторый ресурс.
Есть гораздо более простой способ сделать это: использовать блок finally.
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 в итераторы и генераторы документации.
В любом цикле должно быть установлено условие для преждевременного выхода из цикла, как в уже приведенном решении. Но что, если вы хотите деструктурировать итерируемый объект, чтобы создать массив значений? В этом случае вы захотите ограничить количество итераций, по сути установив максимальную длину итерации.
Для этого я написал функцию под названием limitIterable
, который принимает в качестве аргументов итерацию, предел итераций и необязательную функцию обратного вызова, которая должна выполняться в случае, если итератор завершает работу раньше. Возвращаемое значение - это объект Generator (который одновременно является итератором и итератором), созданный с использованием выражения функции Immediately Invoked (Generator).
Когда генератор выполняется в цикле for..of, с деструктуризацией или путем вызова метода next(), он проверяет, не iterator.next().done === true
или iterationCount < iterationLimit
. В случае бесконечной итерации, такой как последовательность Фибоначчи, последняя всегда будет вызывать выход из цикла while. Однако обратите внимание, что можно также установить iterationLimit, превышающий длину некоторой конечной итерации, и все по-прежнему будет работать.
В любом случае после выхода из цикла while будет проверяться самый последний результат, чтобы узнать, завершился ли итератор. Если это так, будет использоваться исходное возвращаемое значение итерации. В противном случае выполняется дополнительная функция обратного вызова, которая используется в качестве возвращаемого значения.
Обратите внимание, что этот код также позволяет пользователю передавать значения в next()
, который, в свою очередь, будет передан исходной итерации (см. пример с использованием последовательности Фибоначчи MDN внутри прилагаемого фрагмента кода). Это также позволяет совершать дополнительные звонки наnext()
за пределами установленного iterationLimit в функции обратного вызова.
Запустите фрагмент кода, чтобы увидеть результаты нескольких возможных вариантов использования! Вот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);