Книга: JavaScript. Подробное руководство, 6-е издание

11.4.3. Генераторы

11.4.3. Генераторы

Генераторы - это особенность JavaScript 1.7 (заимствованная из языка Python), основанная на использовании нового ключевого слова yield. Программный код, использующий данную особенность, должен явно указать номер версии 1.7, как описывалось в разделе 11.2. Ключевое слово yield используется в функциях и действует аналогично инструкции return, возвращая значение из функции. Разница между yield и return состоит в том, что функция, возвращающая значение с помощью ключевого слова yield, сохраняет информацию о своем состоянии, благодаря чему ее выполнение может быть возобновлено. Такая способность к возобновлению выполнения делает yield замечательным инструментом для создания итераторов. Генераторы - очень мощная особенность языка, но понять принцип их действия совсем не просто. Для начала познакомимся с некоторыми определениями.

Любая функция, использующая ключевое слово yield (даже если инструкция yield никогда не будет выполняться), является функцией-генератором. Функции-генераторы возвращают значения с помощью yield. Они могут использовать инструкцию return без значения, чтобы завершиться до того, как будет достигнут конец тела функции, но они не могут использовать return со значением. За исключением использования ключевого слова yield и ограничений на использование инструкции return, функции-генераторы ничем не отличаются от обычных функций: они объявляются с помощью ключевого слова function, оператор typeof возвращает для них строку «function» и как обычные функции они наследуют свойства и методы от Function.prototype. Однако поведение функции-генератора совершенно отличается от поведения обычной функции: при вызове, вместо того чтобы выполнить свое тело, функция-генератор возвращает объект генератора.

Генератор - это объект, представляющий текущее состояние функции-генератора. Он определяет метод next(), вызов которого возобновляет выполнение функции-генератора и позволяет продолжить ее выполнение, пока не будет встречена следующая инструкция yield. Когда это происходит, значение в инструкции yield в функции-генераторе становится возвращаемым значением метода next() генератора. Если функция-генератор завершает свою работу вызовом инструкции return или в результате достижения конца своего тела, метод next() генератора возбуждает исключение StopIteration.

Тот факт, что генераторы имеют метод next(), который может возбуждать исключение StopIteration, явственно говорит о том, что они являются итераторами.[22] В действительности они являются итерируемыми итераторами, т. е. они могут использоваться в циклах for/in. Следующий пример демонстрирует, насколько просто создавать функции-генераторы и выполнять итерации по значениям, которые они возвращают с помощью инструкции yield:

// Определение функции-генератора для выполнения итераций
// по целым числам в определенном диапазоне
function range(min, max) {
  for(let і = Math.ceil(min); і <= max; i++) yield i;
}
// Вызвать функцию-генератор, чтобы получить генератор, и выполнить итерации по нему,
fог(let n in range(3.8)) console.log(n); // Выведет числа от 3 до 8.

Функции-генераторы могут никогда не завершаться. Каноническим примером использования генераторов является воспроизведение последовательности чисел Фибоначчи:

// Функция-генератор, которая воспроизводит последовательность чисел Фибоначчи
function fibonacci() {
  let х = 0, у = 1;
  while(true) {
    yield у;
    [х,у] = [у,х+у];
  }
}
// Вызвать функцию-генератор, чтобы получить генератор,
f = fibonacci();
// Использовать генератор как итератор, вывести первые 10 чисел Фибоначчи,
for(let і = 0; і < 10; і++) console.log(f.next());

Обратите внимание, что функция-генератор fibonacci() никогда не завершится. По этой причине создаваемый ею генератор никогда не возбудит исключение StopIteration. Поэтому, вместо того чтобы использовать его как итерируемый объект в цикле for/in и попасть в бесконечный цикл, мы используем его как итератор и явно вызываем его метод next() десять раз. После того как фрагмент выше будет выполнен, генератор f по-прежнему будет хранить информацию о состоянии функции-генератора. Если в программе не требуется далее хранить эту информацию, ее можно освободить вызовом метода close() объекта f:

f.close();

