Итераторы
Материал на этой странице устарел, поэтому скрыт из оглавления сайта.
Более новая информация по этой теме находится на странице https://learn.javascript.ru/iterable.
В современный JavaScript добавлена новая концепция «итерируемых» (iterable) объектов.
Итерируемые или, иными словами, «перебираемые» объекты – это те, содержимое которых можно перебрать в цикле.
Например, перебираемым объектом является массив. Но не только он. В браузере существует множество объектов, которые не являются массивами, но содержимое которых можно перебрать (к примеру, список DOM-узлов).
Для перебора таких объектов добавлен новый синтаксис цикла: for..of .
'use strict'; let arr = [1, 2, 3]; // массив — пример итерируемого объекта for (let value of arr) < alert(value); // 1, затем 2, затем 3 >
Также итерируемой является строка:
'use strict'; for (let char of "Привет") < alert(char); // Выведет по одной букве: П, р, и, в, е, т >
Итераторы – расширяющая понятие «массив» концепция, которая пронизывает современный стандарт JavaScript сверху донизу.
Практически везде, где нужен перебор, он осуществляется через итераторы. Это включает в себя не только строки, массивы, но и вызов функции с оператором spread f(. args) , и многое другое.
В отличие от массивов, «перебираемые» объекты могут не иметь «длины» length . Как мы увидим далее, итераторы дают возможность сделать «перебираемыми» любые объекты.
Свой итератор
Допустим, у нас есть некий объект, который надо «умным способом» перебрать.
Например, range – диапазон чисел от from до to , и мы хотим, чтобы for (let num of range) «перебирал» этот объект. При этом под перебором мы подразумеваем перечисление чисел от from до to .
Объект range без итератора:
let range = < from: 1, to: 5 >; // хотим сделать перебор // for (let num of range) .
Для возможности использовать объект в for..of нужно создать в нём свойство с названием Symbol.iterator (системный символ).
При вызове метода Symbol.iterator перебираемый объект должен возвращать другой объект («итератор»), который умеет осуществлять перебор.
По стандарту у такого объекта должен быть метод next() , который при каждом вызове возвращает очередное значение и проверяет, окончен ли перебор.
В коде это выглядит следующим образом:
'use strict'; let range = < from: 1, to: 5 >// сделаем объект range итерируемым range[Symbol.iterator] = function() < let current = this.from; let last = this.to; // метод должен вернуть объект с методом next() return < next() < if (current ; > else < return < done: true >; > > > >; for (let num of range) < alert(num); // 1, затем 2, 3, 4, 5 >
Как видно из кода выше, здесь имеет место разделение сущностей:
- Перебираемый объект range сам не реализует методы для своего перебора.
- Для этого создаётся другой объект, который хранит текущее состояние перебора и возвращает значение. Этот объект называется итератором и возвращается при вызове метода range[Symbol.iterator] .
- У итератора должен быть метод next() , который при каждом вызове возвращает объект со свойствами:
- value – очередное значение,
- done – равно false если есть ещё значения, и true – в конце.
Конструкция for..of в начале своего выполнения автоматически вызывает Symbol.iterator() , получает итератор и далее вызывает метод next() до получения done: true . Такова внутренняя механика. Внешний код при переборе через for..of видит только значения.
Такое отделение функциональности перебора от самого объекта даёт дополнительную гибкость. Например, объект может возвращать разные итераторы в зависимости от своего настроения и времени суток. Однако, бывают ситуации когда оно не нужно.
Если функциональность по перебору (метод next ) предоставляется самим объектом, то можно вернуть this в качестве итератора:
'use strict'; let range = < from: 1, to: 5, [Symbol.iterator]() < return this; >, next() < if (this.current === undefined) < // инициализация состояния итерации this.current = this.from; >if (this.current ; > else < // очистка текущей итерации delete this.current; return < done: true >; > > >; for (let num of range) < alert(num); // 1, затем 2, 3, 4, 5 >// Произойдёт вызов Math.max(1,2,3,4,5); alert( Math.max(. range) ); // 5 (*)
При таком подходе сам объект и хранит состояние итерации (текущий перебираемый элемент).
В данном случае это работает, но для большей гибкости и понятности кода рекомендуется, всё же, выделять итератор в отдельный объект со своим состоянием и кодом.
В последней строке (*) примера выше можно видеть, что итерируемый объект передаётся через spread для Math.max .
При этом . range интерпретируется как последовательность чисел. То есть произойдёт цикл for..of по range , и его результаты будут использованы в качестве списка аргументов.
Возможны и бесконечные итераторы. Например, пример выше при range.to = Infinity будет таковым. Или можно сделать итератор, генерирующий бесконечную последовательность псевдослучайных чисел. Тоже полезно.
Нет никаких ограничений на next , он может возвращать всё новые и новые значения, и это нормально.
Разумеется, цикл for..of по такому итератору тоже будет бесконечным, нужно его прерывать, например, через break .
Встроенные итераторы
Встроенные в JavaScript итераторы можно получить и явным образом, без for..of , прямым вызовом Symbol.iterator .
Например, этот код получает итератор для строки и вызывает его полностью «вручную»:
'use strict'; let str = "Hello"; // Делает то же, что и // for (var letter of str) alert(letter); let iterator = str[Symbol.iterator](); while(true) < let result = iterator.next(); if (result.done) break; alert(result.value); // Выведет все буквы по очереди >
То же самое будет работать и для массивов.
Итого
- Итератор – объект, предназначенный для перебора другого объекта.
- У итератора должен быть метод next() , возвращающий объект , где value – очередное значение, а done: true в конце.
- Метод Symbol.iterator предназначен для получения итератора из объекта. Цикл for..of делает это автоматически, но можно и вызвать его напрямую.
- В современном стандарте есть много мест, где вместо массива используются более абстрактные «итерируемые» (со свойством Symbol.iterator ) объекты, например оператор spread . .
- Встроенные объекты, такие как массивы и строки, являются итерируемыми, в соответствии с описанным выше.
Итераторы и генераторы
Обработка каждого элемента коллекции является весьма распространённой операцией. JavaScript предоставляет несколько способов перебора коллекции, от простого цикла for до map() , filter() и array comprehensions (en-US) . Итераторы и генераторы внедряют концепцию перебора непосредственно в ядро языка и обеспечивают механизм настройки поведения for. of циклов.
Итераторы
Объект является итератором, если он умеет обращаться к элементам коллекции по одному за раз, при этом отслеживая своё текущее положение внутри этой последовательности. В JavaScript итератор — это объект, который предоставляет метод next(), возвращающий следующий элемент последовательности. Этот метод возвращает объект с двумя свойствами: done и value.
После создания, объект-итератор может быть явно использован, с помощью вызовов метода next().
function makeIterator(array) var nextIndex = 0; return next: function() return nextIndex array.length ? value: array[nextIndex++], done: false> : done: true>; > > >
После инициализации, метод next() может быть вызван для поочерёдного доступа к парам ключ-значение в объекте:
var it = makeIterator(['yo', 'ya']); console.log(it.next().value); // 'yo' console.log(it.next().value); // 'ya' console.log(it.next().done); // true
Генераторы
В то время как пользовательские итераторы могут быть весьма полезны, при их программировании требуется уделять серьёзное внимание поддержке внутреннего состояния. Генераторы предоставляют мощную альтернативу: они позволяют определить алгоритм перебора, написав единственную функцию, которая умеет поддерживать собственное состояние.
Генераторы — это специальный тип функции, который работает как фабрика итераторов. Функция становится генератором, если содержит один или более yield операторов и использует function* синтаксис.
function* idMaker() var index = 0; while(true) yield index++; > var it = idMaker(); console.log(it.next().value); // 0 console.log(it.next().value); // 1 console.log(it.next().value); // 2 // .
Итерируемые объекты
Объект является итерируемым, если в нем определён способ перебора значений, то есть, например, как значения перебираются в конструкции for..of . Некоторые встроенные типы, такие как Array или Map (en-US), по умолчанию являются итерируемыми, в то время как другие типы, как, например, Object , таковыми не являются.
Чтобы быть итерируемым, объект обязан реализовать метод @@iterator, что означает, что он (или один из объектов выше по цепочке прототипов) обязан иметь свойство с именем Symbol.iterator :
Пользовательские итерируемые объекты
Мы можем создать свои собственные итерируемые объекты так:
var myIterable = > myIterable[Symbol.iterator] = function* () yield 1; yield 2; yield 3; >; [. myIterable] // [1, 2, 3]
Встроенные итерируемые объекты
Объекты String , Array , TypedArray , Map (en-US) и Set являются итерируемыми, потому что их прототипы содержат метод Symbol.iterator .
Синтаксис для итерируемых объектов
Некоторые выражения работают с итерируемыми объектами, например, for-of циклы, spread operator, yield* , и destructuring assignment.
for(let value of ["a", "b", "c"]) console.log(value) > // "a" // "b" // "c" [. "abc"] // ["a", "b", "c"] function* gen() yield* ["a", "b", "c"] > gen().next() // [a, b, c] = new Set(["a", "b", "c"]) a // "a"
Продвинутые генераторы
Генераторы вычисляют результаты своих yield выражений по требованию, что позволяет им эффективно работать с последовательностями с высокой вычислительной сложностью, или даже с бесконечными последовательностями, как продемонстрировано выше.
Метод next() также принимает значение, которое может использоваться для изменения внутреннего состояния генератора. Значение, переданное в next(), будет рассматриваться как результат последнего yield выражения, которое приостановило генератор.
Вот генератор чисел Фибоначчи, использующий next(x) для перезапуска последовательности:
function* fibonacci() var fn1 = 1; var fn2 = 1; while (true) var current = fn2; fn2 = fn1; fn1 = fn1 + current; var reset = yield current; if (reset) fn1 = 1; fn2 = 1; > > > var sequence = fibonacci(); console.log(sequence.next().value); // 1 console.log(sequence.next().value); // 1 console.log(sequence.next().value); // 2 console.log(sequence.next().value); // 3 console.log(sequence.next().value); // 5 console.log(sequence.next().value); // 8 console.log(sequence.next().value); // 13 console.log(sequence.next(true).value); // 1 console.log(sequence.next().value); // 1 console.log(sequence.next().value); // 2 console.log(sequence.next().value); // 3
Примечание: Интересно, что вызов next(undefined) равносилен вызову next() . При этом вызов next() для нового генератора с любым аргументом, кроме undefined, спровоцирует исключение TypeError .
Можно заставить генератор выбросить исключение, вызвав его метод throw() и передав в качестве параметра значение исключения, которое должно быть выброшено. Это исключение будет выброшено из текущего приостановленного контекста генератора так, будто текущий приостановленный yield оператор являлся throw оператором.
Если yield оператор не встречается во время обработки выброшенного исключения, то исключение передаётся выше через вызов throw() , и результатом последующих вызовов next() будет свойство done равное true .
У генераторов есть метод return(value) , который возвращает заданное значение и останавливает работу генератора.
Found a content problem with this page?
This page was last modified on 17 июл. 2023 г. by MDN contributors.