Рубріки: Back-end

Можно все, и по своим правилам: как работает итерация в JavaScript

Владислав Хирса

Привет всем. Меня зовут Владислав Хирса, я — Software Engineer в Grid Dynamics. В этой статье я расскажу вам много полезного об итерации в JavaScript.

Мы все используем итерацию для перебора массивов и объектов разных размеров и разных задач. Но знаем ли мы, как это работает? Как мы можем изменить поведение итерации над нашими данными и в каких случаях это может потребоваться?

Если вам интересны ответы на эти вопросы, то этот материал точно для вас.

Вспомним термины

Для лучшего понимания вспомним несколько значений, а именно:

  • Перечисляемые свойства enumerable properties — одно из трех свойств (configurable, enumerable, writable), имеющихся в объекте. Относительно enumerable, то она отвечает за то, можно ли вернуть свойство в цикле for...in.

Например, так:

Object.prototype.getType = function() {
  return this.type;
};
const object = {
  language: 'JavaScript',
  type: 'Lesson'
};
for (const key in object) {
  console.log(key); // JavaScript, Lesson, getType;
};
  • Итерированный объект и terable object — это объект, построенный по определенному паттерну и имеющий типичное итерационное поведение. Он итерируется с помощью spread syntax [...], for...of и for await...of. И в этой статье мы поговорим именно о нем.
  • Протокол итерации iterator protocol это протокол, с помощью которого мы можем создать собственные правила, по которым будет итерироваться наш объект. Если подробнее, то итерировать мы сможем такие типы данных, как string, array, object.

Главные правила iterator protocol это:

  1. У нас должен быть метод next().
  2. Метод next() должен обязательно возвращать объект типа iterable object. Он содержит ключи value, которые могут иметь любое значение, и done, который может быть true или false.
  3. Когда итерация завершилась, нужно обязательно вернуть объект { done: true }, ведь только после этого наша итерация закончится.
  4. Если done не будет возвращен или done будет иметь какое-либо негативное значение, такое как undefined, null и другие, то наша итерация будет бесконечной.

Создание итеративного поведения

Для создания итеративного поведения мы будем использовать следующие подходы:

  • Symbol.iterator с методом next();
  • Symbol.iterator с генератором;
  • Symbol.asyncIterator с генератором.

Так что по порядку, и начнем мы с Symbol.iterator с методом next().

var array = [10, 20, 30, 40, 50];
array[Symbol.iterator] = function () {
  let i = 0;
  return {
    next() {
      i++
      return (i <= 5)
        ? { value: i, done }
        : { done: true }
    }
  }
};
for (const el of array) {
  console.log(el) // 1, 2, 3, 4, 5
};

Как это работает?

Сначала цикл for...of ищет в нашем объекте или среди тех, от которых он наследуется в prototype, есть ли у него Symbol.iterator. Если нет — то будет вызвана автоматическая ошибка, а если да — тогда он вызывает функцию, функция возвращает наш объект с методом next(), где и есть вся наша основная логика, и самое главное, что при каждой итерации цикла используется метод next(), который и возвращает значение по нашим условиям.

Symbol.iterator з генератором

С Symbol.iterator уже ознакомились, а как насчет генераторов?

Генератор, если коротко, то это способ создания того же iterable object для итерации по iterator protocol, то есть у него тоже есть метод next() и он итерируется циклом for...of, только синтаксис другой, и использовать его в некоторых случаях удобнее:

var it = {};
it[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};
it.type = 'Lesson';
console.log(it); // { type: "Lesson", Symbol(Symbol.iterator): * Symbol.iterator() }
console.log([...it]); // [1, 2, 3]

В этой части мы изменили поведение объекта и дали ему возможность быть итерированным. Без Symbol.iterator была бы ошибка такого типа — Uncaught TypeError: it is not iterable при попытке итерировать объект. Так же как мы видим, что к итерационным данным мы не имеем доступа напрямую, а с Symbol.iterator нет доступа к значениям объекта. Это очень удобно, и нет никаких мутаций данных.

Symbol.asyncIterator з генератором

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

Файл async-iterator.ts:

import { AsyncLimitIteratorParametersType } from './types';
export default class Iterator {
  public static generateAsyncLimitIterator(
    data: AsyncLimitIteratorParametersType
  ) {
    const {
      from,
      to,
      limit,
      asyncFunction,
      asyncFunctionParams
   } = data;
    return {
      async *[Symbol.asyncIterator]() {
        for (let now = from; now < to; now += limit) {
          yield await asyncFunction(
            asyncFunctionParams,
            { from, to, limit, now }
          );
        }
      }
    }
  }
};

Файл types.d.ts:

export type AsyncLimitIteratorParametersType = {
  from: number;
  to: number;
  limit: number;
  asyncFunction: Function;
  asyncFunctionParams: any;
}
export type IterationDataType = {
  from: number;
  to: number;
  limit: number;
  now: number;
};

Класс Iterator создан для асинхронной обработки (изменения) больших массивов данных.

Так что разберем, как его можно использовать и в чем его преимущества.

Класс Iterator

Итак, класс Iterator. У него есть один статический метод generateAsyncLimitIterator, к которому мы имеем доступ, не используя оператор new, метод generateAsyncLimitIterator принимает параметры:

  • from — из какого индекса изменять массив;
  • to — по какой позиции по индексу изменять массив;
  • limit — сколько элементов за одну итерацию захватить и пройти;
  • asyncFunction — функция, в которой выполняются основные действия.

Например, запрос на базу данных для изменения объекта пользователей. asyncFunctionParams параметры, принимаемые функцией asyncFunction.

Пример применения:

const iteratorParams = {
  from: 0,
  to: 1000,
  limit: 10,
  asyncFunction: changeUsers,
  asyncFunctionParams: params
};
const iterateObj = Iterator.generateAsyncLimitIterator(iteratorParams);
for await (let value of iterateObj) {
  //
}

После того, как мы создали асинхронный итератор, мы просто используем его в асинхронном цикле и не беспокоимся о переполненном стеке вызовов и о том, что заблокирован Event Loop, а просто используем надежный способ обработки данных.


На этом все! Надеюсь, что было полезно. Желаю успехов и продуктивного кодинга 😉

If you have found a spelling error, please, notify us by selecting that text and pressing Ctrl+Enter.

Останні статті

Токсичные коллеги. Как не стать одним из них и прекратить ныть

В благословенные офисные времена, когда не было большой войны и коронавируса, люди гораздо больше общались…

07.12.2023

Делать что-то впервые всегда очень трудно. Две истории о начале карьеры PM

Вот две истории из собственного опыта, с тех пор, когда только начинал делать свою карьеру…

04.12.2023

«Тыжпрограммист». Как люди не из ІТ-отрасли обесценивают профессию

«Ты же программист». За свою жизнь я много раз слышал эту фразу. От всех. Кто…

15.11.2023

Почему чат GitHub Copilot лучше для разработчиков, чем ChatGPT

Отличные новости! Если вы пропустили, GitHub Copilot — это уже не отдельный продукт, а набор…

13.11.2023

Как мы используем ИИ и Low-Code технологии для разработки IT-продукта

Несколько месяцев назад мы с командой Promodo (агентство инвестировало в продукт более $100 000) запустили…

07.11.2023

Университет или курсы. Что лучше для получения IT-образования

Пару дней назад прочитал сообщение о том, что хорошие курсы могут стать альтернативой классическому образованию.…

19.10.2023