Знакомство с CoffeeScript

в 15:18, , рубрики: coffeescript, javascript, Веб-разработка, метки: ,

Статья представляет собой не исчерпывающее описание языка программирования 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

Источник

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


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