
Как известно, 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 перестанут быть таковыми, а разработчикам ядра очевидно предстоит очень много работы, чтобы чтобы удержать «сахарные» позиции. Пожелаем им удачи.
