CoffeeScript: Классы

в 6:31, , рубрики: coffeescript, ECMAScript, javascript, ruby, web-разработка, Веб-разработка, классы, объекты, ооп, руководство, метки: , , , , , , , ,

CoffeeScript: Classes

В ECMAScript пока отсутствует понятие «класс», в классическом понимании этого термина, однако, в CoffeeScript такое понятие есть, поэтому сегодня мы рассмотрим этот вопрос.

Содержание:

1. Основные понятия
2. Члены класса
   2.1. Метод constructor
   2.2. Открытые члены класса
   2.3. Закрытые члены класса
   2.4. Защищенные члены класса
   2.5. Статические члены класса
3. Наследование
4. Дополнительная литература

Основные понятия

Класс — специальная синтаксическая конструкция представленная множеством обобщённых логически связанных сущностей.

Объект — неупорядоченное множество свойств, имеющее объект-прототип, значением которого может быть объект либо null.

Конструктор — объект типа Function, который создаёт и инициализирует объекты.

Прототип — используется для реализации наследования структуры, состояния и поведения.
Cвойство prototype автоматически создается для каждой функции, чтобы обеспечить возможность использования функции в качестве конструктора.

Инстанцирование — создание экземпляра класса.

Общая теория

CoffeeScript это динамический язык с прототипно-классовой парадигмой, в котором работа с классами реализована на уровне делегирующего прототипирования.

Если давать более точно определение, то класс в CoffeeScript это абстрактный тип данных, который определяет альтернативный способ работы с объектами.

Для того чтобы определить класс нужно использовать спецификатор class:

class

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

(function() {
	function Class() {};
	return Class;
})();

Как видите, класс это это всего лишь обёртка (синтаксический сахар) представленная функцией-конструктором:

Object::toString.call class # [object Function]

Создание экземпляра класса (инстанцирование) осуществляется с помощью оператора new

class A
instance = new A

После применения оператора new к функции-конструктору, активируется внутренний метод [[Construct]], который отвечает за создание объекта:

class A
Object::toString.call new A # [object Object]

Иными словами, определение класса — не есть создание объекта, до тех пор, пока не произойдет инстанцирование:

class A
	constructor:
		(@variable) ->

A 1
variable # 1

Обратите внимание на то, что переменная variable стала доступна в глобальном пространстве имен.
Без инстанцирования, определение класса аналогично следующему коду:

A = (variable) ->
	@variable = variable

A 1
variable # 1

После того как мы инициализируем создание объекта, this внутри конструктора будет указывать на создаваемый объект:

class A
	constructor:
		(@property) ->

object = new A 1
object.property # 1
property # undefined

Как видите, this уже не указывает на глобальный объект, и переменная property не определена. Тоже самое относится и к коду без спецификатора class:

A = (@property) ->

object = new A 1
object.property # 1
property # undefined

На самом деле, разницы между этими вариантами нет.
Однако стоит заметить, что синтаксис со спецификатором class более предпочтителен для создания таких объектов и сейчас я расскажу почему…

Если вы помните, то я уже упоминал о неявном добавлении инструкции return в функциях. Такое поведение может сыграть с нами очень злую шутку:

A = (variable) ->
	method = -> variable

Вроде бы, вполне безобидный код?
Что же, давайте попробуем создать объект:

A = (param) ->
	method = -> param

object = new A 1
object.method() # TypeError: has no method 'method'

Что за дела?
Чтобы понять почему так происходит достаточно посмотреть результат трансляции:

var A = function(param) {
	return this.method = function() {
		return param;
	};
};

К сожалению нормального способа избежать подобных сюрпризов нет (я сейчас имею ввиду неявное добавление инструкции return).
Во-первых, мы можем предварить параметр символом @ и определить функцию в качестве инициализирующего параметра:

A = (@method, @param) ->

object = new A (-> @param), 1
do object.method # 1

Такое решение возможно благодаря тому что this определяется на момент создания объекта.

Давайте рассмотрим результат трансляции:

var A, object;

A = function(method, param) {
	this.method = method;
	this.param = param;
};

object = new A((function() {
	return this.param;
}), 1);

object.method(); //1

Примечание: функции, принимающие в качестве параметров другие функции или возвращающие другие функции в качестве результата называются функции высшего порядка (first-class functions). В этом случае, параметр функции называется функциональным параметром или фунаргом.

