Змеиный сахар или пишем свой 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 += 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 ]
    

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

    Удачи!
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 13

      +10
      Дополнительным плюсом будет тот факт, что у нас появляются другие возможности итераторов. Например, можно очень просто преобразовать наш range в массив:
      Преобразование в массив это не эксклюзивная фишка генераторов.
      Ничто не мешает преобразовать в массив и итератор из второго примера:
      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
                      }
                  }
              }
          }
      }
      console.log([...range(0, 10)])
      


      Последний пример можно ещё упростить и просто объявить генератор. Вместо
      function range(from, to, step = 1){
          return {
              *[Symbol.iterator](){
                  for(let val = from; val < to; val += step){
                      yield val;
                  }
              }
          }
      }
      можно
      function* range(from, to, step = 1){
          for(let val = from; val < to; val += step){
              yield val;
          }
      }
        0

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

          –2
          Всегда можно ещё проще!

          function* range(from, to, step = 1) {
              while (from < to) {
                  yield from;
                  from += step;
              }
          }
          


          И да, ладно, что нет проверок, что входные данные — это числа. Тут будут проблемы с отрицательным step ещё.
          0

          Надо бы проверить, что все параметры являются числами. Или преобразовать их в числа при помощи унарного плюса.


          Потому что со строками функция range работает совершенно логичным, но не соответствующим своему названию образом.

            +4

            А на Бабеле написать квадратные скобочки с поддержкой двоеточий внутри слабо?

              0
              С удовольствием посмотрим Вашу реализацию :)
              Тем не менее, хотел бы отметить, что синтаксис [1..10] в JS невалиден, поскольку выкинет SyntaxError: Unexpected number из-за того, что это по сути попытка получить атрибут «10» числа «1.» — на это может намекнуть строка «15..toString(2)»
            +1
            Для сравнения приведу питонокод со всем синтаксическим сахаром и без него:

            for i in range(1, 10):
                print(i)
            


            range_iter = range(1, 10).__iter__()
            
            while True:
                try:
                    value = range_iter.__next__()
                    print(value)
                except StopIteration:
                    break
            

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое