25 июня 2012 в 10:31

CoffeeScript: Классы tutorial

CoffeeScript: Classes

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


Содержание:

1. Основные понятия
2. Члены класса
   2.1. Метод constructor
   2.2. Открытые члены класса
   2.3. Закрытые члены класса
   2.4. Защищенные члены класса
   2.5. Статические члены класса
   2.6. Оператор => (fat arrow)
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

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

Далее мы рассмотрим реализации модификаторов уровня доступа, которые не являются частью языка CoffeeSript (хотя многие так считают).

Открытые члены класса (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__

Но есть есть одна проблема, наше закрытое свойство доступно напрямую через метод __private__:

object.__private__ 'privated', 2 # 3

C учетом небольших правок (спасибо nayjest за то, что обратил на это внимание и предложил решение), можно закрыть и этот вопрос:

__private__: (name, param...) ->
	parent = @constructor.__super__

	for key, value of @constructor::
		allow = on if arguments.callee.caller is value and not parent

	eval(name).apply @, param if alllow

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

Из недостатков:
— использование функции eval и arguments.callee.caller
— лишняя «прослойка» __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...) ->
			parent = @constructor.__super__

			for key, value of @constructor::
				allow = on if arguments.callee.caller is value and not parent

			eval(name).apply @, param if allow

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...) ->
		parent = @constructor.__super__

		for key, value of @constructor::
			allow = on if arguments.callee.caller is value

		eval(name).apply @, param if allow

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...) ->
		parent = @constructor.__super__

		for key, value of @constructor::
			allow = on if arguments.callee.caller is value

		eval(name).apply @, param if allow

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