При вызове метода close() генератора производится завершение связанной с ним функции-генератора, как если бы она выполнила инструкцию return в той точке, где ее выполнение было приостановлено. Если это произошло в блоке try, автоматически будет выполнен блок finally перед тем, как close() вернет управление. Метод close() никогда не возвращает значение, но если блок finally возбудит исключение, оно продолжит свое распространение из вызова close().

Генераторы часто бывает удобно использовать для последовательной обработки данных - элементов списка, строк текста, лексем в лексическом анализаторе и т.д. Генераторы можно объединять в цепочки, подобно конвейеру команд в Unix. Самое интересное в этом подходе заключается в том, что он следует принципу отложенных вычислений: значения «извлекаются» из генератора (или из конвейера генераторов) по мере необходимости, а не все сразу. Эту особенность демонстрирует пример 11.1.

Пример 11.1. Конвейер генераторов

// Генератор, возвращающий строки текста по одной.
// Обратите внимание, что здесь не используется метод s.split(), потому что
// он обрабатывает текст целиком, создает массив, тогда как нам необходимо
// реализовать отложенную обработку,
function eachline(s) {
  let р;
  while((p = s.indexOf( n')) != -1) {
    yield s. substrings, p);
    s = s.substring(p+1);
  }
  if (s.length > 0) yield s;
}
// Функция-генератор, возвращающая f(x) для каждого элемента х итерируемого объекта і
function map(і. f) {
  fоr(let x in i) yield f(x);
}
// Функция-генератор, возвращающая элементы і, для которых f(x) возвращает true
function select(i, f) {
  for(let x in i) {
    if (f(x)) yield x;
  }
}
// Обрабатываемый текст
let text = " «comment n n hello nworldn quit n unreached n";
// Сконструировать конвейер генераторов для обработки текста.
// Сначала разбить текст на строки
let lines = eachline(text);
// Затем удалить начальные и конечные пробелы в каждой строке
let trimmed = map(lines, function(line) { return line.trim(); });
// Наконец, игнорировать пустые строки и комментарии
let nonblank = select(trimmed, function(line) {
  return line.length > 0 && 1ine[0] !=
});
// Теперь извлечь отфильтрованные строки из конвейера и обработать их,
// остановиться, если встретится строка “quit",
for (let line in nonblank) {
  if (line === "quit") break; console.log(line);
}

Обычно инициализация генераторов выполняется при их создании: аргументы, передаваемые функции-генератору, являются единственными значениями, которые принимают генераторы. Однако имеется возможность передать дополнительные данные уже работающему генератору. Каждый генератор имеет метод send(), который перезапускает генератор подобно методу next(). Разница лишь в том, что методу send() можно передать значение, которое станет значением, возвращаемым выражением yield в функции-генераторе. (В большинстве генераторов, которые не принимают дополнительных входных данных, ключевое слово yield выглядит как инструкция. Однако в действительности yield - это выражение, возвращающее значение.) Кроме методов next() и send() существует еще один способ перезапустить генератор - метод throw(). Если вызвать этот метод, выражение yield возбудит аргумент метода throw() как исключение, как показано в следующем примере:

// Функция-генератор, ведущая счет от заданного начального значения.
// Метод send() позволяет увеличить счетчик на определенное значение.
// Вызов throw("reset") сбрасывает счетчик в начальное значение.
// Это всего лишь пример - здесь метод throw() используется не самым лучшим образом,
function counter(initial) {
  let nextValue = initial; // Сохранить начальное значение
  while(true) {
    try {
      let increment = yield nextValue; // Вернуть значение и получить приращение
      if (increment)                   // Если передано приращение...
        nextValue += increment;        // ...использовать его.
      else nextValue++;                 // Иначе увеличить на 1
    }
    catch (e) {                        // Если был вызван метод
      if (e==="reset")  // throw() генератора
      nextValue = initial; else throw e;
    }
  }
}
let c = counter(10); // Создать генератор с начальным значением 10
console.log(c.next()); //Выведет 10
console.log(c.send(2)); // Выведет 12
console.log(c.throw("reset")); // Выведет 10

Оглавление книги


Генерация: 0.050. Запросов К БД/Cache: 0 / 0
поделиться
Вверх Вниз