Итераторы и генераторы

Обработка каждого элемента коллекции является весьма распространённой операцией. JavaScript предоставляет несколько способов перебора коллекции, от простого цикла for до map() и filter(). Итераторы и генераторы внедряют концепцию перебора непосредственно в ядро языка и обеспечивают механизм настройки поведения цикла for...of.

Итераторы

Объект является итератором (перебираемым) , если он умеет обращаться к элементам коллекции по одному за раз, при этом отслеживая своё текущее положение внутри этой последовательности. В JavaScript итератор - это объект, который предоставляет метод next(), возвращающий следующий элемент последовательности. Этот метод возвращает объект с двумя свойствами: done и value:
done
  • Принимает значение true если итератор достиг конца итерируемой последовательности. В этом случае свойство value может определять возвращаемое значение итератора.
  • Принимает значение false если итератор может генерировать следующее значение последовательности. Это эквивалентно не указанному done.
value
любое JavaScript значение, возвращаемое итератором. Может быть опущено, если done имеет значение true.

Перебираемые (или итерируемые) объекты – это концепция, которая позволяет использовать любой объект в цикле for..of.

Конечно же, сами массивы (Array) являются перебираемыми объектами. Но есть и много других встроенных перебираемых объектов: String, Map, Set, arguments. ...

Если объект не является массивом, но представляет собой коллекцию каких-то элементов, то удобно использовать цикл for..of для их перебора

Чтобы сделать объект итерируемым (и позволить for..of работать с ним), нужно добавить в объект метод с именем Symbol.iterator (специальный встроенный Symbol, созданный как раз для этого).

  1. Когда цикл for..of запускается, он вызывает этот метод один раз (или выдаёт ошибку, если метод не найден). Этот метод должен вернуть итератор – объект с методом next.
  2. Дальше for..of работает только с этим возвращённым объектом.
  3. Когда for..of хочет получить следующее значение, он вызывает метод next() этого объекта.
  4. Результат вызова next() должен иметь вид {done: Boolean, value: any}, где done=true означает, что итерация закончена, в противном случае value содержит очередное значение.

Пример

let range = {
  from: 1,
  to: 5
};

// 1. вызов for..of сначала вызывает эту функцию
range[Symbol.iterator] = function() {

  // ...она возвращает объект итератора:
  // 2. Далее, for..of работает только с этим итератором, запрашивая у него новые значения
  return {
    current: this.from,
    last: this.to,

    // 3. next() вызывается на каждой итерации цикла for..of
    next() {
      // 4. он должен вернуть значение в виде объекта {done:.., value :...}
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    }
  };
};

let s='';
for (let num of range) { s += num+'  '; }
  alert(s);    // 1  затем 2  3  4  5

Итерируемые объекты и псевдомассивы

Есть два официальных термина, которые очень похожи, но в то же время сильно различаются. Поэтому убедитесь, что вы как следует поняли их, чтобы избежать путаницы.

При использовании JavaScript в браузере или других окружениях мы можем встретить объекты, которые являются итерируемыми или псевдомассивами, или и тем, и другим.

Например, строки итерируемы (для них работает for..of) и являются псевдомассивами (они индексированы и есть length).

Но итерируемый объект может не быть псевдомассивом. И наоборот: псевдомассив может не быть итерируемым.

Генераторы

Генераторы -- новый вид функций в современном JavaScript. Они отличаются от обычных тем, что могут приостанавливать своё выполнение, возвращать промежуточный результат и далее возобновлять его позже, в произвольный момент времени.

Генераторы - это специальный тип функции, который работает как фабрика итераторов. Функция становится генератором, если содержит один или более yield операторов и использует синтаксическую конструкцию: function* (функция со звёздочкой). Её называют "функция-генератор" (generator function).

yield

Ключевое слово yield используется для остановки и возобновления функций-генераторов (function*)..

Синтаксис

 [rv] = yield [[выражение]];
выражение
Возвращаемое выражение. Если не указано, то возвращается значение undefined.
rv
Возвращает необязательное значение, которое передаётся в next() генератора, чтобы возобновить его выполнение.