Следующий способ, который позволяет нам определить члены объекта заключается в использовании объектов-прототипов:

A = (@param) ->
A::method = -> @param

object = new A 1
object.method() # 1

Стоит заметить, что определение свойств объекта напрямую, и через объект-прототип это не одно и тоже:

A = (@param) ->
A::param = 0

object = new A 1
object.param # 1

У собственных свойств приоритет выше, поэтому сперва анализируются они, а потом уже поиск производится в цепочке прототипов.

Напомню, что сейчас мы рассматриваем вопрос относительно неявного добавления инструкции return в функциях-конструкторах и способы решения этой проблемы.

A = (@param) ->
	@method = ->
		@param
	@

object = new A 1
object.method() # 1

this будет указывать на вновь созданный объект.
При этом стоит учесть, что если возвращаемым значением будет объект, то именно он и будет являться результатом выражения new:

A = (@param) ->
	@method = ->
		@param
	[]

object = new A 1
object # []
Object::toString.call(object) # [object Array]
object.method() # TypeError: Object has no method 'method'

Соответственно чтобы иметь доступ к свойству method нужно явно вернуть объект которому принадлежит это свойство:

A = (param) ->
	object = {}
	object.method = ->
		param
	object

object = new A 1
object.method() #1

Разумеется не важно как будет возвращаться ссылка на объект:

A = (param) ->
	method: -> param

object = new A 1
object.method() #1

В данном случае, в функции-конструкторе A возвращается ссылка на анонимный объект.

Все тоже самое относится и к возвращению функций:

A = (one) ->
	(two) -> one + two

object = new A 1
object 2 # 3

Как вы понимаете, использование оператора new в этом случае опционально. А this внутри вложенной функции будет указывать на глобальный объект:

A = ->
	 -> @

new A()() # global

Cтоит заметить, что при использовании строгого режима (use strict), this будет указывать на undefined:

A = ->
	'use strict'
	 -> @

new A()() # undefined

Чтобы this указывал на вновь созданный объект, нужно явно добавить оператор new:

A = ->
	new -> @

new A() # object

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

A = (one) ->
	new (two) -> one + two

object = new A 1
object.constructor 2 # 3

Прошу не путать внутренний CoffeeScript метод constructor() и ссылку на функцию constructor, через которую можно получить ссылку на прототип объекта:

A = ->
	new ->

object = new A
object.constructor:: # object
object.constructor::constructor 2 # 3

Такое поведение осуществляется за счет диспетчеризации вызовов, когда объект следуя по цепочке делегирующих указателей не может найти соответствующее свойство он обращается к своему прототипу. Такая цепь обращений от объекта к прототипу называется цепью прототипов (prototype chain).
Подобное поведение хорошо знакомо программистам пишущих на языках Ruby, Python, SmallTalk и Self.

Примечание: к сожалению, для constructor нет псевдонима как для ptototype, но возможно в ближайшем будущем это будет учтено, т.к. во некоторых диалектах CoffeeScript это уже реализовано. Например в coco слово constructor можно заменить двоеточием (..):

@..:: # this.constructor.prototype

Члены класса

Метод constructor()

Конструктор класса — специальный метод constructor() определенный в теле класса и предназначенный для инициализации членов объекта.

class A
	A::property = 1

object = new A
object.property # 1

В этом примере, мы инициализировали создание класса с именем A.
Единственный член класса — свойство property, которое формально находится в прототипе объекта A.

Т.к. this всегда будет указывать на A (и транслировано тоже), есть смысл переписать определение класса:

class A
	@::property = 1

Несмотря на компактность записи, подобное определение членов класса не принято в CoffeScript. К тому же, есть более изящное решение:

class A
	property: 1

Для того чтобы определить члены класса за его пределами, следует использовать первую форму записи:

class A
A::property = 1

Как уже отмечалось ранее, каждый экземпляр класса имеет ссылку на собственный конструктор, которому доступно свойство prototype.
Получив таким образом ссылку на первоначальный прототип объекта, можно определить новые члены класса:

class A

object1 = new A
object1.constructor::property = 1

object2 = new A
object2.property # 1
object2.constructor is A # true

Если нужно добавить несколько свойств в прототип, то есть смысл сделать это «скопом»:

class A

A:: =
	property: 1
	method: -> @property

