Змеиный сахар или пишем свой range в JavaScript
Многие любят Python… Новички восщищаются отсутствием точек с запятой, а продвинутые радуются действительной простотой. Сегодня речь и пойдет о том, как в JavaScript реализовать подобие той самой простоты Python, а конкретно функцию range
В Python по функции range можно итерировать или, например, преобразовать в массив — list (range (begin, end)).
Но вопрос в том, можно ли мощностями JavaScript создать что-то подобное и при этом, чтобы решение выглядело нативным и простым?
Первое, что приходит в голову — написать подобный класс:
function range(from, to, step = 1){
this.current = from
this.to = to
this.step = step
this.next = () => this.current++ % to
}
Да, я написал, что next возвращает обрезанное значение, но это тоже проблема, которую нужно придумать как решать — выбрасывать ошибку при переходе за максимум или делать что-то другое?
В общем, подобное решение совсем не добавит удобств и проще будет написать стандартный for, можно ещё, например, добавить функцию end и после каждой итерации проверять, не вышли ли мы за границу, но это решение нет смысла развивать.
В ES6 появился новый примитивный тип данных — Symbol. Он открывает перед нами двери в метапрограммирование.
Чтобы решить нашу проблему с range мы будем использовать Symbol.iterator. Он дает возможность создавать объект-итератор по определенному протоколу.
Для того, чтобы объект стал итератором, в нем нужно определить функцию [Symbol.iterator](), но, так как мы хотим создать общую функцию, а не класс, то у нас будет функция, возвращающая объект-итератор.
Вот как это выглядит:
function range(from, to, step = 1){
return {
[Symbol.iterator](){
return {
current: from,
to: to,
from: from,
step,
next(){
const it = { done: this.current >= this.to, value: this.current }
this.current += this.step
return it
}
}
}
}
}
Что происходит? У нас есть функция range, она возвращает, как раз таки, наш объект-итератор, в котором функция [Symbol.iterator]() возвращает объект. Самое важное, чтобы этот объект содержал функцию next иначе объект не будет итерируемым и вылезет ошибка при попытке его использования. Функция next должна (!) возвращать объект со свойствами done и value, где done (bool) сигнализирует об окончании итерирования, а value содержит текущее значение.
В принципе всё работает, и уже можно написать что-то питоно-подобное:
for(let i of range(0, 10, 2))
console.log(i)
Вывод:
0
2
4
6
8
На этом можно было бы остановиться… Но можно написать более элегантное решение с использованием функций-генераторов:
function range(from, to, step = 1){
return {
*[Symbol.iterator](){
for(let val = from; val < to; val += step){
yield val;
}
}
}
}
Дополнительным плюсом будет тот факт, что у нас появляются другие возможности итераторов. Например, можно очень просто преобразовать наш range в массив:
[...range(0, 10, 2)]
// [ 0, 2, 4, 6, 8 ]
В заключении хочу сказать, что изначально планировал написать статью со списком подобных реализаций аналогий, но в голову ничего больше не пришло. И, если вдруг, появятся идеи, то я продолжу это как цикл.
Удачи)