Як застосовувати генератори у JavaScript

11 хв. читання

Сьогодні поговоримо про генератори JavaScript, які є у ES6, та розглянемо кілька прикладів їхнього застосування.

Що ж таке генератори

Генератор — це функція, яка дозволяє генерувати одне або кілька значень через виведення та повторне введення процедури виконання; при цьому її стан (контекст) зберігається у кількох викликах. Простіше кажучи, генератор схожий на звичайні функції, але може продовжувати виконання з точки, в якій він був раніше припинений, просто зберігаючи свій попередній стан.

Ось ця блок-схема ілюструє різницю між звичайною функцією та функцією генератора:

Як застосовувати генератори у JavaScript

Синтаксис

Як ви вже здогадалися, існують деякі синтаксичні відмінності між звичайною функцією та генератором:

// Звичайна функція
function normalFunction(params) {
  // логіка вашої функції
  return value;
}

/* --------------------------------- */

// Функція-генератор
function* generatorFunction(params) {
  // логіка вашої функції
  yield value1;

  // логіка вашої функції
  yield value2;

  /*
    .
    .
    .
  */

  // логіка вашої функції
  yield valueN;
}

Перша помітна відмінність у синтаксисі — оголошення генератора за допомогою ключового слова function* замість function. Також зверніть увагу на те, що ми вживаємо ключове слово return у звичайній функції. Натомість у функції генератора ми вживаємо ключове слово yield.

Ключове слово yield усередині генератора дозволяє нам «повернути» значення, припинити виконання, зберегти стан (контекст) поточної лексичної ділянки та чекати наступного виклику, щоб відновити виконання з останньої точки.

Примітка: у звичайній функції можна задіяти ключове слово return лише один раз, тобто воно поверне значення і повністю зупинить функцію. У генераторі ключове слово yield спрацює скільки разів, скільки ви схочете «повернути» значення для послідовних викликів. Ви також можете застосовувати ключове слово return всередині генератора, але цю тему залишимо для іншого обговорення.

Виклик генератора

Тепер, коли ми висвітлили відмінності синтаксису обох функцій, подивімось, як можна викликати генератор і отримати його значення. Спочатку розглянемо цей фрагмент коду з викликом звичайної функції:

function normalFunction() {
  console.log('Мене викликано');
}

// виклик
normalFunction();

Загалом ви можете викликати звичайну функцію, ввівши підпис функції, а потім пару дужок (). Попередній код виведе:

Мене викликано

Тепер спробуємо застосувати таку ж процедуру для виклику генератора. Уважно подивіться на цей фрагмент коду:

function* generatorFunction() {
  console.log('Мене викликано');
  yield 'first value';

  console.log('відновлення виконання');
  yield 'second value';
}

// чи це викликає генератор?
generatorFunction();

Що ви очікуєте від такої програми? По суті, що функція буде виконуватись, доки не досягне першого ключового слова. Однак вивід попередньої програми був порожній:

Це відбувається, оскільки звичайний синтаксис виклику не призводить до виконання тіла функції генератора. Замість цього він створює об'єкт Generator, який містить кілька властивостей та методів. Щоб довести це, ми спробуємо вивести результат console.log(generatorFunction()) і вивід має бути таким:

Object [Generator] {}

Тож питання: як же отримати наші значення з генератора?

Що ж, існують важливі методи, які належать до об'єкта Generator і якими ми можемо скористатися. Перший і найважливіший метод називається next(). Він дає наступне значення з визначеного генератора. Тепер змінимо наш попередній код, щоб отримати потрібні значення:

function* generatorFunction() {
  console.log('Мене викликано');
  yield 'first value';

  console.log('відновлення виконання');
  yield 'second value';
}

// збереження об'єкта генератора до змінної
let foo = generatorFunction();

// виконується, поки не отримаємо перше значення
console.log(foo.next());

// поновлюється виконання, поки не отримаємо друге значення
console.log(foo.next());

// виконується до завершення функції
console.log(foo.next());

Наведений код виведе:

