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: Добавил откат констант для ИЕ, от греха подальше =)
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Что-нибудь из этого было полезно?
31.31%Пространства имён62
20.71%Импорт пространств41
11.11%Константы22
9.09%Приватные переменные18
12.12%Имена классов24
14.14%Абстрактные методы28
20.71%Это всё от лукавого, наоборот только путает41
48.99%Не использую CoffeeScript97
Проголосовали 198 пользователей. Воздержались 109 пользователей.
