Что такое итераторы и как их использовать в JavaScript/TypeScript?
September 08, 2020
Можно ли итерироваться по собственному классу в Typescript? Можно, если у класса есть метод [Symbol.iterator]
, который возвращает итератор. О них и поговорим.
Итераторы были введены в язык с приходом ES6, они используются для итерации по последовательности значений, обычно динамически генерируемой или, иногда, предзаданной коллекции.
Что бы объект мог считаться итератором, он должен иметь метод next()
, который возвращает объект {value, done}
, где value
— следующее значение в последовательности, а done
— булев флаг, показывающий закончились ли уже значения или нет.
Давайте решим классическую задачу с собеседований — напишем итератор, который возвращает последовательность Фибоначчи.
class Fib {
private a = 0
private b = 1;
[Symbol.iterator]() {
return {
next: () => {
const c = this.a + this.b
this.a = this.b
this.b = c
return {
value: this.b,
done: false,
}
},
}
}
}
const fib = new Fib()
for (const n of fib) {
console.log(n)
if (n > 20) {
break
}
}
Что мы сделали?
Создали класс Fib
с методом [Symbol.iterator]
, который возвращает итератор.
Теперь по инстансу этого класса можно итерироваться в цикле, как по обычному массиву.
Инкапсулировали в нём логику получения чисел, мы просто можем забирать из него числа одно за другим, пока нам это нужно. В этом случае мы выйдем из цикла, когда число Фибоначчи превысит 20.
Можно было бы ограничить максимальное значение и внутри класса, достаточно вернуть из функции next
объект с done=false
.
И ещё один любопытный пример. Так как [Symbol.iterator]
- просто метод объекта, можно вызывать его вручную у других объектов. А учитывая, что итератор — просто объект, то в одном классе можно иметь разные методы для итерации по нему.
Напишем список дел, который в основном итераторе возвращает все дела, а в дополнительном методе незавершённые.
class TodoItem {
constructor(public text: string, public done = false) {}
}
class TodoList {
constructor(private readonly items: TodoItem[]) {}
[Symbol.iterator]() {
return this.items[Symbol.iterator]()
}
workInProgress() {
return this.items.filter(value => !value.done)[Symbol.iterator]()
}
}
const todoList = new TodoList([
new TodoItem('task1'),
new TodoItem('task2', true),
new TodoItem('task3'),
])
console.log('All:')
for (const item of todoList) {
console.log(item.text)
}
console.log('Work in progress:')
for (const item of todoList.workInProgress()) {
console.log(item.text)
}
В классе TodoList
мы определили метод для итерации, который просто возвращает итератор внутреннего массива, что бы можно было использовать цикл прямо по объекту, а не по его свойству. Дополнительно создали метод workInProgress
, в котором мы фильтруем массив items
, возвращая итератор полученного массива, в котором только незавершённые задачи.
Вот, по большому счёту, всё, что нужно знать об итераторах. Их используют, чтобы иметь возможность генерировать последовательности и передавать их в обработку, не храня все значения в памяти и получая последующие по запросу. Или просто для удобства, как во втором примере, что бы можно было использовать циклы по объекту.