Мене викликано
{ value: 'first value', done: false }
відновлення виконання
{ value: 'second value', done: false }
{ value: undefined, done: true }

Розберемо вивід рядок за рядком. Під час виклику першого методу foo.next() генератор починає виконуватись, поки не натрапить на перше ключове слово yield і не зупиниться. Це видно у перших двох рядках результату. Зверніть увагу, як foo.next() повертає Object замість фактичного отриманого значення. Цей об'єкт завжди повинен містити такі властивості:

  • «value»: утримує поточне отримане від генератора значення.

  • «done»: логічний прапор, який вказує, чи завершено виконання генератора.

Перейдімо до другого виклику foo.next(). Як і очікувалося, генератор поновлює виконання з останнього кроку на якому все зупинилося. Він виконується, поки не натрапить на друге ключове слово yield, яке ми бачимо у третьому та четвертому рядках результату. Зверніть увагу, що прапор done все одно має значення false, оскільки ми ще не дійшли до кінця функції.

На останньому виклику foo.next() функція відновлює виконання після другого ключового слова yield та нічого не знаходить далі, тобто ми досягли кінця функції. На цьому етапі немає більше значень для виводу: прапору done присвоюється значення true, що ми бачимо в останньому рядку виводу.

Тепер, коли ми розглянули основні поняття генераторів у JavaScript, подивимось на деякі корисні варіанти їхнього застосування.

Як застосовувати генератори

Варіант 1: імітуємо функцію range() з Python

