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

в 20:58, , рубрики: coffeescript, javascript, Веб-разработка, руководство, метки: , ,

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

Также допустимо использование арфиметических и логических операций:

[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);

Для того чтобы вывести список элементов (не индексов) массива нужно задать еще один параметр:

[value for i, value of ['a', 'b', 'c'][0...2]] # a, b

C одной стороны, списочные выражения представляет собой очень эффективный и компактный способ для работы с объектами. С другой стороны, нужно четко представлять какой код будет получен после трансляции в JavaScript.

К примеру, код выше, который выводит список элементов от 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 на сервере, то вам не о чем беспокоится, ели нет, то стоит помнить, что IE8- не поддерживает метод 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 позволяет писать очень компактный и эффективный код.

Условные операторы 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

jQuery и пр.

Скажу сразу, для CoffeeScript не важно какая JavaScript библиотека у вас используется.

Начнем с самой главной функции jQuery.ready()

.ready():

$ -> @

Результат трансляции:

$(function() {
	return this;
});

Не знаю как у вас, но у меня такая запись почти всегда вызывает бодрый смех

Следующий метод jQuery в нашем списке — .each(), который почти эквивалентен стандартному методу .forEach()

$.each:

$.each [1..3], (i) -> i

Результат трансляции:

$.each([1, 2, 3], function(i) {
	return i;
});

ECMASctipt 6

Если вас не интересует будущее развитие стандарта ECMASctipt 6, можете смело пропустить этот раздел

Как вы знаете, в будущем стандарте ECMASctipt 6 планируется имплементировать генераторы, инераторы и списочные выражения.
Firefox уже сейчас поддерживает большую часть драфтового стандарта.

К чему это я?

Дело в том, что будующий синтаксис ES6 практически более чем полностью не совместим с сегоднишним CoffeeScript.

К примеру инструкция for...of, сейчас носит более общий характер нежели это нужно:

[value for key, value of [1,2,3]]

На выходе мы получим следующее извращенство :

var key, value;

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

Автор: monolithed

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js