object2 = new A
object2.method() # 1

Однако, в этом случае, свойство constructor будет указывать на другой объект:

class A
A:: = {}

object = new A
object.constructor is A # false

Несмотря на то, что ссылка на оригинальный прототип будет утеряна, мало кто это заметит:

class A
A:: = {}

object = new A
object.constructor::property = 1 # Опасно!

object.property # 1

Нужно быть особо внимательными в таких ситуациях, потому что добавив новое свойство через экземпляр класса мы добавим это свойство всем объектам!

Смотрим пример внимательно:

class A

A:: =
	property: 1
	method: -> @property

object = new A

object.constructor::property = 'Oh my god!'

object.method() # 1
object.property # 1

list = []
list.property # 'Oh my god!'

Такое поведение стало возможно в связи с тем, что свойство constructor теперь указывает на Object:

class A
A:: = {}

object = new A
object.constructor # [Function: Object]
object.constructor is Object # true

Иными словами, сами того не подозревая у нас получилось следующее:

Object::property = 'Oh my god!'

Разумеется мало кому понравится видеть сторонние методы в своих объектах.
Для того чтобы иметь правильную ссылку на исходный конструктор следует явно ее воссоздать:

class A
A:: = constructor: A

object = new A

object.constructor::property = 1
object.property # 1
object.constructor is A # true

Теперь ссылка на объект-прототип корректная.

Если вы помните, то мы начинали рассматривать метод класса constructor(). Так вот, единственное назначение этого метода — инициализация параметров:

class A
	constructor: (param) ->
		@param = param

object = new A 1
object.param # 1

Соответственно, если нет необходимости в передаче параметров конструктору класса — разумно будет опустить метод constructor().

Идеологически так сложилось, что конструктор не должен возвращать никаких значений и не может быть перегружен (в CoffeeScipt отсутствует перегрузка операторов и функций).

Так как передача параметров в функцию-конструктор довольно частая операция, в CoffeeScript предусмотрен специальный синтаксис:

class A
	constructor: (@param) ->

object = new A 1
object.param # 1

Давайте посмотрим на результат трансляции:

var A, object;

A = (function() {

	function A(param) {
		this.param = param;
	}

	return A;
})();

object = new A(1);
object.param; //1

Как видите, param является прямым свойством свойством объекта A, т.е. в любой момент его можно модифицировать и даже удалить.

class A
	constructor: (@param) ->

object = new A 1
object.param = 2

object.param # 2

В этом случае, мы переопределили значение свойства конкретного экземпляра класса, что никак не отразится на другие экземпляры.

Открытые члены класса (public)

Во многих объектно-ориентированных языках инкапсуляция определяется по средствам применения таких модификаторов как public, private, protected и в какой-то степени static. В СoffeeScript для этих целей предусмотрен несколько иной подход, предлагаю начать рассмотрение этого вопроса с открытых членов (public)

Все открытые члены класса записываются в ассоциативной нотации, без ведущего символа @ и/или this:

class A
	constructor: (@param) ->
	method: -> @param

object = new A 1
object.method() # 1

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

var A, object;

A = (function() {
	function A(param) {
		this.param = param;
	}

	A.prototype.method = function() {
		return this.param;
	};

	return A;
})();

object = new A(1);
object.method(); //1

Из результата трансляции видно, что открытые члены класса добавляются в прототип объекта A.
Следовательно, обращение членам класса, технически осуществляется как в любом другом объекте:

class A
	property: 1

	method: ->
		@property

object = new A
object.method() # 1

Закрытые члены класса (private)

В закрытых членах класса обращения к члену допускаются только из методов того класса, в котором этот член определён. Наследники класса не имеют доступа к закрытым членам.

Закрытые члены класса записываются в литеральной нотации:

class A
	constructor: (@param) ->

	property = 1 # private

	method: ->
		property + @param

object = new A 1

object.method() # 2
object.property # undefined

Технически, закрытые члены класса, являются обычными локальными переменными:

var A, object;

A = (function() {
	var property;

	function A(param) {
		this.param = param;
	}

	property = 1;

	A.prototype.method = function() {
		return property + this.param;
	};

	return A;
})();

object = new A(1);
object.method();
object.property; # 2

На данный момент реализация закрытых членов весьма ограничена. В частности, закрытые члены класса не доступны для членов определенных вне класса:

