Небольшие полезности для 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) → 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 нету.