Pull to refresh

CoffeeScript: Подробное руководство по циклам

Reading time11 min
Views31K
CoffeeScript: Подробное руководство по циклам

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

Articles

Change theme settings