Рубріки: 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