class Foo
	__private = 1

Foo::method = ->
	try
		__private
	catch error
		'undefined'

object = new Foo
object.method() #undefined

Чтобы понять почему так происходит стоит взглянуть на результат трансляции:

var A;

A = (function() {
	var __private;

	function A() {}

	__private = 1;

	return Foo;
})();

A.prototype.method = function() {
	try {
		return __private;
	}
	catch (error) {
		return 'undefined';
	}
};

Наверняка у вас уже возник вопрос: почему нельзя поместить определение внешних членов в функцию-конструктор?
На самом деле, это не решит проблему, потому что определение членов класса может находится в разных файлах!

Частично решить эту задачу можно очень простым способом:

class A
	constructor: (@value) ->

	privated = (param) ->
		@value + param

	__private__: (name, param...) ->
		eval(name).apply @, param if !@constructor.__super__

A::method = ->
	@__private__ 'privated', 2

class B extends A

B::method = ->
	@__private__ 'privated', 2

object = new A 1
object.method() # 3

object = new B 1
object.method() # undefuned

object.privated # undefuned

Как видите, член класса privated доступно только для членов базового класса!

Все что нам потребовалось так это определить следующий метод в базовом классе:

__private__: (name, param...) ->
	eval(name).apply @, param if !@constructor.__super__

К преимуществам данной реализации можно отнести:
+ простота и эффективность
+ легкое внедрение в существующий код

Из недостатков:
— использование функции eval
— лишняя «прослойка» __private__
— в методе __private__ не установлены атрибуты дескриптора контролирующие перечисление этого метода, модификацию и удаление.

Последний пункт относящийся к недостаткам реализации, можно исправить так:

Object.defineProperty @::, '__private__'
	value: (name, param...) ->
		eval(name).apply @, param if !@constructor.__super__

Теперь метод __private__ не будет перечислен циклом for-of, его нельзя будет модифицировать и удалить. Давайте рассмотрим конечный пример:

class A
	constructor: (@value) ->

	privated = (param) ->
		@value + param

	Object.defineProperty @::, '__private__'
		value: (name, param...) ->
			eval(name).apply @, param if !@constructor.__super__

A::method = ->
	@__private__ 'privated', 2

class B extends A

B::method = ->
	@__private__ 'privated', 2

object = new A 1
object.method() # 3

object = new B 1
object.method() # undefuned
object.privated # undefuned

i for i of object # 3, value, method

Защищенные члены класса (protected)

Защищённые члены класса доступны только внутри методов базового класса и его наследников.

Формально, в CoffeeScript отсутствует специальный синтаксис для определения защищённых членов класса. Тем не менее, мы можем самостоятельно реализовать подобный функционал:

class A
	constructor: (@value) ->

	protected = (param) ->
		@value + param

	__protected__: (name, param...) ->
		eval(name).apply @, param

A::method = ->
	@__protected__ 'privated', 2

class B extends A

B::method = ->
	@__protected__ 'privated', 2

object = new A 1
object.method() # 3

object = new B 1
object.method() # 3

object.protected # undefuned

Как видите, архитектура этого решения практически идентична решению задачи с закрытыми членами класса, и следовательно имеет туже проблему с атрибутами дескриптора. Итоговое решение будет следующее:

Object.defineProperty @::, '__private__'
	value: (name, param...) ->
		eval(name).apply @, param

Стоит заметить, что это далеко не единственное решение данной задачи, просто оно наиболее универсально для понимания реализации.

Статические члены класса (static)

Статический член класса:
— предваряется символом @ или this
— может существовать только в единственном экземпляре
— для нестатических членов класса доступен только через объект-прототип базового класса

Рассмотрим пример определения статического члена класса:

class A
	@method = (param)
		-> param

A.method 1 # 1

Теперь давайте рассмотрим возможные (наиболее адекватные) формы записи статического члена класса:

@property: @
@property = @
this.property = @
this.constructor::property = 1
@constructor::property = 1
Class.constructor::property = 1

Если вы заметили, то использование символа @, более универсально.

Обращение к другим нестатическим членам класса возможно только через объект-прототип базового класса:

class A
	property: 1
	@method: = ->
		@::property

do A.method # 1

Обращение к другим статическим членам класса доступно через символ @ или this или имя класса:

