Статья представляет собой не исчерпывающее описание языка программирования CoffeeScript, а именно знакомство, обзор некоторых интересных возможностей. Целевая аудитория — те, кто еще не смотрел в сторону CoffeeScript, но так или иначе используют JavaScript в своих проектах.
CoffeeScript — это маленький язык, который транслируется в JavaScript. Его документация умещается на одной странице — coffeescript.org и отличается компактностью и наглядностью. Я даже сомневался в необходимости данной статьи, когда есть такое классное описание «от производителя», но все же рискнул расставить акценты и прояснить некоторые детали.
Введение
Если капнуть немного истории, то с 2009-го года язык писался на Ruby, с 2010 — он пишется на самом же CoffeeScript.
И в Ruby on Rails, начиная с версии 3.1, он «заменил» JavaScript.
По сути CoffeeScript просто синтаксический сахар над JavaScript. А значит, его ценность в том, что он позволяет нагляднее выражать свои мысли и понимать чужие.
JavaScript (читай ECMAScript), конечно, тоже не стоит на месте, развивается. В том числе перенимая некоторые идеи из CoffeeScript.
Но если говорить про кросс-браузерный JavaScript, то лично у меня большие подозрения, что светлое будущее с продвинутым JavaScript наступит скоро. А CoffeeScript уже сейчас позволяет наслаждаться плодами технологического прогресса.
В этом ключе нельзя не упомянуть TypeScript, в определенном смысле, конкурента CoffeeScript. Он позиционируется, как надмножество JavaScript, добавляя новые фичи в язык, во многом отражая будущее JavaScript. С этой позиции он интереснее.
Но у CoffeeScript, есть преимущество, что ему не нужно сохранять совместимость с JavaScript, что, по-моему, дает больше свободы и позволяет сделать язык более выразительным. Так что, как минимум одна заслуживающая внимания альтернатива CoffeeScript есть. Но вернемся к теме.
Трансляция кода
Хорошо, как пользоваться этим вашим CoffeeScript?
Удобнее всего, на мой взгляд, работать с ним, как с модулем node.js. Ставится он проще простого:
npm install -g coffee-script
Создаем две папки, для определенности назовем их lib
и src
.
Создаем файл src/helloWorld.coffee
и напишем, что нибудь на CoffeeScript. Например:
console.log('Hello world')
После этого запускаем транслятор:
coffee --compile --output lib/ src/
В итоге в папке lib
будет лежать файл helloWorld.js
, готовый к выполнению.
Конечно, каждый раз запускать транслятор на каждый чих не интересно. Запуск команды
coffee -o lib/ -cw src/
заставляет следить за всеми изменениями файлов в папке src
и самостоятельно транслировать их в JavaScript-код.
Синтаксис
Функции
Перейдем к самому языку. Напишем простенький код на CoffeeScript:
square = (x) -> x * x
cube = (x) -> square(x) * x
Его JavaScript-эквивалент:
(function() {
var cube, square;
square = function(x) {
return x * x;
};
cube = function(x) {
return square(x) * x;
};
}).call(this);
Здесь мы создаем две функции, вычисляющие квадрат и куб числа соответственно.
Первым делом обратим внимание, что весь код спрятан внутри анонимной функции, которую мы сразу же вызываем.
Этот прием позволяет прятать все локальные переменные внутри функции, не заботясь о том, что они будут засорять глобальную область видимости. Ниже в статье мы будем опускать эту функцию для наглядности.
Далее обратим внимание, что объявления всех локальных переменных var cube, square
вынесено в начало. Что защищает от распространенной ошибки, когда переменная не с того, не с сего стала глобальной из-за того, что банально забыли добавить объявление var
.
Стрелочка ->
заменяет слово function
.
И еще обратите внимание, что нет необходимости добавлять слово return
. Просто к последнему выражению в функции слово оно добавляется автоматически.
Значения параметров по умолчанию
CoffeeScript добавляет значения по умолчанию для параметров функций, чего нет в JavaScript.
Пример на CoffeeScript:
fill = (container, liquid = "coffee") ->
"Filling the #{container} with #{liquid}..."
Эквивалент на JavaScript:
var fill;
fill = function(container, liquid) {
if (liquid == null) {
liquid = "coffee";
}
return "Filling the " + container + " with " + liquid + "...";
};
JavaScript-реализация сводится проверке параметра liquid
на равенство null
или undefined
.
Другая деталь, которую иллюстрирует пример — в качестве выделения блоков используются не фигурные скобки, а отступы, как в Питоне.
Итерация свойств объекта
Другая вещь, которая раздражает в JavaScript очень многословная итерация по свойствам объектов.
Дело в том, что в большинстве случаев при обходе объекта интересуют его собственные свойства, а не свойства прототипа.
А делать каждый раз for
а в нем сразу же проверку hasOwnProperty
немного утомляет.
Решение же в стиле jQuery.each() никто не запрещал, но оно уступает по эффективности дедовскому for
.
Смотрим, как сделать круто:
yearsOld = max: 10, ida: 9, tim: 11
for own child, age of yearsOld
console.log "#{child} is #{age}"
Эквивалент:
var age, child,
__hasProp = {}.hasOwnProperty;
for (child in yearsOld) {
if (!__hasProp.call(yearsOld, child)) continue;
age = yearsOld[child];
console.log("" + child + " is " + age);
}
Приятные мелочи
В JavaScript оператор == ведет себя мягко говоря странно. Гораздо безопаснее использовать ===. Поэтому CoffeeScript преобразует оператор == в ===, оберегая начинающих разработчиков от подстерегающих в JavaScript ловушек. Хотя приходит в голову один случай, когда оператор == все-таки полезен. Это сравнение с null
, которое позволяет проверить null
и undefined
одним махом. В CoffeeScript для этого предназначен оператор ?
. Рассмотрим пример:
alert "I knew it!" if elvis?
И на выходе:
if (typeof elvis !== "undefined" && elvis !== null) {
alert("I knew it!");
}
Классы
Переходим к классам. На всякий случай уточним, что классами будем называть функции-конструкторы объектов.
Рассмотрим пример:
class Animal
constructor: (@name) ->
move: (meters) ->
alert @name + " moved #{meters}m."
class Snake extends Animal
move: ->
alert "Slithering..."
super 5
class Horse extends Animal
move: ->
alert "Galloping..."
super 45
sam = new Snake "Sammy the Python"
tom = new Horse "Tommy the Palomino"
sam.move()
tom.move()
Даже интуитивно можно догадаться, что происходит. Описаны базовый класс Animal
и два его наследника: Snake
и Horse
.
Обратим внимание на класс Animal
. Запись @name
в параметрах конструктора — это удобное сокращение, которое определяет свойство класса name и автоматически ему присваивает значение, передаваемое в конструкторе. В методе move запись @name
— сокращение от this.name
.
В методах move
в подклассах super
вызывает родительский метод с тем же названием. Ведь, и правда, когда мы находимся в дочернем классе, ссылка на родителя бывает нужна только для того, чтобы обратиться к одноименному методу родительского класса. Другие случаи даже в голову не приходят.
Не буду томить и, наконец-то, перейдем к js-варианту наших классов.
var Animal, Horse, Snake, sam, tom, _ref, _ref1,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
Animal = (function() {
function Animal(name) {
this.name = name;
}
Animal.prototype.move = function(meters) {
return alert(this.name + (" moved " + meters + "m."));
};
return Animal;
})();
Snake = (function(_super) {
__extends(Snake, _super);
function Snake() {
_ref = Snake.__super__.constructor.apply(this, arguments);
return _ref;
}
Snake.prototype.move = function() {
alert("Slithering...");
return Snake.__super__.move.call(this, 5);
};
return Snake;
})(Animal);
Horse = (function(_super) {
__extends(Horse, _super);
function Horse() {
_ref1 = Horse.__super__.constructor.apply(this, arguments);
return _ref1;
}
Horse.prototype.move = function() {
alert("Galloping...");
return Horse.__super__.move.call(this, 45);
};
return Horse;
})(Animal);
sam = new Snake("Sammy the Python");
tom = new Horse("Tommy the Palomino");
sam.move();
tom.move();
В основе наследования лежит вариация классической функции extend
.
Реализация достаточно простая. Конечно, если сравнивать с другими JavaScript библиотеками, которые предоставляют удобную кросс-браузерную реализацию классов на чистом JavaScript.
Минус навороченных библиотек в том, что не всегда легко разобраться, как они работают изнутри.
А функция extend
очень хорошо описана во множестве источников, например, здесь javascript.ru/tutorial/object/inheritance#nasledovanie-na-klassah-funkciya-extend.
Эффективность
Еще достаточно важный критерий — эффективность генерируемого кода. Так вот, с этим все в порядке, никаких глупостей я не обнаружил. Функции как положено добавляются не как свойства класса, а в прототип. Также порадовало, что значение по умолчанию свойств класса тоже добавляются в прототип.
Рассмотрим очень простой класс:
class Foo
bar: 10
На выходе имеем JavaScript:
var Foo;
Foo = (function() {
function Foo() {}
Foo.prototype.bar = 10;
return Foo;
})();
Здесь используется, так называемая асимметричность свойств объекта на чтение и на запись.
В реальной жизни значение свойства по умолчанию практически всегда выгодней добавлять в прототип объекта.
Пока нам не понадобится изменить это значение по умолчанию, мы не тратим лишнюю память для каждого объекта определенного класса. Но, допустим, мы решили изменить значение данного свойства так:
obj = new Foo()
obj.bar = 500
Здесь создается персональное свойство bar
у объекта obj
. При этом свойство bar
прототипа объекта obj
по-прежнему равно 10. Все безопасно и эффективно.
Единственное, что может смущать в этом подходе, что при обращении к свойству, которое находится в прототипе, приходится продвигаться по цепочке прототипов. А это дается не бесплатно. Но на современных движках это не существенно, тем более на фоне радикальной оптимизации использования памяти, ну а старые IE-ки, в которых ощущалась деградация, постепенно уходят в небытие.
Назначение обработчиков событий
Другая крутая фича — назначение обработчиков событий для методов объектов. Пример:
Account = (customer, cart) ->
@customer = customer
@cart = cart
$('.shopping_cart').bind 'click', (event) =>
@customer.purchase @cart
Выдача:
var Account;
Account = function(customer, cart) {
var _this = this;
this.customer = customer;
this.cart = cart;
return $('.shopping_cart').bind('click', function(event) {
return _this.customer.purchase(_this.cart);
});
};
Чтобы в качестве обработчика события указать метод этого же объекта на чистом JavaScript, приходится выкручиваться.
Один из самых распространенных способов — создание замыкания. В CoffeeScript этот костыль не нужен. Достаточно функцию обработчика указать не как ->
, а =>
. После этого this
внутри обработчика будет ссылаться на базовый объект.
Интеграция с чистым JavaScript
Если потребуется подключить чистый JavaScript-код, то это также просто сделать:
hi = `function() {
return [document.title, "Hello JavaScript"].join(": ");
}`
На выходе получаем:
var hi;
hi = function() {
return [document.title, "Hello JavaScript"].join(": ");
};
Массивы
Ну и конечно, присутствует много фишек для работы с массивами и объектами. Для иллюстрации рассмотрим одну.
Например, пусть мы хотим получить массив кубов чисел от 1 до 5.
В CoffeeScript достаточно написать:
cubes = (Math.pow(num, 3) for num in [1..5])
В многословном JavaScript получаем:
var cubes, num;
cubes = (function() {
var _i, _results;
_results = [];
for (num = _i = 1; _i <= 5; num = ++_i) {
_results.push(Math.pow(num, 3));
}
return _results;
})();
Заключение
Надеюсь, для знакомства должно хватить. Дальше добро пожаловать на coffeescript.org.
Ну и как положено, несколько выводов.
- Coffee увеличивает выразительность кода, упрощает и ускоряет как первоначальную разработку, так и дальнейшую поддержку кода.
- Обучение очень быстрое (мне хватило пару дней втянуться).
- Удобная поддержка со стороны WebStorm (Для других IDE тоже есть плагины, но про их качество ничего сказать не могу)
- Большое community
- Уберегает особенно начинающих разработчиков от многих ошибок.
Главное — понимать, что генерирует CoffeeScript. Тогда он превращается из лишнего подозрительного слоя абстракции в мощный инструмент.
Автор: ks7it