Описание

Ключевое слово yield вызывает остановку функции-генератора и возвращает текущее значение выражения, указанного после ключевого слова yield. Его можно рассматривать как аналог ключевого слова return в функции-генераторе.

На самом деле ключевое слово yield возвращает объект с двумя параметрами, value и done. При этом, value является результатом вычисления выражения после yield, а done указывает, была ли завершена функция-генератор.

Во время остановки на операторе yield, выполнение кода в функции-генераторе не возобновится, пока не будет вызван метод next() возвращаемого функцией объекта-генератора. Это предоставляет непосредственный контроль за выполнением генератора и возвратом его значений.

Примеры

Следующий фрагмент кода содержит определение функции-генератора и вспомогательной функции:

function* foo(){
  var index = 0;
  while(index <= 2) // при достижении 2, done в yield станет true, а value undefined;
    yield index++;
}

// После того как тело функции-генератора определено, оно может использоваться для получения итератора:

var iterator = foo();
let a = iterator.next(); 
let s = '\n' + a.value + ', '+a.done;  // { value:0, done:false }

a = iterator.next();
s += '\n' + a.value + ', '+a.done;     // { value:1, done:false }

a = iterator.next();
s += '\n' + a.value + ', '+a.done;     // { value:2, done:false }

a = iterator.next();
s += '\n' + a.value + ', '+a.done;     // { value:undefined, done:true }

alert(s);

function*

function* (ключевое слово function со звёздочкой) определяет функцию-генератор.

Синтаксис

function* name([param[, param[, ... param]]]) { statements }
name
Имя функции.
param
Именованные аргументы функции (параметры). Функция-генератор может иметь 255 аргументов..
statements
Инструкции составляющие тело функции..
.

Описание

Генераторы являются функциями с возможностью выхода и последующего входа. Их контекст исполнения (значения переменных) сохраняется при последующих входах.

Когда вызывается функция-генератор, её тело исполняется не сразу; вместо этого возвращается объект-итератор. При вызове метода next() итератора тело функции-генератора исполняется до первого встреченного оператора yield, который определяет возвращаемое значение или делегирует дальнейшее выполнение другому генератору при помощи yield* anotherGenerator(). Метод next() возвращает объект со свойством value, содержащим отданное значение, и свойством done, которое указывает, что генератор уже отдал своё последнее значение. Вызов метода next() с аргументом прекращает выполнение функции-генератора, и заменяет инструкцию yield на которой было приостановлено выполнение на аргумент переданный в next().

Примеры

Простой пример

function* idMaker() {
  var index = 0;
  while (index < 3)
    yield index++;
}

var gen = idMaker();
alert( `
 ${ gen.next().value }
 ${ gen.next().value }
 ${ gen.next().value }
 ${ gen.next().value }
`);

Пример с yield*

function* anotherGenerator(i) {
  yield i + 1;
  yield i + 2;
  yield i + 3;
}

function* generator(i) {
  yield i;
  yield* anotherGenerator(i);
  yield i + 10;
}

var gen = generator(10);

alert( `
 ${ gen.next().value }
 ${ gen.next().value }
 ${ gen.next().value }
 ${ gen.next().value }
 ${ gen.next().value }
`);

Передача аргументов в генератор

function* logGenerator() {
  console.log(yield);
  console.log(yield);
  console.log(yield);
}

var gen = logGenerator();

// первый вызов next выполняется от начала функции
// и до первого оператора yield
gen.next();
gen.next('pretzel'); // pretzel
gen.next('california'); // california
gen.next('mayonnaise'); // mayonnaise

Инструкция return в генераторе

function* yieldAndReturn() {
  yield "Y";
  return "R";
  yield "unreachable";
}

var gen = yieldAndReturn();

let a = gen.next(); 
let s = '\n' + a.value + ', '+a.done;  // { value: "Y", done: false }

a = gen.next();
s += '\n' + a.value + ', '+a.done;     // { value: "R", done: true }

a = gen.next();
s += '\n' + a.value + ', '+a.done;     // { value: undefined, done: true }
alert(s);