class A
	@property: 1
	@method: = ->
		@property + A.property

do A.method # 2

Еще один момент, на который стоит обратить особое внимание, это использование оператора new:

class A
	@property: 1

	@method: ->
		@property

object = new A
object.method() # TypeError: Object # <A> has no method 'method'

Как видите, обращение к несуществующему методу привело к типу ошибки TypeError!
Теперь, давайте рассмотрим корректный способ вызова статического члена класса через экземпляр класса:

class A
	@property: 1

	@method: ->
		@property

object = new A
object.constructor.method() # 1

=> (fat arrow)

Еще одним не маловажным моментом при работе с членами класса, является возможность использовать оператор => (fat arrows), который позволяет не терять контекст вызова.
Например, это может быть полезно для создания функций обратного вызова (callback)

class A
	constructor: (@one, @two) ->

	method: (three) =>
		@one + @two + three

instance = new A 1, 2

object = (callback) ->
	callback 3

object instance.method # 6

Того же результата, мы могли бы добиться используя метод call():

class A
	constructor: (@one, @two) ->

	method: (three) ->
		@one + @two + three

instance = new A 1, 2

object = (callback) ->
	callback.call instance, 3

object instance.method # 6

Теперь, давайте рассмотрим ситуацию с использованием предиката:

class A
	constructor: (@splat...) ->

	method: (three) =>
		@splat

instance = new A 1, 2, 3, 4, 5

object = (callback, predicate) ->
	predicate callback()

object instance.method,
	(callback) ->
		callback.filter (item) ->
		 	item % 2

 # [1, 3, 5]

В этом примере, мы инициализировали создание класса A, с n-м количеством параметров типа Number.
Далее, член класса method вернул массив с переданными в конструктор параметрами. После чего, полученный массив был передан предикату, который отфильтровал значения массива по модулю 2, в результате чего был получен новый массив.

Наследование

Наследование в CoffeScript осуществляется с помощью оператора extends.

Рассмотрим пример:

class A
	constructor: (@property) ->

	method: ->
		@property

class B extends A

object = new B 1
object.method() # 1

Несмотря на то, что в классе B не определены собственные члены класса он наследует их от A.
Теперь, обратите внимание на то, что свойство property также доступно внутри класса B:

class A
	constructor: (@property) ->

class B extends A
	method: ->
		@property

object = new B 1
object.method() # 1

Как видите, суть оператора extends очень проста — установление родственной связи между двумя объектами.
Не сложно догадаться, что оператор extends можно использовать не только с классами:

A = (@param) ->

A::method = (x) ->
	@param * x

B = (@param) ->

B extends A

(new B 2).method 2 # 4

К сожалению, официальная документация CoffeeScript довольна «скудна» на примеры, однако вы всегда можете воспользоваться транслятором чтобы посмотреть реализацию того или иного куска кода:

Наиболее важные опции для анализа программного кода:

coffee -c file.coffee # Трансляция .coffee скрипта в JavaScript файл с тем же именем.
coffee -p file.coffee # Вывод на терминал результат трансляции
coffee -e 'console.log i for i in [0..5]' # Интерактивный режим трансляции
coffee -t # Возвращает токены

Очень важную ценность для анализа структуры программы имеет параметр -n (--nodes), он возвращает синтаксическое дерево:

class A
	@method: @

class B extends A

do (new B).method

coffee -n

Block
 Class
  Value "A"
  Block
   Value
    Obj
     Assign
      Value "this"
       Access "method"
      Value "this"
 Class
  Value "B"
  Value "A"
  Block
 Call
  Value
   Parens
    Block
     Op new
      Value "B"
   Access "method"

Наиболее полную информация о синтаксической структуре CoffeeScript смотрите в разделе nodes.coffee официальной документации.

Если определить в классах методы с одинаковыми именами, то родной метод перекроет унаследованный:

class A
	constructor: ->
	method: -> 'A'

class B extends A
	method: -> 'B'

object = new B
object.method() # B

Несмотря на то, что такое поведение вполне ожидаемое, у нас есть возможность вызвать метод класса A из B:

class A
A::method = -> 'A'

class B extends A
B::method = ->
	super

object = new B
object.method() # A

Этот код мало чем отличается от предыдущего, за небольшим лишь исключением, что в методе класса B возвращается некий оператор super.
Задача оператора super — вызов свойств определенных в родительском классе и инициализация параметров вызова.