Статические члены класса (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 реализована настолько просто — насколько это возможно.
Alexander Abashkin @monolithed
карма
27,0
рейтинг 0,0
Пользователь
Самое читаемое Разработка

Комментарии (15)

  • –1
    Эээ батенька, а как же классы в ES Harmony, следовало бы об этом хоть слово сказать вместо категорического
    В ECMAScript пока отсутствует понятие «класс»

    К тому же, разработчики Coffescript, работая над классами в своем языке, ориентировались именно на черновики ES Harmony.
    • 0
      Эээ батенька, а как же классы в ES Harmony, следовало бы об этом хоть слово сказать вместо категорического.


      Именно по этой причине я и сделал акцент на слове «пока».
      Если по факту, то обсуждать классы описанные в ECMAScript Harmony классы нет смысла, потому что в том виде котором они представлены в Traceur ни один из движков не поддерживает.
      И несмотря на то, что их перенесли из ветки Strawman'a в драфтовой спецификации это не отображено.

      К тому же, разработчики Coffescript, работая над классами в своем языке, ориентировались именно на черновики ES Harmony.


      В CoffeeScript нет модификаторов доступа, а то что есть сейчас лишь условность (все это описано в статье).
  • 0
    Хмм, сразу видно, что вы старались и проделали немалую работу над этой статьей, но по-моему это как-то идеологически неправильно — обучать азам ООП в Javascript сразу на Coffeescript. К тому же, у меня складывается впечатление, что те, кто не знали ООП в Javascript, из этой статьи, пардон, ничерта не поймут.

    + есть критика по закрытым и защищенным членам класса в вашей статье
    • +2
      обучать азам ООП в Javascript сразу на Coffeescript

      Эта статья ориентирована на людей которые целенаправленно изучают CoffeeScript.

      PS: Более общая тема про работу с объектами в ECMAScript также планируется (написана уже большая часть)
      • 0
        Имхо: не зная азов ООП в Javascript, Coffeescript изучать рановато
  • 0
    class A
        constructor: (@param) ->
        property = 1 # private
    

    Тогда уж не private, а static private
  • 0
    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 доступно только для членов базового класса!


    Вот это вообще бред.

    1) Я могу вызвать object.__private__ 'privated' извне точно так же, как внутри методов класса.
    2)
     if !@constructor.__super__ 
    

    — вы сами понимаете смысл этой проверки? Условие будет выполнено, если у данного класса нет классов-предков, т. е. он первый в цепочке наследования, которая строится с помощью Coffeescript. Это абсолютно никак не помагает реализовать приватные свойства. Например, что мне делать, если я хочу заиметь такие вот приватные методы в классе B? Раз уж вы решили так поизвращаться, вам нужно было делать что-то типа такого:
     if arguments.callee.caller == constructor
    

    Но Function.caller is deprecated и не работает в strict mode
    • 0
      Пардон, не constructor, а
      @constructor
      
      • 0
        Вот это вообще бред.

        1) Я могу вызвать object.__private__ 'privated' извне точно так же, как внутри методов класса.


        Не совсем понял, что вы имеете ввиду?

        object = new A 1
        console.log object.method() # 3
        
        object = new B 1
        console.log object.__private__ 'privated' # undefuned
        


        — вы сами понимаете смысл этой проверки? Условие будет выполнено, если у данного класса нет классов-предков, т. е. он первый в цепочке наследования, которая строится с помощью Coffeescript. Это абсолютно никак не помагает реализовать приватные свойства. Например, что мне делать, если я хочу заиметь такие вот приватные методы в классе B?

        Пожалуйста не путайте закрытые свойства (те свойства которые доступны только базовому классу) и защищенные (те которые доступны всем наследникам).

        Раз уж вы решили так поизвращаться, вам нужно было делать что-то типа такого:
         if arguments.callee.caller == constructor
        


        Во-первых, как вы заметили arguments.callee.caller не соответствует ECMAScript 5 (хотя eval, тоже, но я его использовал для большей ясности реализации)
        Во-вторых, вы сами проверяли запускать это?

        PS: Если вы найдете какие-то неточности в статье, прошу писать в ЛС, чтобы не вводить в заблуждение других.
        • 0
          Ок, объясню на примере: как вы предлагаете использовать этот метод __private__ для реализации закрытых или защищенных свойств в классе В, если у нас есть:
          class A
          class B extends A
          class C extends B
          ?

          То, что вы написали не является реализацией приватных/защищенных членов класса.

          По поводу arguments.callee.caller — вот так могла бы выглядеть реализация protected static методов:
          class A
            myPrivateMethod = (x)->console.log 'hello', x
            myPublicMethod: (x)-> 
              @__protected__('myPrivateMethod',[x])
            __protected__: (name, args)->    
              isAllowed = false    
              for key,val of @constructor.prototype      
                if arguments.callee.caller == val
                  isAllowed = yes
              unless isAllowed  
                console.log 'Private access only!' 
                return
              eval(name).apply @, args
             
          class B extends A
            myPublicMethod2: ()-> 
              @__protected__('myPrivateMethod',[2])
          #test case
          a = new A
          b = new B
          a.myPublicMethod 1
          b.myPublicMethod2()
          a.__protected__ 'myPrivateMethod', [3]
          

          Но я никак не могу рекомендовать кому-то использовать такой подход.
          А вообще, завязывайте так извращаться, не нужно оно вам (я о костылях для модификаторов доступа). А новичкам в Cofeescript и подавно.

        • 0
          # Я об этом:
          object = new A 1
          console.log object.__private__ 'privated', [2]
          # какой же это приватный метод?
          
          • 0
            Действительно не доглядел, спасибо что заметили.
            А вообще, завязывайте так извращаться, не нужно оно вам (я о костылях для модификаторов доступа). А новичкам в Cofeescript и подавно.

            Вы правы, я не сторонник таких велосипедов. Однако тот факт, что очень много статей посвященных Cofeescript пестрят заголовками о неких модификаторах доступа меня

            Истинной целью описания реализации модификаторов доступа была попытка показать, что:
            — при большом желании реализовать можно многое;
            — не стоит принимать за правду все что пишут в большинстве статей о Cofeescript. Особенно тем статьям и книгам в которых описываются модификаторы доступа как часть языка.
  • 0
    You make my day!
    На завтра что-то планируется? :)
  • 0
    На завтра что-то планируется? :)

    В данный момент, готовятся еще несколько статей (темы: паттерны проектирования, функции, объекты), но когда получится дописать сложно сказать.
  • 0
    1. Класс — специальная синтаксическая конструкция представленная множеством обобщённых логически связанных сущностей.
    2. Если давать более точно определение, то класс в CoffeeScript это абстрактный тип данных, который определяет альтернативный способ работы с объектами.

    Странная размытая статья. Если второе определение более точное, почему бы именно его не указать первым?

    Недавно пытался понять, почему один знакомый не понимает как использовать классы (он только учит ООП), и понял, что большинство людей пытается заучить шаблонные определения, без попыток пояснить своими словами.

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

    Потому что на самом деле — наоборот.
    Класс — это в первую очередь данные, а потом уже функции, которые работают именно с этими данными.

    Если рассматривать именно в таком ключе, иерархия становится на свои места. Проще проектировать что где должно быть.

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