Ключевое слово yield
используется в JavaScript для приостановки выполнения функций. При повторном вызове функции ее выполнение продолжается с последнего оператора yield
.
Функция, возвращающая (yields) значения, является генератором. Сравнительная иллюстрацию запуска функции и функциональности генератора:
Генератор возвращает объект-генератор, который является итератором. Этот объект однократно генерирует значение и приостанавливает работу. Он не хранит значения, поэтому эффективно использует память.
Как превратить функцию в генератор
Для этого нужно:
- Добавить символ звездочки (
*
) после ключевого словаfunction
. - Использовать
yield
вместоreturn
.
Пример
Создадим обычную функцию square ()
, которая возводит в квадрат массив чисел:
function square(numbers){
let result = [];
for (const n of numbers){
result.push(n * n);
}
return result;
}
const numbers = [1, 2, 3, 4, 5];
const squared_numbers = square(numbers);
console.log(squared_numbers);
Результат:
[1, 4, 9, 16, 25]
Превратим эту функцию в генератор:
Вместо добавления в массив значений квадратов, выводим (yield
) их по одному:
function* square(numbers){
for (const n of numbers){
yield n * n;
}
}
const numbers = [1, 2, 3, 4, 5];
const squared_numbers = square(numbers);
console.log(squared_numbers);
Выход:
: [object Generator]
В результате мы пока не получаем массив возведенных в квадрат чисел, потому что squared_numbers
— это объект-генератор. И он еще не производил никаких вычислений.
А теперь смотрите, как можно получать из объект-генератора возведенные в квадрат числа.
next() method
Генератор не сохраняет числа в памяти. Вместо этого он их последовательно вычисляет и по одному выдает. После выдачи значения объект-генератор приостанавливается до запроса следующего значения.
Чтобы получить возведенные в квадрат числа из приведенного выше примера, нужно запросить у объект-генератора следующее значение, используя метод next ()
.
Запросим генератор вычислить квадрат первого числа.
console.log(squared_numbers.next())
Результат:
{
value:1,
done:false
}
Этот объект результата имеет два атрибута:
value
— результат возведения в квадрат первого из чисел массива.done
— сообщает о завершении функции генератора.
На этом этапе после возведения в квадрат первого числа объект-генератор был приостановлен.
Позволим генератору вычислить остальные значения, вызывая next()
четыре раза:
console.log(squared_numbers.next().value)
console.log(squared_numbers.next().value)
console.log(squared_numbers.next().value)
console.log(squared_numbers.next().value)
Выход:
4
9
16
25
Генератор возвел в квадрат все числа. Если вызвать next ()
еще раз:
console.log(squared_numbers.next())
То получим объект, в котором value
не определено (undefined
), а done
верно (true
).
{
value: undefined,
done: true
}
И это естественно, потому что генератор достиг конца массива. Работа генератора прекращена.
Теперь понятно, как работает генератор и как заставить его вычислять значения.
Посмотрим, как можно создать цикл без неудобного метода next ()
.
Get rid of next()
Использование функции next()
демонстрирует действие генераторов, но для работы с объект-генераторами ее вызывать не нужно. Вместо этого можно использовать цикл for-of
, очень похожий на перебор массива чисел.
Например, повторим приведенный выше пример с генератором, используя цикл for-of
:
function* square(numbers){
for (const n of numbers){
yield n * n;
}
}
const numbers = [1, 2, 3, 4, 5];
const squared_numbers = square(numbers);
for (const n of squared_numbers){
console.log(n);
}
Результат:
1
4
9
16
25
Синтаксически это похоже на организацию цикла в обычном массиве чисел.
К тому же это удобно, так как не нужно самим вызывать next()
. Цикл for-of
знает, как это сделать за нас. А также он позаботится о том, чтобы генератор не вызывал next ()
, когда закончатся значения для возведения в квадрат.
Генераторы и массивы — сравнение по времени исполнения
Давайте сравним генераторы и функции по времени выполнения.
Возьмем для примера массив из десяти чисел и двух функций:
- Функция
data_array ()
случайным образом выбирает число из массива n раз. - Функция генератора
data_generator()
также случайным образом выбирает число из массива n раз.
Этот код сравнивает время выполнения этих функций для построения набора из 1 миллиона случайно выбранных чисел. Не беспокойтесь о деталях реализации:
const range = (start, end) => {
const length = end - start;
return Array.from({ length }, (_, i) => start + i);
}
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
function data_array(n){
let result = [];
for (const i of range(0, n)) {
result.push(numbers[Math.floor(Math.random() * numbers.length)]);
}
return result;
}
function* data_generator(n){
for (const i of range(0, n)) {
yield numbers[Math.floor(Math.random() * numbers.length)];
}
}
const t_array_start = performance.now();
const rand_array = data_array(1000000);
const t_array_end = performance.now();
const t_gen_start = performance.now();
const rand_gen = data_generator(1000000);
const t_gen_end = performance.now();
const t_gen = t_gen_end - t_gen_start;
const t_array = t_array_end - t_array_start;
console.log(`Array creation took ${t_array} milli seconds`);
console.log(`Generator creation took ${t_gen} milli seconds `);
console.log(`The generator is ${t_array / t_gen} times faster`);
Результат:
Создание массива заняло 121.39999997615814 мс
Создание генератора заняло 0.10000002384185791 мс
Генератор работает в 1213.9997103214955 раз быстрее
Генератор можно создать намного быстрее, чем массив чисел. Это связано с тем, что весь миллион чисел массива должен храниться в памяти. Генератор же ничего не сохраняет в памяти. Он даже не начинает выбирать числа до тех пор, пока для него не будет вызван метод next ()
.
Следует отметить, что этот пример нельзя считать полностью объективным. Генератор не использует данные. Таким образом, в отношении производительности такое сравнение не имеет смысла. Но оно наглядно демонстрирует, что создание генератора практически не занимает время и не требует использования памяти.
Примеры использования генераторов в JavaScript
Бесконечные потоки
Поскольку объекты-генераторы (итераторы) не хранят значения, они позволяют создавать бесконечный поток значений.
Например, создадим бесконечный генератор, который производит все числа после начальной точки:
function* infinite_values(start){
let current = start;
while(true){
yield current;
current += 1;
}
}
Этот генератор выдает значения от начала (start
) до бесконечности.
Выполним его с начальной точки 10
:
const infinite_nums = infinite_values(10);
for (const num of infinite_nums){
console.log(num);
}
Результатом будет бесконечный поток чисел, начиная с 10
:
10
11
12
13
14
15
.
.
.
Синтаксически infinite_nums
представляется действительно бесконечным массивом чисел. На самом деле это просто генератор с бесконечным циклом.
Упростим создание итераторов
Создающим свои итераторы программистам знакомы проблемы с кодом.
Для примера создадим итератор, который печатает три строки:
const helloIterator = {
[Symbol.iterator]() {
let count = 0;
return {
next() {
count++;
if (count === 1) {
return { value: 'Hello', done: false};
} else if (count === 2) {
return { value: 'World', done: false};
} else if (count === 3) {
return { value: 'Its me', done: false};
}
return { value: '', done: true };
}
}
},
}
for (const val of helloIterator) {
console.log(val);
}
Выход:
Hello
World
Its me
Сделать код намного чище позволяет функция генератора:
function * iterableObj() {
yield 'Hello';
yield 'World';
yield 'Its me'
}
for (const val of iterableObj()) {
console.log(val);
}
Выход:
Hello
World
Its me
Заключение
yield
— это ключевое слово в JavaScript, превращающее функцию в генератор.
В отличие от массива генератор не хранит значения. Он имеет только текущее значение и знает, как получить следующее. Что позволяет говорить об экономии памяти.
Использование генераторов может быть выгодно при организации цикла в большой группе элементов, и при этом не нужно хранить их все сразу.
Читайте также:
- Применение методов Bind(), Call(), and Apply() в JavaScript
- 8 мощных пакетов NPM для любого веб-разработчика
- Темная сторона Javascript: избегайте данных трех функций
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Artturi Jalli: How To Transform JavaScript Functions Into Memory-Efficient Generators