Самое понятное руководство по итераторам в ES6 JavaScript. С примерами
В этой статье мы попытаемся понять и проанализировать итераторы в JS. Это сравнительно новые способы для того, чтобы пройтись по любой коллекции данных. Итераторы были представлены в ES6 и стали действительно популярны, так как оказались ну уж очень полезными и могут широко использоваться во многих рабочих кейсах.
Мы собираемся понять концепцию того, чем являются итераторы и где их можно использовать, всё будет конечно же с примерами. Также в статье мы увидим несколько примеров рабочего кода на JavaScript.
Введение
Представим, что у нас есть массив:
const myFavouriteAuthors = [
'Neal Stephenson',
'Arthur Clarke',
'Isaac Asimov',
'Robert Heinlein'
];
Рано или поздно, вам понадобится получить все отдельные значения массива для их: последующего вывода на страницу, манипулирования или для вообще, любого действия связанного с ними. Если я спрошу вас, как вы это всё сделаете? Вы ответите — да легко! Вы просто пустите цикл по всему массиву с помощью for
, while
, for-of
или одним из этих методов цикла. Пример выполнения был бы таким:
Теперь представьте, что вместо предыдущего массива, у нас есть кастомная структура данных, которая хранит ваших любимых авторов. Как тут:
Теперь myFavoriteAuthors
это объект, который содержит другой объект allAuthors
. AllAuthors
содержит три массива с ключами fiction
, scienceFiction
и fantasy
. Теперь, если я попрошу вас пройтись по myFavouriteAuthors
, чтобы получить всех авторов, как вы это сделаете? Вы можете попробовать какие-нибудь комбинации циклов, чтобы получить все нужные данные.
Тем не менее, если вы сделаете так, то:
for (let author of myFavouriteAuthors) {
console.log(author)
}
// TypeError: {} is not iterable
// Хоба, а так нельзя!
То вы получите TypeError
, говорящую о том, что объект нельзя проитерировать. Давайте посмотрим, что мы можем итерировать и как мы можем сделать сам объект таковым. В конце статьи вы узнаете как использовать for-of
цикл на кастомных объектах, а в нашем случае на myFavoriteAuthors
.
Итераторы и итерируемые
Вы уже видели проблему в предыдущем параграфе. Там нет простого способа пройтись по всем авторам заданного кастомного объекта. Нам понадобится какой-нибудь метод с помощью которого мы можем последовательно извлечь все нужные нам данные.
Давайте добавим метод getAllAuthors
в myFavouriteAuthors
, который отдаст нам всех авторов. Например:
Это довольно простой подход. Он решает нашу задачу в том, чтобы получить всех авторов. Тем не менее, несколько проблем всё таки всплывают в такой имплементации. Некоторые из них:
Имя getAllAuthors
очень специфичное. Если кто-то ещё сделает свой myFavouriteAuthors
, то они могут назвать его retrieveAllAuthors
.
Мы как разработчики, всегда должны знать конкретные методы, которые возвращают все требуемые данные. В нашем случае, это getAllAuthors
.
GetAllAuthors
возвращает массив строк, состоящий из всех авторов. А что если другой разработчик возвращает массив объектов в таком формате:
[ {name: 'Agatha Christie'}, {name: 'J. K. Rowling'}, ... ]
Разработчику нужно будет точно знать имя и тип метода, которые возвращает все данные.
Что если мы сделаем правило, в котором имя метода и его тип будут фиксированными и неизменными?
Давайте назовем этот метод — iteratorMethod
.
Подобный шаг был сделан ECMA, чтобы стандартизировать этот процесс проведения цикла по кастомным объектам. Однако, вместо использования имени iteratorMethod
, ECMA использовала имя Symbol.iterator
. Symbols предлагают имена, которые уникальны и не могут конфликтовать с именами других свойств. Также, Symbol.iterator
вернет объект именуемый iterator
. У этого итератора будет метод, под названием next
, который возвращает объект с ключами value
и done
.
Ключ value
, будет содержать актуальное значение. Это может быть всё, что угодно. А done
— это булево значение(логический тип данных). Он определяет все ли значения были отданы.
Схема нарисованная ниже может помочь с выстраиванием отношений между итерируемыми, итераторами и next. Эти отношения называются Iteration Protocol, то есть протокол итерации.
Согласно книге ExploringJS от Dr Axel Rauschmayer —
Итерируемые — это структура данных, которой нужно сделать свои элементы доступными для вне. Это делается выполнением метода, чей ключ — Symbol.iterator
. Этот метод является основой для итераторов. Вот так, он создаст итераторы.
Итератор — это указатель для навигации по элементам структуры данных.
Делаем объекты итерируемыми
Как мы уже узнали в предыдущей секции, нам нужно выполнить метод под названием Symbol.iterator
. Мы будем использовать синтаксис вычисления свойств. Короткий пример:
На 4й строке мы сделали итератор. Это объект с методом next
. Этот метод возвращает значение в соответствии с переменной step
. На строке 25, мы получаем iterator. На 27й, мы вызываем next
. И мы продолжаем вызывать next
, пока done
не станет true
.
Это именно то, что происходит в for-of
цикле. Циклы for-of
берут итерируемое и создают их итератор. Next()
же продолжает вызываться до то момента, пока done
, не станет true
.
Итерируемые в JavaScript
Много всего является итерируемым в JavaScript. Это может быть заметным не сразу, но если вы начнете внимательно вглядываться, то итерируемые начнут постепенно представляться вашему взору.
Вот это всё итерируемые-
Массивы (Arrays) и Типизированные массивы (TypedArrays)
Строки (Strings) — итерируются по каждому символу или кодовому символу в Unicode.
Maps — итерируется по всем парам ключевых значений.
Set — итерируется по всем элементам.
Arguments — массиво-подобная специальная переменная в функциях.
Элементы DOM — тут над этим ещё ведется работа
Другие итерируемые конструкции в JS это —
for-of
цикл —для for-of нужны итерируемые элементы. В противном случае, он выкинет ошибку TypeError
.
for (const value of iterable) { ... }
Деструктурицизация массивов — деструктуризация происходит из-за итерируемых. Давайте посмотрим как:
const array = ['a', 'b', 'c', 'd', 'e'];
const [first, ,third, ,last] = array;
Это по сути следующий код:
const array = ['a', 'b', 'c', 'd', 'e'];
const iterator = array[Symbol.iterator]();
const first = iterator.next().value
iterator.next().value // Так как оно пропущено, то не назначается
const third = iterator.next().value
iterator.next().value // Так как оно пропущено, то не назначается
const last = iterator.next().value
Оператор расширения spread. Вот код:
const array = ['a', 'b', 'c', 'd', 'e'];
const newArray = [1, ...array, 2, 3];
Его можно записать вот так:
const array = ['a', 'b', 'c', 'd', 'e'];
const iterator = array[Symbol.iterator]();
const newArray = [1];
for (let nextValue = iterator.next(); nextValue.done !== true; nextValue = iterator.next()) {
newArray.push(nextValue.value);
}
newArray.push(2)
newArray.push(3)
Promise.all
и Promise.race
принимают итерируемые.
Maps и Set.
Конструктор Map делает итерируемыми[key, value]
пары в Map, а конструктор Set делает итерируемыми элементы в Set.
const map = new Map([[1, 'one'], [2, 'two']]);
map.get(1)
// one
const set = new Set(['a', 'b', 'c]);
set.has('c');
// true
Итераторы также обязательны для понимания функций генераторов
Делаем myFavouriteAuthors итерируемыми
Вот код, который сделает myFavouriteAuthors
итерируемым:
const myFavouriteAuthors = {
allAuthors: {
fiction: [
'Agatha Christie',
'J. K. Rowling',
'Dr. Seuss'
],
scienceFiction: [
'Neal Stephenson',
'Arthur Clarke',
'Isaac Asimov',
'Robert Heinlein'
],
fantasy: [
'J. R. R. Tolkien',
'J. K. Rowling',
'Terry Pratchett'
],
},
[Symbol.iterator]() {
// Получаем все жанры
const genres = Object.values(this.allAuthors);
// Индекс заданного жанра и автора
let currentAuthorIndex = 0;
let currentGenreIndex = 0;
return {
// Выполнение next()
next() {
// Авторы в соответствии с индексом заданного автора
const authors = genres[currentGenreIndex];
// doNotHaveMoreAuthors будет true, когда закончатся все авторы в массиве.
// Вот и всё, собственно, всех авторов взяли.
const doNothaveMoreAuthors = !(currentAuthorIndex < authors.length);
if (doNothaveMoreAuthors) {
// Когда это происходит, мы двигаем индекс жанра к следующему жанру
currentGenreIndex++;
// И сбрасываем индекс автора на 0, чтобы получить новый набор авторов
currentAuthorIndex = 0;
}
// Если мы прошлись по всем жанрам, то нам нужно сказать всем итераторам о том,
// что мы больше не можем дать значений.
const doNotHaveMoreGenres = !(currentGenreIndex < genres.length);
if (doNotHaveMoreGenres) {
// Следовательно, мы возвращаем done со значением true.
return {
value: undefined,
done: true
};
}
// Если всё верно, возвращаем автора из данного жанра
// и увеличиваем currentAuthorindex
// так что в следующий раз, будет отдам уже следующий автор.
return {
value: genres[currentGenreIndex][currentAuthorIndex++],
done: false
}
}
};
}
};
for (const author of myFavouriteAuthors) {
console.log(author);
}
console.log(...myFavouriteAuthors)
Со знаниями, полученными в этой статье, вы сможете легко понять то, как работают итераторы. Логика может быть немного сложна. Поэтому тут есть комментарии в коде. Но лучшим способом усвоить и понять эту концепцию, является игра с кодом в бразуере или ноде.