Небольшие полезности для CoffeeScript разработчика

    CoffeeScript поистине удивительный язык, который позволяет взглянуть на JavaScript с совершенно иной и намного более притягательной стороны. Давным давно, когда я только начинал заниматься фронт-эндом — меня буквально силками заставляли писать именно на нём (корпоративный стандарт), сейчас же я не могу писать на языке оригинала.

    За время (уже более двух лет), проведённое за штурвалом этого препроцессора, накопилось довольно много «хотелок», которые желалось бы увидеть в JS (благо есть опыт общения с другими языками), некоторые из которых мне удалось претворить в жизнь, местами коряво, но как есть — CoffeeScript позволяет почти что придумывать свои конструкции. Об этих «хотелках» я и хочу поведать в статье, прошу под кат.

    Пространства имён


    Первое что хотелось бы видеть — это пространства имён. Какому разработчику не хочется видеть аккуратно расположенный код не только в файловой системе, но и в иерархии классов? Гугление подсказало несколько решений, и самым элегантным было использование литералов объектов в качестве имён пространств:

    namespace MyApplication:Some:
      class Any
        # Этот класс будет располагаться в window.MyApplication.Some.Any
    
    namespace global:
      class Some
        # Этот класс будет лежать в window.Some
    

    Происходит следующее: Мы вызываем функцию namespace и отправляем туда объект {MyApplication:{Some: сам_класс}}

    Единственное «но» — подобное решение обладало некоторыми проблемами, а именно — требовалось строгая последовательность подключения файлов (вначале с пространством MyApplication, затем с MyApplication.Some и т.д.), а так же хардкор регулярками с получением имени класса. Я постарался избавиться от их фатальных недостатков, и в результате получился такой код:

    window.namespace = ->
      args   = arguments[0]
      target = global || window
      loop
        for subpackage, obj of args
          target = target[subpackage] or= {}
          args   = obj
        break unless typeof args is 'object'
    
      Class  = args
      target = window if arguments[0].hasOwnProperty 'global'
      name   = if !Class.name? # IE Fix
        args.toString().match(/^function\s(\w+)\(/)[1]
      else
        Class.name
      proto        = target[name] or undefined
      target[name] = Class
    
      if proto?
        for i of proto
          target[name][i] = proto[i]
    

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

    Импорт классов\функций


    Конечно же, хотелось бы наличия операторов, вроде using, use, import, но увы — реализовать подобные можно лишь на уровне самого препроцессора, но никак не на уровне языка. Но в случае CoffeeScript, оказывается, есть некоторые свойства самого языка, позволяющие реализовать импорт почти что красиво:

    {Any} = MyApplication.Some # Импортировать MyApplication.Some.Any под именем Any
    {Any: Test} = MyApplication.Some # Импортировать MyApplication.Some.Any под именем Test
    

    эти операции аналогичны, допустим use в php:

    use MyApplication\Some\Any;
    use MyApplication\Some\Any as Test;
    


    Подобное поведение конечно же задокументировано (пункт: «Destructuring Assignment» пример №3 в офф. документации), но, если честно, я очень сильно удивился, когда заметил подобную конструкцию в чьём-то коде. Когда читал документацию — просто не заметил этого.

    Константы


    Есть несколько популярных вариантов реализации констант в CoffeeScript.

    Вариант 1


    class Some
      @MY_DEFINE = 42
    
      @getVar: ->
        @MY_DEFINE
    
      getVar: ->
        @contructor.MY_DEFINE # или Some.MY_DEFINE
    

    В таком варианте есть как плюсы, так и минусы:
    + Константу (теоретическую, т.к. это просто переменная) можно получить из любого места, обратившись к Some.MY_DEFINE
    — Использовать не всегда удобно
    — Это переменная (т.е. можно перезаписать), а использование __defineGetter__ и аналогичных конструкций создания геттеров — только усложнит чтение.

    Вариант номер два


    class Some
      MY_DEFINE = 42
    
      @getVar: ->
        MY_DEFINE
    
      getVar: ->
        MY_DEFINE
    

    плюсы и минусы:
    + Внутри класса выглядит великолепно, читаемо и пользоваться очень удобно
    — Одноразовая, т.к. ограничена только текущей (и вложенными) областью видимости, за пределами класса получить её значение невозможно
    — Невозможно реализовать геттеры\сеттеры, чтобы оградить значение от изменений

    Есть ещё варианты, вроде использования джаваскриптовых const MY_DEFINE = 42 в обратных одинарных кавычках (там где буква «Ё»), или добавления функций в прототип Function, которые будут регистрировать константы с помощью геттеров\сеттеров, но это малопопулярные техники, так что я о них промолчу и лучше предложу свой вариант (чуть более приближенный к реальности):

    Третий вариант


    class Ajax
      define AJAX_UNSENT:   0
      define AJAX_OPENED:   1
      define AJAX_HEADERS_RECEIVED: 2
      define AJAX_LOADING:  3
      define AJAX_READY:    4
    
      request: ->
        # некоторый код
        if xhr.status is AJAX_READY
          # делать что-нибудь
    

    Реализация самой функции:

    window.define = (args) ->
      for name, val of args
        continue if window[name]?
        do (name, val) ->
          unless window.__defineGetter__? # IE Fix
            return window[name] = val
    
          window.__defineGetter__ name, -> val
          window.__defineSetter__ name, -> throw new Error "Can not redeclare define #{name}. Define already exists."
    
    
    # Ну и можно добавить функцию проверки на существование
    window.defined = (name) ->
      return window.__lookupGetter__(name)? && window[name]?
    

    Происходит следующее: Вызываем функцию, куда передаём объект. Далее мы в window регистрируем геттер, который будет возвращать нужное значение и сеттер, который блокирует возможность перезаписи константы.
    Плюсы:
    + Внутри классов также выглядит очень красиво и читаемо
    + Можно получить в любом месте кода
    — Висит в window — глобалсы никогда не были хорошим решением, но я не думаю, что это так уж существенно в свете возможных выигрышей по читаемости и удобству кода, а проблемы коллизий решаются обычными префиксами.

    Приватные переменные


    Этот пример только в качестве бонуса и просто как идея. Мне самому подобная реализация не особо нравится, но пока лучшего я придумать не смог:

    class Some
      test = $private 'test'
    
      constructor: ->
        @[test] = 23
    
    console.log(new Some)
    

    Что же тут происходит: Внутри класса мы объявляем var переменную test, в качестве значения которой будет строка [private test] (её возвращает функция $private). Далее мы просто используем эту переменную как имя для нашей реальной переменной. А так как имя у нас начинается с невидимого символа — доступ к переменной получить довольно сложно, особенно если префикс будет генерироваться из случайных невидимых символов.

    Реализация:

    window.$private = (name) ->
      unless defined 'ZERO_WIDTH_SPACE'
        define ZERO_WIDTH_SPACE: '​' # Тут пробел с нулевой шириной, в качестве значения
      return "#{ZERO_WIDTH_SPACE}[private #{name}]"
    

    В результате:
    + Реальные приватные переменные
    + В классах довольно объёмного размера может очень сильно помочь, т.к. очищает интерфейс этого класса от лишних методов\свойств, которые не стоит делать публичными
    — Очень громоздко и некрасиво
    — Неудобно пользоваться
    — Приходится добавлять префикс "$", т.к. это ключевое слово и оно зарезервировано

    Имена классов


    Иногда хочется получить имя класса или различать массив и объект. Такие ситуации встречаются довольно часто, и для таких случаев я припрятал для себя небольшую функцию:

    nameof [] # 'Array'
    nameof {} # 'Object'
    nameof SomeClass # 'SomeClass'
    

    Сама реализация выглядит вот так:

    window.nameof = (cls) ->
      if typeof cls is 'object'
        cls = cls.constructor
      else if typeof cls isnt 'function'
        return typeof cls
    
      name = unless cls.name?
        cls.toString().match(/function\s(\w+)\(/)[1]
      else
        cls.name
      return name
    


    Абстрактные методы


    Наверное, самое элегантное и простое решение:

    class Some
      abstractMethod: abstract
    
    class Any extends Some
    
    (new Any).abstractMethod() # Error 'Can not call abstract method'
    
    # Но зато
    class Any2 extends Some
      abstractMethod: -> console.log 42
    
    (new Any).abstractMethod() # 42
    

    Реализация элементарнейшая и очевиднейшая:

    window.abstract = -> throw new Error 'Can not call abstract method'
    


    Эпилог


    Я привёл несколько интересных примеров, как можно улучшить читаемость (по моему мнению) и удобство использования кода парой-тройкой небольших функций. Некоторые из них вполне могут сгодиться для организации серьёзного кода, некоторые, как небольшие хелперы, но в целом они выполняют одну роль — добавляют некоторые языковые конструкции в сам язык. Именно по этому я и хочу предостеречь тебя, читатель. Может быть всё это хорошо и красиво, и даже на практике удобно, но сахар — хорош в меру, не стоит сильно злоупотреблять подобными безумными функциями.

    P.S. Прошу прощения за хаб «JavaScript», увы, по CoffeeScript отсутствует.

    UPD: Добавил откат констант для ИЕ, от греха подальше =)

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

    Что-нибудь из этого было полезно?
    Поделиться публикацией
    Комментарии 12
      –7
      Не очень люблю кофескрипт и не пишу на нем, но с нейсмпейсами, импортами и абстрактными методам — очень остроумно.
        –2
        Ну в случае абстрактных методов — они должны выкидывать ошибки при отсутствии их реализации в ребёнке, в моём случае — только при вызове, что не совсем отвечает требованиям. Но в качестве простенькой «защиты» от ошибок реализации потомков — вполне сойдёт, наверное.
          0
          Прямо питоновский raise NotImplementedError.
        +2
        «Константы». __define(Getter|Setter)__ — нестандартные свойства — IE11+, явно не для продакшна. К слову, на нем JS уже поддерживает ключевое слово const (ES6). Уж лучше, хотя бы defineProperty (ES5, IE9+). А еще лучше дурью не маяться.
        Имена классов. Какой-то совсем упоротый подход. Общепринятый способ получения внутреннего класса — Object::toString.call(it).slice 8, -1, а на пользовательские после сжатия кода и ваше решение работать не будет, тут разве что поможет свойство — подсказка (аналог Symbol.toStringTag).
        Странное у вас представление о приватности. new Some()['[private test]'] доступно и снаружи. Лучше в сторону символов (Symbol) и их полифилов посмотрите, не совсем приватные свойства, но куда честнее ваших.
          0
          1) В случае __define(Getter|Setter)__ — можно сделать откат в ИЕ до обычных переменных (да, не подумал про него, когда писал), смысл ведь в том, что бы разработчик смог получить значение оной переменной и получить ошибку, если попробует переопределить её.
          2) Упоротый или нет, но можно получить через name, а откат для ИЕ выдран из оригинала (там ссылочка). К слову — минификация никак не повлияет, т.к. toString для функции вернёт всегда с отступами и прочим. Но можно заменить на Ваш вариант, ничего страшного.
          3) new Some()['[private test]'] не доступно, т.к. содержит невидимые чары, которые можно воспроизвести лишь используя магию и num-клавиатуру, а оно надо? Ведь главное — красиво и наглядно показать разработчику, что к ним обращаться не стоит. По поводу Symbol — есть ссылочка? Гугл и can i use посылают меня куда-то не туда…
            0
            1) Поправил (на всякий случай)
            3) Нашёл, ES6: developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Symbol На что сейчас даже рассчитывать не стоит.
              +1
              минификация никак не повлияет

              Функция function SomeClass(){/* ... */} будет сжата до какой-нибудь function vE(){}, что и будет содержаться в toString. name тоже нестандартное свойство и не поддерживается IE. Совсем.

              не доступно, т.к. содержит невидимые чары

              Эти «невидимые» чары легко подбираются и постоянны для всех «приватных» свойств. «Приватные» свойства участвуют в переборе объекта через for-in и получаются через Object.keys.

              По поводу Symbol — есть ссылочка?

              Символы — новый тип данных в ES6, пример использования полностью аналогичен вашему. Символы уникальны — единственный способ получить ключ вне IIFE — предназначенный для рефлексии метод Object.getOwnPropertySymbols, символы не участвуют в обходе через for-in и недоступны через Object.keys. Доступны в V8 (с 38 Chrome — без флажка) и FF (с 33). Обещают в IE12. Полный полифил не возможен — нельзя добавить новый тип данных, но базовые возможности (уникальные неперечислимые ключи) легко реализовать генерацией уникальной строки и сеттерами в прототипе Object (эта часть решения сомнительна).
                0
                Благодарю за подробности, буду знать, но думаю поправлять статью (в связи с новыми подробностями) уже нет смысла — очень многое придётся переписывать.

                В любом случае из всего моего перечисленного — только namespace's претендуют на роль использования в продакшене (ну может ещё дефайны), остальное — скорее размышления на тему, как можно ещё улучшить язык и чего бы ещё туда всунуть без лишних хлопот.
            0
            Что только люди ни придумают, чтобы не использовать модули.
              0
              Кофе — ООП направленный язык, так что вполне логично предполагать, что классы доступны глобально в своём пространстве имён, без всяких require.

              Причём, по моему скромному мнению — пространства красивее, нежели модули. Хотя может просто это сказывается совершенно неуместная любовь к Java, C#, PHP, etc…
                +2
                Во-первых, JavaScript — язык с прототипным наследованием. Объекты наследуются от объектов. CoffeeScript этот факт никак не меняет. Директива class — не более чем синтаксический сахар, преобразующийся при компиляции в подобие Query.extend().

                Во-вторых, глобального пространства имен в JavaScript нет. То, что принято называть глобальными переменными, на самом деле является свойствами корневого объекта (в браузере это объект window). Как пользователь CoffeeScript вы должны это знать, ведь единственный тамошний способ объявить «глобальную переменную» — это window.foo = 'bar'.

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

                Использование модулей обе проблемы решает кардинально. Рассмотрим для примера модули CommonJS.

                У CommonJS два источника модулей: пакетный менеджер NPM и локальные файлы. В первом случае зависимости подключаются по названию пакета (require 'foo'), во втором — по относительному пути (require './foo'). Ни в том, ни в другом случае, вы не сможете создать два пакета, использующие одинаковое имя.

                При использовании CommonJS вы дробите код на множество мелких файлов, каждый из которых соответствует одному модулю, который в свою очередь решает какую-то одну конкретную задачу. Когда один модуль нуждается в другом модуле, вы просто берете его в локальную переменную и используете.

                $ = require 'jquery'
                
                $ '.foo'
                  .bar()
                

                Поверьте, это гораздо проще, удобнее и красивее, чем пространства имен.

                Что касается разрешения зависимостей, то изначально модули ничего не знают друг о друге. У вас есть корневой файл приложения, что-то вроде application.js.coffee, и из этого файла вы можете использовать любые модули. Вам не нужно заботиться о порядке подключения файлов, потому что вам не нужно подключать файлы вручную. За вас это сделает Browserify.

                PS По поводу приватных переменных и в особенности приватных методов разделяю вашу боль. Но JS это такой язык, кишками наружу. Ничего не поделаешь, остается толко использовать костыли вроде jQuery UI Widget Factory. Кстати, горячо рекоммендую.
                  0
                  В-третьих, существуют две смежные проблемы, проистекающие из вышесказанного: коллизии имен и разрешение зависимостей. Ваш подход облегачет первую пролему, не устраняя ее, и никак не помогает со второй.

                  ну можно создать такой же синтаксический сахар для подгрузки классов. Вместо:
                  {Some} = Any.Ololo
                  писать:
                  Some = import Any:Ololo:Some
                  Получается тоже самое, что и модули. Осталось только не пихать всё в window, а хранить внутри — будет копия.

                  На счёт приватных переменных — есть одна общепринятая практика (это так, к слову) — называть методы\переменные с нижнего подчёркивания. Но да, это не особо удобно, боль.

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

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