Pull to refresh

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

Website development *JavaScript *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: Добавил откат констант для ИЕ, от греха подальше =)
Only registered users can participate in poll. Log in, please.
Что-нибудь из этого было полезно?
31.31% Пространства имён 62
20.71% Импорт пространств 41
11.11% Константы 22
9.09% Приватные переменные 18
12.12% Имена классов 24
14.14% Абстрактные методы 28
20.71% Это всё от лукавого, наоборот только путает 41
48.99% Не использую CoffeeScript 97
198 users voted. 109 users abstained.
Tags:
Hubs:
Total votes 37: ↑28 and ↓9 +19
Views 12K
Comments 12
Comments Comments 12