Pull to refresh

Змеиный сахар или пишем свой range в JavaScript

Reading time 2 min
Views 5.8K
Многие любят 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 += step) % 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 ]

В заключении хочу сказать, что изначально планировал написать статью со списком подобных реализаций аналогий, но в голову ничего больше не пришло. И, если вдруг, появятся идеи, то я продолжу это как цикл.

Удачи!
Tags:
Hubs:
+14
Comments 13
Comments Comments 13

Articles