Как известно, CoffeeScriptпредлагает несколько иной набор управляющих конструкций, нежели JavaScript.
Не смотря на то, что разработчики языка максимально упростили грамматику и дали подробное описание всех инструкций, сделать более или менее нестандартный цикл для многих остается большой сложностью.
В этой статье я попытаюсь максимально подробно рассказать о принципах работы с циклами в CoffeeScript и тесно связанными с ними управляющими конструкциями.
Весь код сопровождается сравнительными примерами на JavaScript.
Инструкция for-in
Начнем с самого простого цикла for:
for (var i = 0; i < 10; i++) {
//...
}
В CoffeeScript он будет записан так:
for i in [0...10]
Для определения количества итераций используются диапазоны.
В нашем случае, диапазон от 0...10 означает: выполнить 10 итераций цикла.
Но как быть если требуется задать условие типа i <= 10?
for i in [0..10]
На первый взляд ничего не изменилось, но если присмотреться, то можно заметить, что в диапазоне одной точкой стало меньше.
В итоге, мы получим следующую запись:
for (var i = 0; i <= 10; i++) {
//...
}
Если начальное значение диапазона больше конечного [10..0], то мы получим обратный цикл с инвертированным результатом:
for (var i = 10; i >= 0; i--) {
//..
}
Хочу заметить, также допустимо использование отрицательных значений диапазона:
for i in [-10..0]
А так, можно заполнить массив отрицательными значениями:
[0..-3]
#[0, -1, -2, -3]
Теперь рассмотрим реальную ситуацию, на примере функции которая, вычисляет факториал числа n:
JavaScript:
var factorial = function(n) {
var result = 1;
for (i = 1; i <= n; i++) {
result *= i;
}
return result;
};
factorial(5) //120
CoffeeScript:
factorial = (n) ->
result = 1
for i in [1..n]
result *= i
result
factorial 5 #120
Как видно из примера выше, код на CoffeeScript более компактный и читабельный по сравнению с JavaScript.
Однако и этот код можно немного упростить:
factorial = (n) ->
result = 1
result *= i for i in [1..n]
result
В этой статье, это не последний пример вычисления факториала, более эффективные способы будет рассмотрены чуть позже.
[...]
Позволю себе немного отступится от темы и упомянуть еще один интересный момент связанный с применением конструкции [...] (slice).
Иногда к чужом коде можно встретить примерно такую конструкцию:
'a,b,c'[''...][0]
Что в конечно счете будет означать следующее:
'a,b,c'.slice('')[0]; //a
На первый взгляд, отличить диапазоны от слайсов довольно сложно. Основных отличий два:
Во-первых, в слайсах можно пропустить одно крайнее значение
[1...]
Здесь мне бы хотелось обратить особое внимание на то, что мы получим после трансляции этого выражения:
var __slice = Array.prototype.slice;
__slice.call(1);
Это может быть удобно, например, для получения списка аргументов функции:
fn = -> [arguments...]
fn [1..3] #0,1,2,3
Хочу заметить, что в CoffeeScript для получения списка аргументов функции есть более безопасный и изящный вариант (splats):
fn = (args...) -> args
fn [1..3] #0,1,2,3
Также допустимо использование арифметических и логических операций:
[1 + 1...]
Во-вторых, перед слайсами допустимо наличие объекта
[1..10][...2] #1,2
В-третьих, в слайсах допустимо использование перечислений
[1,2,3...]
В этом примере выполняется простая операция конкатенации:
[1, 2].concat(Array.prototype.slice.call(3));
//[1,2,3]
Более полезный пример:
list1 = [1,2,3]
list2 = [4,5,6]
[list1, list2 ...] #[1,2,3,4,5,6]
List comprehension
Наиболее яркой синтаксической конструкцией для работы с объектами в CoffeeScript, являются списочные выражения (List comprehension).
Пример того, как можно получить список всех вычислений факториала от 1 до n:
factorial = (n) ->
result = 1
result *= i for i in [1..n]
factorial 5 #1,2,6,24,120
Теперь давайте рассмотрим более интересный пример и выведем список первых пяти членов объекта location:
(i for i of location)[0...5]
# hash, host, hostname, href, pathname
На JavaScript этот код выглядел бы так:
var list = function() {
var result = [];
for (var i in location) {
result.push(i);
}
return result;
}().slice(0, 5);
Для того чтобы вывести список элементов (не индексов) массива нужно задать еще один параметр:
foo = (value for i, value of ['a', 'b', 'c'][0...2]) # [ a, b ]
C одной стороны, списочные выражения представляет собой очень эффективный и компактный способ для работы с объектами. С другой стороны, нужно четко представлять какой код будет получен после трансляции в JavaScript.
К примеру, код выше, который выводит список элементов от 0 до 2, более эффективно можно переписать так:
foo = (value for value in ['a', 'b', 'c'][0...2])
Или так:
['a', 'b', 'c'].filter (value, i) -> i < 2
На этом месте прошу обратить особое внимание на пробел перед между именем метода и открывающей скобкой. Наличие этого пробела обязательно!
Если пропустить пробел, то мы получим следующее:
['a', 'b', 'c'].filter(value, i)(function() {
return i < 2;
});
//ReferenceError: value is not defined!
Теперь, вам наверное интересно узнать почему вариант с методом .filter() оказался наиболее предпочтителен?
Дело в том, что когда мы используем инструкцию for-of, транслятор подставляет более медленный вариант цикла чем требуется, а именно for-in:
Результат трансляции:
var i, value;
[
(function() {
var _ref, _results;
_ref = ['a', 'b', 'c'].slice(0, 2);
_results = [];
for (i in _ref) {
value = _ref[i];
_results.push(value);
}
return _results;
})()
];
Скажем прямо, итоговый код ужасен.
Теперь давайте посмотрим на код полученный при использовании метода filter:
['a', 'b', 'c'].filter(function(value, i) {
return i < 2;
});
Как видите, мы получили идеальный и эффективный код!
Если вы используете CoffeeScript на сервере, то вам не о чем беспокоится, ели нет, то стоит помнить, что IE9- не поддерживает метод filter. Поэтому вы сами должны позаботиться о его наличии!
Оператор then
Как известно, для интерпретации выражений, парсер CoffeeScript анализирует отступы, переводы строк, символы возврата каретки и пр.
Ниже представлен типичный цикл для возведения чисел от 1 до n в степень двойки:
for i in [1...10]
i * i
Для наглядности, мы использовали перевод строки и отступ.
Однако в реальной ситуации, большинство разработчиков предпочтут записать это выражение в одну строчку:
for i in [1...10] then i * i
В инструкциях while, if/else, и switch/when оператор then указывает анализатору на разделение выражений.
Оператор by
До этого момента мы рассматривали только "простые" циклы, сейчас пора поговорить о циклах с пропусками значений в определенный шаг.
Выведем только четные числа от 2 до 10:
alert i for i in [0..10] by 2 #0,2,4,6,8,10
На JavaScript этот код выглядел бы так:
for (var i = 2; i <= 10; i += 2) {
alert(i);
}
Опрератор by применяется к диапазону элементов, в которых можно установить шаг итерации.
Также мы можем работать не только с числами или элементами массива, но и со строками:
[i for i in 'Hello World' by 3] #H,l,W,l
Операторы by и then могут примеятся совместно:
[for i in 'hello world' by 1 then i.toUpperCase()] # H,E,L,L,O, ,W,O,R,L,D
Хотя этот пример немного надуман и в реальной ситуации шаг "в один" слудует упостить, тем не менее совместная работа операторов by-then позволяет писать очень компактный и эффективный код.
Оператор own
В JavaScript довольно часто используется метод .hasOwnProperty(), который в отличии от оператора in не проверяет свойства в цепочке прототипов объекта:
var object = {
foo: 1
};
object.constructor.prototype.bar = 1;
console.log('bar' in object); // true
console.log(object.hasOwnProperty('bar')); // false
Рассмотрим пример использования метод .hasOwnProperty() в теле цикла for-in:
var object = {
foo: 1
};
object.constructor.prototype.toString = function() {
return this.foo;
};
for (i in object) {
if (object.hasOwnProperty(i)) {
console.log(i); //foo
}
}
Несмотря на то, что мы добавили метод .toString() в прототип объекта object, в теле цикла он перечислен не будет. Хотя к нему можно обратиться напрямую:
object.toString() //1
В CoffeeScript для этих целей предусмотрен специальный оператор own:
object = foo: 1
object.constructor::toString = -> @foo
for own i of object
console.log i #foo
Если нужно использовать второй ключ инструкции for-of , то достаточно его указать через запятую, при этом добавлять еще раз оператор own не нужно:
for own key, value of object
console.log '#{key}, #{value}' #foo, 1
Условные операторы if/else
Сейчас мне бы хотелось обратить внимаение на один очень важный момент, который связан с совместным использованием циклов с операторами if/else.
Иногда в JavaScript приложениях мы можем встретить подобный код:
for (var i = 0; i < 10; i++) if (i === 1) break;
Не будем обсуждать и тем более осуждать разработчиков, которые так пишут.
Для нас представляет интерес только как корректно записать выражение в CoffeeScript.
Первое что приходит в голову, сделать так:
for i in [0..10] if i is 1 break # Parse error on line 1: Unexpected 'TERMINATOR'
Прекрасно..., однако согласно правилам лексического анализа CoffeeScriptперед инструкцией if будет обнаружено неожидаемое значение терминала, что приведет к ошибке парсинга!
Из предыдущего материала мы помним, что записать выражение в одну строчку мы можем реализовать с помощью оператора then:
for i in [0..10] then if i is 1 break #Parse error on line 1: Unexpected 'POST_IF'
Однако и это не помогло, мы снова видим ошибку парсинга.
Давайте попробуем разобраться...
Дело в том, что инструкция if подчиняется тем же правилам, что и другие инструкции, для которых возможно применение оператора then. А именно, для того чтобы наше выражение правильно распарсилось нужно после выражения с if добавить еще раз оператор then:
for i in [0..10] then if i is 1 then break
Таким образом мы получим следующий код:
for (i = 0; i <= 10; i++) {
if (i === 1) {
break;
}
}
Иногда бывают ситуации, когда перед циклом нужно проверить выполнение к.л. условия:
if (foo === true) {
for (i = 0; i <= 10; i++) {
if (i === 1) {
break;
}
}
}
Благодаря использованию недетерминированной обработки данных и списочным выражениям, наш код мы можем представить следующим образом:
(if i is 1 then break) for i in [0..10] if foo is on
Обратитие внимание, что то в этом случае мы не стали использовать опрератор then, при этом никаких ошибок парсинга не произошло!
Условный оператор when
Мы уже рассмотрели операторы by и then, настало время поговорить о следующем операторе в нашем списке, а именно об условном операторе when.
И начнем мы пожалуй с коррекции предыдущего примера:
if foo is on then for i in [0..10] when i is 1 then break
В этом случае, кода получился немного больше в чем предыдущем варианте, однако он приобрел куда больше выразительности и смысла.
Давайте рассмотрим еще один пример, как можно вывести порядок чисел от 1 до 10 по модулю натурального числа n:
alert i for i in [1..10] when i % 2 is 0
После трансляции в JavaScript код:
for (i = 1; i <= 10; i++) {
if (i % 2 === 0) {
alert(i);
}
}
Как видите использование оператора when дает нам еще больше возможностей для работы с массивами.
Инструкция for-of
Вы уже видели примеры использования инструкции for-of, когда рассматривали списочные выражения. Теперь давайте более подробно познакомимся с инструкцией for-of, которая наряду с for-in позволяет перебирать свойства объекта.
Давайте сразу проведем сравнительную аналогию с инструкцией for-in в JavaScript:
var object = {
foo: 0,
bar: 1
};
for (var i in object) {
alert(key + " : " + object[i]); //0 : foo, 1 : bar
}
Как видите для получения значения свойств объекта мы использовали следующий синтаксис: object[i].
В CoffeeScript же, все проще, во-первых мы можем получить значение объекта используя списочные выражения:
value for key, value of {foo: 1, bar: 2}
Во-вторых, для более сложных выражений мы можем применить более разверную нотацию с применением уже знакомых нам операторов:
for key, value of {foo: 1, bar: 2}
if key is 'foo' and value is 1 then break
В JavaScript тот же результат можно получить так:
var object = {
foo: 1,
bar: 2
};
for (key in object) {
if (key === 'foo' && object[i] === 1) {
break;
}
}
Еще один пример эффективного использования for-in:
(if value is 1 then alert "#{key} : #{value}") for key, value of document
#ELEMENT_NODE : 1,
#DOCUMENT_POSITION_DISCONNECTED : 1
Напомню, что самым эффективным способом получения списка свойств объекта, явлется метод keys():
Object.keys obj {foo: 1, bar: 2} # foo, bar
Для того чтобы получить значения свойств, метод keys() нужно использовать совместно с методом map():
object =
foo: 1
bar: 2
Object.keys(object).map (key) -> object[key]; # 1, 2
Инструкция while
По мимо инструкций for-of/in в CoffeeScript также реализована инструкция while.
Когда мы рассматривали инструкцию for-in, я обещал показать еще более эффективный способ вычисления фактриала числа n, время как раз подходящее:
factorial = (n) ->
result = 1
while n then result *= n--
result
На вскидку хочу добавить, что самое элегантное решение вычисления факториала следующее:
factorial = (n) -> !n and 1 or n * factorial n - 1
Инструкция loop
На этой инструкции мы не будем долго останавливаться, потому что единственное ее назначение это создание бесконечного цикла:
loop then break if foo is bar
Рельтат трансляции:
while (true) {
if (foo === bar) {
break;
}
}
Инструкция until
Инструкция until аналогична инструкуции while, за одиним исключением, что в выражение добавляется отрицание.
Это может быть полезно например для нахождения следующей позиции набора символов в строке:
expr = /foo/g;
alert "#{array[0]} : #{expr.lastIndex}" until (array = expr.exec('foofoo')) is null
Рельтат трансляции:
var array, expr;
expr = /foo*/g;
while ((array = expr.exec('foofoo')) !== null) {
alert("" + array[0] + " : " + expr.lastIndex);
}
//foo : 3, foo : 6
Как видно из примера, цикл выполняется до тех пока результат выражения не станет равен нулю.
Инструкция do-while
Скажу сразу, что в CoffeeScript отсутствует реализация инструкции do-while. Однко с помощью нехитрых манипуляций эмитировать ее частичное поведение можно с помощью инструкции loop:
loop
#...
break if foo()
Методы массивов (filter, forEach, map и пр.)
Как известно в CoffeeScript доступны абсолютно все те же методы, что и в JavaSctipt.
Разбирать всю эту группу методов нет смысла, рассмотрим лишь общий принцип работы на примере метода map().
Создадим массив из трех элементов и возведем каждый из них в квадрат:
[1..3].map (i) -> i * i
Рельтат трансляции:
[1, 2, 3].map(function(i) {
return i * i;
});
Рассмотрим еще один пример:
['foo', 'bar'].map (value, i) -> "#{value} : #{i}"
#foo : 0, bar : 1
Вторым аргументом, метод map принимает контекст вызова:
var object = new function() {
return [0].map(function() {
return this
});
};
// [Window map]
Как видите this внутри map указывает на Window, чтобы сменить контекст вызова, сделать это нужно явно:
var object = new function() {
return [0].map(function() {
return this;
}, this);
};
// [Object {}]
В CoffeeScript для этой цели предназначен специальный оператор =>:
object = new -> [0].map (i) => @
Рельтат трансляции:
var object = new function() {
var _this = this;
return [0].map(function() {
return _this;
}, this);
};
Иными словами используйте эти методы массивов максимально, где это только возможно.
Кроссбраузерную реализация этих методов я разместил на github'e
Реальный пример использования методов map и filter в CoffeeScript, также можно посмотреть в одном из моих проектов на github'e
Инструкция do / Замыкания
Как известно, в JavaScript активно используются замыкания, при этом CoffeeScript тоже не лишает нас этого удовольствия.
Для создания анонимной самозавязывающейся функции в CoffeeScript используется инструкция do, котороая принимает произвольное число аргументов.
Рассмотрим пример:
array = [];
i = 2
while i-- then array[i] = do (i) -> -> i
array[0]() #0
array[1]() #1
Суть этого кода сводится к заполнению массива. При этом, элементы массива содержат не элементарные значения, а функции, каждая из которых возвращает индекс своего элемента.
На JavaScript код выглядел бы так:
var array = [],
i = 2;
while (i--) {
array[i] = function(i) {
return function() {
return i;
};
}(i);
}
array[0]() //0
array[1]() //1
Вложенные инструкции
Вложенные инструкции особо ничем не отличаются от других инструкций и подчиняются тем же правилам:
Для примера, заполним массив парными элементами от 1 до 3:
list = []
for i in [0..2]
for j in [1..2]
list.push i
list # [0,0,1,1,2,2]
Как видите нет ничего сложного!
Возможно у вас появится желание записать это в одну строчку. Что же, давайте теперь попробуем упростить запись:
list = []
for i in [0..2] then for j in [1..2] then list.push i
PS: лично я бы, не стал использовать такую запись, однако, вы должны знать, что так тоже допустимо писать, ведь рано или поздно вам придется работать с чужим кодом.
А что если нужно добавить перед вторым циклом какое-то выражение?
В качестве примера выведем три пары элементов от 0-3:
list = []
for i in [0..2]
list.push i
for j in [1..1]
list.push i
list #[0,0,1,1,2,2]
Это правильный вариант и особо улучшать здесь нечего. Записать все в одну строчку тоже не получится, потому что перед вторым циклом требуется явная идентация. А вот тело цикла можно записать в короткой нотации.
list = []
for i in [0..2]
list.push i
list.push i for j in [1..1]
list #[0,1,2,3]
В третьей строке можно использовать как префиксную так и постфиксную форму записи.
ECMASctipt 6
Как вы знаете, в будущем стандарте ECMASctipt 6 планируется имплементировать генераторы, итераторы и возможно списочные выражения. А Firefox уже сейчас поддерживает большую часть драфтового стандарта. И дело в том, что будущий синтаксис ES6 практически более чем полностью не совместим с сегодняшним CoffeeScript.
К примеру инструкция for...of, сейчас носит более общий характер нежели это нужно:
[value for key, value of [1,2,3]]
На выходе мы получим следующее
var key, value;
[
(function() {
var _ref, _results;
_ref = [1, 2, 3];
_results = [];
for (key in _ref) {
value = _ref[key];
_results.push(value);
}
return _results;
})()
];
//[1, 2, 3]
Будущий стандарт дает возможность использовать итерацию через объекты, куда проще:
[for i of [1,2,3]]
Здорово, не правда ли?
Также будет доступны генераторы выражений:
[i * 2 for each (i in [1, 2, 3])];
//2,4,6
Возможным станет и такая запись:
[i * 2 for each (i in [1, 2, 3]) if (i % 2 == 0)];
//2
Станут доступными итераторы:
var object = {
a: 1,
b: 2
};
var it = Iterator(lang);
var pair = it.next(); //[a, 1]
pair = it.next(); //[b, 2]
Итераторы также можно применять совместно с генераторами выражений:
var it = Iterator([1,2,3]);
[i * 2 for (i in it)]; //1, 4, 6
С выходом нового стандарта и многие фишки из CoffeScript перестанут быть таковыми, а разработчикам ядра очевидно предстоит очень много работы, чтобы чтобы удержать «сахарные» позиции. Пожелаем им удачи.