На приверженцев объектно-ориентированных языков классы в JavaScript действуют как чеснок на Дракулу (хотя вряд ли вы стали бы читать эту книгу, если бы придерживались таких взглядов). Однако классы в JavaScript столь же полезны, сколь и в других языках, а CoffeeScript сильно упрощает работу с ними.
За кулисами CoffeeScript использует JavaScript-прототипы для создания классов, добавляя немного синтаксического сахара для наследования статичных свойств и определении контекста исполнения. Всё, что требуется написать разработчику — это следующий код:
class Animal
В приведенном выше примере Animal - это название класса и полученной переменной, которую в дальнейшем можно использовать для создания экземпляров объекта. CoffeeScript использует функции-конструкторы, что позволяет создавать объекты с помощью оператора new
.
animal = new Animal
Создать конструктор (функцию, вызываемую при создании экземпляра объекта) очень просто - создайте функцию с именем constructor
, аналогично методу initialize
в Ruby или __init__
в Python:
class Animal
constructor: (name) ->
@name = name
Более того, CoffeeScript предоставляет элегантную реализацию распространенного способа установки исходных значений полей объекта. Префикс @
автоматически превращает переменную в свойство экземпляра класса. На самом деле, этот синтаксический сахар работает даже для обыкновенных функций за пределами класса. Приведённый ниже пример эквивалентен предыдущему:
class Animal
constructor: (@name) ->
Таким образом переменные, передаваемые при создании объекта, передаются в конструктор:
animal = new Animal("Parrot")
alert "Animal is a #{animal.name}
Добавление классу дополнительных свойств является довольно простым: оно полностью совпадает с добавление свойств в обычный объект. Нужно только убедиться, что свойства добавлены в тело класса.
class Animal
price: 5
sell: (customer) ->
animal = new Animal
animal.sell(new Customer)
В JavaScript часто приходится прибегать к смене контекста. В главе «Синтаксис» мы уже рассказывали о том, как привязать значение this к конкретному контексту, используя «жирную» стрелку: =>. Используя её мы можем быть уверены, что функция будет выполняться в том контексте, в котором была создана, независимо от контекста, в котором она вызывается. CoffeeScript расширяет поддержку «жирной» стрелки для классов, и поэтому используя её для метода класса можно не беспокоиться о контексте: this
всегда будет являться текущим экземпляром объекта.
class Animal
price: 5
sell: =>
alert "Give me #{@price} shillings!"
animal = new Animal
$("#sell").click(animal.sell)
В примере выше видно, что этот приём особенно полезен в коллбэках событий. Если бы мы использовали тонкую стрелку, метод sell() был бы вызван в контексте элемента #sell. В нашем же случае используется корректный контекст и this.price
равно 5.
Как насчёт статичных свойств? В CoffeeScript this
внутри класса ссылается на сам объект класса. Другими словами, вы можете задать статичные свойства класса, установив их непосредственно через this.
class Animal
this.find = (name) ->
Animal.find("Parrot")
Вы уже знаете, что в CoffeeScript существует аналог this
- символ @
, который позволяет нам писать статические свойства еще более кратко:
class Animal
@find: (name) ->
Animal.find("Parrot")
Реализация классов не была бы полной без наследования в какой-либо форме, и CoffeeScript, естественно, реализовал его. Вы можете унаследовать класс от другого, используя ключевое слово extends
. В примере ниже Parrot наследуется от Animal, получая все его свойства и методы.
class Animal
constructor: (@name) ->
alive: ->
false
class Parrot extends Animal
constructor: ->
super("Parrot")
dead: ->
not @alive()
В примере выше, как вы могли заметить, мы используем ключевое слово super(). "За кулисами" происходит вызов функции прототипа родительского класса, выполненный в текущем контексте. В данном случае это будет Parrot.__super__.constructor.call(this, "Parrot");
. На практике, эффект будет точно такой же, как при вызове super
в Ruby или Python, вызовется переопределенная унаследованная функция.
Если вы не переопределили конструктор, по умолчанию CoffeeScript вызовет конструктор родителя в момент создания экземпляра класса.
CoffeeScript использует прототипное наследование, чтобы автоматически унаследовать все свойства экземпляра класса. Это гарантирует динамичность классов. Даже если вы добавите свойства в родительский класс после того как был создан наследник, свойство распространится на все унаследованные классы.
class Animal
constructor: (@name) ->
class Parrot extends Animal
Animal::rip = true
parrot = new Parrot("Macaw")
alert("This parrot is no more") if parrot.rip
Хотя стоит отметить, что статические свойства копируются в дочерние классы, вместо наследования через прототипы, как это делается со свойствами экземпляров классов. Так получилось из-за деталей реализации прототипной архитектуры в Javascript, и обойти эту проблему довольно трудно.
CoffeeScript не поддерживает примеси из коробки, но вы можете довольно легко реализовать их самостоятельно. Например, две функции, extend()
и include()
, которые добавляют свойства классу и экземплярам класса соответственно.
extend = (obj, mixin) ->
obj[name] = method for name, method of mixin
obj
include = (klass, mixin) ->
extend klass.prototype, mixin
# Usage
include Parrot,
isDeceased: true
(new Parrot).isDeceased
Примеси - это отличный паттерн, который пригодится для использования общей логики между модулями в тот момент, когда вы не можете использовать наследование. Преимущество примесей в том, что вы можете включить в объект логику нескольких объектов, тогда как унаследоваться можно только от одного.
Применение примесей выглядят довольно изящно, но они не достаточно объектно-ориентированы. Но мы можем интегрировать примеси в CoffeeScript классы. Попробуем определить класс, именуемый Module, от которого мы сможем унаследоваться для поддержки примесей. Module будет иметь два статических метода, @extend() и @include(), которые будут использоваться для расширения классов статическими свойствами и свойствами экземпляров класса соответственно.
moduleKeywords = ['extended', 'included']
class Module
@extend: (obj) ->
for key, value of obj when key not in moduleKeywords
@[key] = value
obj.extended?.apply(@)
this
@include: (obj) ->
for key, value of obj when key not in moduleKeywords
# Assign properties to the prototype
@::[key] = value
obj.included?.apply(@)
this
Придется немного повозиться с переменной moduleKeywords, чтобы гарантировать поддержку функции обратного вызова после расширения класса. Посмотрим на наш класс Module в действии:
classProperties =
find: (id) ->
create: (attrs) ->
instanceProperties =
save: ->
class User extends Module
@extend classProperties
@include instanceProperties
# Usage:
user = User.find(1)
user = new User
user.save()
Как вы можете видеть, мы добавили несколько статичных свойств, find()
и create()
в класс User, а также метод экземпляра save(). Поскольку у нас есть функции обратного вызова, которые выполняются, когда модули расширяются, мы можете сократить процесс добавления статических свойств и свойства экземпляра:
ORM =
find: (id) ->
create: (attrs) ->
extended: ->
@include
save: ->
class User extends Module
@extend ORM
Очень просто и элегантно!