При этом, структура наследования значения не имеет, вызывается метод ближайшего класса в иерархической цепочке наследования:

class A
A::method = -> 'A'

class B extends A
B::method = -> 'B'

class C extends B
C::method = -> super

object = new C
object.method() # B, потому что ближайший метод method определен в классе B

Если метод не определен в прямом родителе, то поиск продолжается следуя по цепочке делегирующих указателей:

class A
A::method = -> 'A'

class B extends A

class C extends B
C::method = -> super

object = new C 1
object.method() # 'A', потому что ближайший метод method определен в классе A

Оператор super, также может принимать параметры:

class A
	constructor: (@param) ->

A::method = (x) ->
	x + @param

class B extends A

B::method = ->
	super 3

object = new B 1
object.method 2 # 4 (3 + 1)

Несмотря на то, что вызывался метод с параметром 2, позже мы переопределили это значение на 3.

Если определить оператор super в методе constructor, то можно переопределить параметры с которыми инициализируется конструктор класса:

class A
	constructor: (@param) ->

A::method = (x) ->
	x + @param

class B extends A
	constructor: (@param) ->
		super 3

object = new B 1
object.method 2 # 5 (2 + 3)

Разумеется, допустимо одновременное использование оператора super в членах класса и конструкторе:

class A
	constructor: (@param) ->

A::method = (x) ->
	x + @param

class B extends A
	constructor: (@param) ->
		super 3

B::method = (x) ->
	super 4

object = new B 1
object.method 2 # 7 (3 + 4)

Теперь, предлагаю подробно рассмотреть внутреннюю реализацию паттерна, который реализует интерфейс наследования:

var A, B, object,

// Ссылка на метод hasOwnProperty
__hasProp = {}.hasOwnProperty;

// Функция __extends первым параметром принимает дочерний класс,
// вторым, родительский, т.е. child наследует свойства от parent
__extends = function(child, parent) {

	// Перебор свойств объекта parent
	for (var key in parent) {

		// Если свойство собственное (не унаследованное)
		if (__hasProp.call(parent, key)) {

			// Записать в объект child, свойства объекта parent.
			// Если свойство с таким именем уже существует,
			//то оно перезапишится c новым значением
			child[key] = parent[key];
		}
	}

	// Создание промежуточной функции-конструктора
	function ctor() {
		// Записать в свойство constructor ссылку на объект child
		this.constructor = child;
	}

	// Поменять ссылку прототипа.
	// Теперь ссылка будет указывать на прототип объекта parent
	ctor.prototype = parent.prototype;

	// Установить прототип дочернего объекта на новый объект
	child.prototype = new ctor();

	// Установить ссылку на родительский прототип
	// Используется для реализации оператора super
	child.__super__ = parent.prototype;

	// вернуть ссылку на
	return child;
};

// Родительский конструктор
A = (function() {
	function A() {};
	return A;
})();

// Дочерний конструктор
B = (function(_super) {

	// Вызов функции __extends.
	// Первым параметром передается ссылка на объект B,
	// вторым - ссылка на родительский объект A
	__extends(B, _super);

	function B() {
		// Вызвать родительский конструктор A в контексте конструктора B
		// Равносильно A.apply(this, arguments);
		return B.__super__.constructor.apply(this, arguments);
	}

	return B;
})(A);

Подведем итоги:
— Классы позволяют более четко визуализировать структуру обобщённых логически связанных сущностей;
— Наличие классов разрешает некоторое недопонимание при работе с объектами и наследованием, особенно у тех кто работал с классами ранее;
— Несмотря на то, что в ECMAScript не определены модификаторы уровня доступа (public, private и protected) их можно реализовать самостоятельно.
— Внутренняя реализация классов очень проста;

Дополнительная литература

Тонкости ECMA-262-3. Часть 7.1. ООП: Общая теория
Тонкости ECMA-262-3. Часть 7.2. ООП: Реализация в ECMAScript
Прототипное программирование
Объектно-ориентированное программирование
Наследование
Полиморфизм
Инкапсуляция
Делегирование

PS: На самом деле, некоторое время я даже сомневался в целесообразности написания этой темы, поскольку работа с классами в CoffeeScript реализована настолько просто — насколько это возможно.

Автор: monolithed

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


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