Как язык, JavaScript построен на концепции итерации. Возможность повторять задачу или действие над набором данных является фундаментальной для многих операций, которые мы выполняем в программировании. В JavaScript концепция итерации реализуется с помощью концепции итерируемых объектов.
Итерируемые объекты — это структуры данных, которые позволяют другим потребителям данных осуществлять последовательный доступ к своим элементам. Например, структура данных, которая выгружает данные один за другим по порядку при помещении в цикл for...of.
Итерируемый протокол состоит из итерируемого объекта, который является самой структурой данных, и итератора, указателя, который перемещается по итерируемому объекту. Когда массив помещается в цикл for...of, его итерируемое свойство с именем Symbol.iterator возвращает итератор. Этот объект можно использовать в общем интерфейсе, используемом всеми структурами управления циклом.
Перебор массивов
Одно из наиболее распространенных применений итерируемых объектов в JavaScript — перебор массивов. Массивы — это наборы данных, которые можно индексировать числовым индексом. В следующем примере показано, как перебирать массив чисел с помощью цикла for...of
:
const numbers = [1, 2, 3, 4, 5]; for (const number of numbers) { console.log(number); }
В этом примере цикл for...of
перебирает массив numbers
, присваивая каждый элемент по очереди переменной number
. Затем цикл записывает каждое число в консоль.
Перебор строк
Строки — еще одна распространенная итерация в JavaScript. Строку можно рассматривать как массив символов. В следующем примере показано, как перебирать строку с помощью цикла for...of
:
const str = "hello"; for (const char of str) { console.log(char); }
В этом примере цикл for...of
перебирает строку str
, по очереди присваивая каждый символ переменной char
. Затем цикл выводит каждый символ на консоль.
Перебор наборов
Множества — это относительно новая структура данных в JavaScript, добавленная в ECMAScript 6. Множества похожи на массивы тем, что представляют собой наборы данных, но имеют некоторые важные отличия. Одно из этих отличий состоит в том, что наборы не допускают дублирования. В следующем примере показано, как перебирать набор с помощью цикла for...of
:
const mySet = new Set([1, 2, 3]); for (const value of mySet) { console.log(value); }
В этом примере цикл for...of
перебирает набор mySet
, по очереди присваивая каждое значение переменной value
. Затем цикл записывает каждое значение в консоль.
Создание пользовательских итераций
Хотя многие встроенные структуры данных JavaScript являются итерируемыми, также можно создавать собственные итерируемые объекты. Чтобы создать пользовательскую итерацию, объект должен реализовать метод Symbol.iterator
. Этот метод должен возвращать объект итератора, предоставляющий метод next()
.
В следующем примере показано, как создать пользовательскую итерацию, которая генерирует последовательность Фибоначчи:
const fibonacci = { [Symbol.iterator]() { let a = 0, b = 1; return { next() { const value = a; a = b; b = value + b; return { value, done: false }; } } } } for (const number of fibonacci) { console.log(number); if (number > 1000) { break; } }
В этом примере объект fibonacci
представляет собой пользовательскую итерацию, которая генерирует последовательность Фибоначчи. Метод Symbol.iterator
возвращает объект итератора, который генерирует последовательность. Метод next()
объекта итератора возвращает следующее значение в последовательности при каждом вызове.
Символ.iterator
Мы можем легко понять концепцию итерируемых объектов, создав свою собственную.
Например, у нас есть объект, который не является массивом, но выглядит подходящим для for..of
.
Подобно объекту range
, представляющему интервал чисел:
let range = { from: 1, to: 5 };
Чтобы сделать объект range
итерируемым (и, таким образом, позволить for..of
работать), нам нужно добавить метод к объекту с именем Symbol.iterator
(специальный встроенный символ именно для этого).
- Когда
for..of
запускается, он вызывает этот метод один раз (или ошибки, если не найден). Метод должен возвращать итератор — объект с методомnext
. - Далее
for..of
работает только с этим возвращенным объектом. - Когда
for..of
требуется следующее значение, он вызываетnext()
для этого объекта. - Результат
next()
должен иметь вид{done: Boolean, value: any}
, гдеdone=true
означает, что цикл завершен, иначеvalue
является следующим значением.
Вот полная реализация range
с примечаниями:
let range = { from: 1, to: 5 }; // 1. call to for..of initially calls this range[Symbol.iterator] = function() { // ...it returns the iterator object: // 2. Onward, for..of works only with the iterator object below, asking it for next values return { current: this.from, last: this.to, // 3. next() is called on each iteration by the for..of loop next() { // 4. it should return the value as an object {done:.., value :...} if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; }; // now it works! for (let num of range) { alert(num); // 1, then 2, 3, 4, 5
Обратите внимание на основную особенность итерируемых объектов: разделение задач.
- Сам
range
не имеет методаnext()
. - Вместо этого вызовом
range[Symbol.iterator]()
создается другой объект, так называемый «итератор», и егоnext()
генерирует значения для итерации.
Таким образом, объект итератора отделен от объекта, по которому он выполняет итерацию.