Згідно з документацією Python, «тип range є незмінною послідовністю чисел і зазвичай потрібен для зациклювання певної кількості чисел у циклі for». Функція range() у Python зазвичай містить такі параметри:

  • start (необов'язковий, типове значення = 0): перше число у послідовності, включно з ним.

  • end (обов'язковий): останнє число послідовності, без його включення.

  • step (необов'язковий, типове значення = 1): різниця між будь-якими двома заданими числами в послідовності.

Базове застосування функції range() у Python :

# Код Python
for i range(3):
    print(i)

# вивід:
# 0
# 1
# 2

Нам потрібно імітувати цю функціональність у JavaScript за допомогою генераторів. Уважно подивимось на такий код:

/*
функція range впроваджена за допомогою JavaScript
*/
function* range({start = 0, end, step = 1}) {
  for (let i = start; i < end; i += step) yield i;
}

Розберемо його крок за кроком. По-перше, підпис функції визначає генератор, який приймає три параметри: start, end та step, в якому start та step мають типові значення 0 і 1 відповідно. Рухаємось тілом функції, у ній є базовий цикл for, який починає ітерацію від start (включно) до end (без включення). Всередині циклу ми отримуємо значення i поточного числа у послідовності.

Погляньмо на це в дії. Ось різні приклади імплементації функції range:

// перший приклад
for (let i of range({end: 4})) console.log(i);

/*
вивід:
0
1
2
3
*/

// другий приклад
for (let i of range({start: 2, end: 4})) console.log(i);

/*
вивід:
2
3
*/

// третій приклад
for (let i of range({start: 1, end: 8, step: 2})) console.log(i);

/*
вивід:
1
3
5
7
*/

Варіант 2: візуалізація алгоритму бульбашкового сортування (Bubble Sort)

Спробуємо вивести поетапне виконання алгоритму бульбашкового сортування для масиву — так його буде легко візуалізувати. Якщо коротко, то бульбашкове сортування працює так: дано масив довжини n та i поточної ітерації. Тоді max(array[0:n - i]) поширюється на індекс n - i та продовжує це робити, доки масив не відсортується. Типова реалізація:

/*
Впровадження бульбашкового сортування за допомогою javascript
*/
function bubbleSort(arr) {
  for (let i = arr.length - 1; i >= 0; i--) {
    for (let j = 0; j < i; j++) {
      // якщо поточне значення більше суміжного,
       // вони міняються місцями 
      if (arr[j] > arr[j+1]) {
        [arr[j], arr[j+1]] = [arr[j+1], arr[j]];
      }
    }
  }

  return arr;
}

Наше завдання — візуалізувати покрокові порівняння та обміни (свопи), які виконуються у цьому алгоритмі. Це легко зробити за допомогою генераторів. Ми просто повертаємо поточний масив після кожної ітерації у внутрішньому циклі. Нова функція буде такою:

/*
візуалізація впровадження бульбашкового сортування за допомогою javascript
*/
function* visualizeBubbleSort(arr) {
  for (let i = arr.length - 1; i >= 0; i--) {
    for (let j = 0; j < i; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }

      yield arr;
    }
  }
}

Це повертатиме масив для кожної ітерації у внутрішньому циклі, показуючи нам поточний стан масиву. Розглянемо такий приклад:

let inputArray = [40, 30, 2, 20];
let currentStep = 1;
for (let val of visualizeBubbleSort(inputArray)) {
  console.log(`step #${currentStep}: [${val}]`);
  currentStep++;
}

Вивід попередньої програми буде таким:

step #1: [30,40,2,20]
step #2: [30,2,40,20]
step #3: [30,2,20,40]
step #4: [2,30,20,40]
step #5: [2,20,30,40]
step #6: [2,20,30,40]

Ми можемо чітко бачити, що відбувається по всьому алгоритму, завдяки реалізованому генератору:

  • крок 1 -> заміна 40 на 30
  • крок 2 -> заміна 40 на 2
  • крок 3 -> заміна 40 на 20
  • крок 4 -> заміна 30 на 2
  • крок 5 -> заміна 30 на 20
  • крок 6 -> нічого не замінюється, масив відсортовано.

Примітка: ця техніка може згодитись для візуалізації будь-якого алгоритму. Іноді вона буває дуже корисною.

Варіант 3: генерування різних випадкових чисел на вимогу

У цьому разі ми спробуємо створити серію різних випадкових чисел за допомогою генераторів. По-перше, ми накладемо деякі обмеження. Наша функція повинна :

  • генерувати лише додатні цілі числа;
  • приймати параметр limit, який визначає максимальну кількість згенерованих цілих чисел, а також максимально можливе згенероване ціле додатне число;
  • зберігати дійсний пул цілих додатних чисел.

Із такими обмеженнями ми можемо легко втілити цю функціональність через генератори:

/*
впровадження distinctRandom за допомогою js 
*/
function* distinctRandom({limit = 10}) {
  // ми створюємо масив, який містить усі числа в діапазоні [0: limit)
   // це наш початковий пул чисел на вибір
  const availableValues = [...new Array(limit)].map((val, index) => index);

  // ми повторюємо цикл, поки наявний пул чисел не буде порожнім
  while (availableValues.length !== 0) {
    // генеруємо випадковий індекс у діапазоні [0: availableValues.length)
     // потім отримуємо число, яке є у вибраному індексі
     // Нарешті, вилучаємо вибраний елемент із пулу доступних чисел
    const currentRandom = Math.floor(Math.random() * availableValues.length);
    yield availableValues[currentRandom];
    availableValues.splice(currentRandom, 1);
  }
}

Якщо коротко, то попередній генератор намагається підтримувати пул доступних цілих чисел, які можна вибрати. За кожної ітерації ми вибираємо звідти випадкове число, потім повертаємо його та вилучаємо із нашого переліку.

Теоретично, максимальна кількість згенерованих цілих чисел має дорівнювати limit, а всі згенеровані цілі додатні числа повинні бути різними. Ми можемо легко довести це, коли вичерпаємо доступні числа нашого генератора до кінця виконання:

// встановлюємо обмеження до 8 чисел
for (const val of distinctRandom({limit: 8})) {
  console.log(val);
}

/*
приклад виводу:
3
7
5
2
4
0
1
6
*/

Підсумуємо

Генератори є чудовим доповненням до ES6: вони допомагають впоратися з мільйоном проблем та корисні в багатьох ситуаціях. Ви можете використовувати їх будь-де, але спершу варто ознайомитися з альтернативами для конкретних випадків, оскільки генератори можуть ускладнити і ваш код, і процес налагодження. Успіху! 🎉

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.9K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація