Генераторы | Generators

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

Для того чтобы сделать функцию генератор - нужно проделать следующее

  • После ключевого слова function нужно добавить символ звёздочка *. Звёздочка может распологаться в трёх положениях: function* generate(), function * generate() , function *generate()
  • В  теле функции нужно использовать новое ключевое слово yield
function* generate() {
    console.log('Start');
    yield;
    console.log('Finish');
}
let iterator = generate();
iterator.next();
iterator.next();
// Start
// Finish

Дополнительные способы определить генератор

  •  В качестве генератора может использоваться анонимная функция
    let generator = function*(){
    }
  • Функция генератор может быть определена у литерала объекта
    let obj = {
    	*generator(){}
    }
  • Генератор может быть определён у класса
    class SomeClass {
    	*generator(){}
    }

Генераторы работают как фабрика итераторов.
Когда мы вызываем функцию генератор - мы получаем объект итератор, с помощью которого мы можем приостанавливать и возобновлять процесс выполнения функции. В свою очередь слово yield - производит и отдаёт информацию, т.е. объект со свойствами IteratorResult{done, value}, но при этом также уступает или отдаёт контроль над функцией.

Кроме приостановки ключевое слово yield - также возвращает промежуточный вариант вычислений и передаёт данные в функцию.

function* generate() {
    console.log('Start');
    yield 1;
    yield 2;
    yield 3;
    console.log('Finish');
}

let iterator = generate();
console.log( iterator.next() ); // первый вызов - запускает генератор, в него нельзя отправить  значение
console.log( iterator.next() );
console.log( iterator.next() );
console.log( iterator.next() );

// Start
// Object { value: 1, done: false }
// Object { value: 2, done: false }
// Object { value: 3, done: false }
// Finish
// Object { value: undefined, done: true }

Создание собственных итераторов может быть весьма полезным, но при их создании требуется уделять большое внимание поддержке внутреннего состояния. Генераторы предоставляют альтернативу - они позволяюбт определить алгоритм перебора написав функцию, которая умеет сама поддерживать собственное состояние.

В качестве более пркатического пример создадим функцию range, с помощью которой будем получать ряд чисел. А то с какого числа начинать и заканчивать - будет указано в аргументах.

function * range(start, end) {
	let current = start;
	while (current <= end) {
		yield current++; // возвращаем значение с помощью yield 
	}
}

for (let num of range(1, 10)) {// перебираем значения с помощью итератора объекта
    console.log(num);
}

Делегирование генератора

Делегирование генератора - это метод когда элементы массива отдаются по одному, так если бы перед каждыйм элементом стояло бы ключевое слово yield.

function * generator(){
	yield 42;
	yield* [1, 2, 3]; // Делегирование генератора
	yield 43;
}
let iterator = generator();
console.log(iterator.next().value);
console.log(iterator.next().value);
console.log(iterator.next().value);
console.log(iterator.next().value);
console.log(iterator.next().value);
console.log(iterator.next().value);
// 42
// 1 
// 2
// 3
// 43
// undefined

Также при помощи синтаксиса yield * - в теле одного генератора можно вызвать другой генератор.

function * generateArray(){
	yield * [1, 2, 5];
}
function * generator(){
	yield 42;
	yield * generateArray();
	yield 44;
}
let iterator = generator();
console.log(iterator.next().value);
console.log(iterator.next().value);
console.log(iterator.next().value);
console.log(iterator.next().value);
console.log(iterator.next().value);
console.log(iterator.next().value);
// 42
// 1 
// 2
// 5
// 44
// undefined

Функция генератор возвращает объект соответсвующий интерфейску итератора. Помимо метода next() у возвращаемого объекта есть ещё 2 метода, которых нет у итераторов. С помощью этих методов  можем досрочно остановить генератор.

iterator{
	next(),
	return(),
	throw()
}

Отличие return() от throw() в том что return() - просто останавливает выполнение, а throw() - генерирует ошибку которую модно поймать конструкцией try...catch.

function * generator(){
	try{
		yield 1;
		yield 2;
		yield 3;
	}
	catch(e){
		console.log(e)
	}
}
let iterator = generator();
console.log(iterator.next());
console.log(iterator.throw(new Error('Custom Generated Error')));
console.log(iterator.next());

// Object { value: 1, done: false }
// Error: Custom Generated Error
// Object { value: undefined, done: true }
// Object { value: undefined, done: true }

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