В этой статье я хочу рассказать об альтернативном способе программирования на JavaScript, без использования классов и конструкторов — чистым прототип-ориентированным ООП и особенностях его реализации на ECMA Script 5.
ООП можно разделить на две группы: класс-ориентированное (классическое) и прототип-ориентированное. Классический подход отражает взгляд Аристотеля на мир, в котором всё описывается идеальными понятиями. Прототипное ООП ближе к философии Людвига Витгенштейна, которая не полагается на строгую категоризацию и классификацию всего и вся, а пытается представить понятия предметной области материальными и интуитивно понятными (насколько это возможно). Типичным аргументом в пользу прототипирования является то, что обычно намного проще сначала разобраться в конкретных примерах, а только потом, изучая и обобщая их, выделить некоторые абстрактные принципы и впоследствии их применять.
JavaScript, согласно этой классификации, находится где-то посередине: с одной стороны, в нем присутствуют прототипы, с другой — классы и оператор new, как средство создания новых объектов, что не свойственно прототип-ориентированному подходу.
Классы
В JavaScript нет классов, скажете вы. Я бы не стал так утверждать.
Под классами в JS я подразумеваю функции-конструкторы: функции, вызываемой при создании экземпляра (выполнении оператора new), со ссылкой на прототип — объект, содержащий свойства (данные) и методы (функции) класса.
Как известно, в ЕСМА Script 6 возможно таки введут ключевое слово class:
class Duck{ constructor(name){ this.name = name; }, quack(){ return this.name +" Duck: Quack-quack!"; } } /// Наследование class TalkingDuck extends Duck{ constructor(name){ super(name); }, quack(){ return super.quack() + " My name is " + this.name; } } /// Инстанцирование var donald = new TalkingDuck("Donald");
Но по сути, ничего существенного (например модификаторов public, private) данное нововведение не принесет. Это нечто иное, как синтаксический сахар для подобной конструкции:
var Duck = function(name){ this.name = name; }; Duck.prototype.quack = function(){ return this.name +" Duck: Quack-quack!"; }; /// Наследование var TalkingDuck = function(name){ Duck.call(this, name); } TalkingDuck.prototype = Object.create(Duck.prototype); TalkingDuck.prototype.constructor = TalkingDuck; TalkingDuck.prototype.quack = function(){ return TalkingDuck.prototype.quack.call(this) + " My name is " + this.name; }; /// Инстанцирование var donald = new TalkingDuck("Donald");
Следовательно, классы в текущей версии JS уже есть, только нет удобной синтаксической конструкции для их создания.
В конце-концов, давайте определимся, что же такое класс. Вот определение (из википедии):
Класс — разновидность абстрактного типа данных в ООП, характеризуемый способом своего построения. Суть отличия классов от других абстрактных типов данных состоит в том, что при задании типа данных класс определяет одновременно и интерфейс, и реализацию для всех своих экземпляров, а вызов метода-конструктора обязателен.
Следуя этому определению, функция-конструктор является классом:
Функция-конструктор это абстрактный тип данных? — Да.
Функция-конструктор (вместе с свойствами из прототипа) определяет одновременно и интерфейс, и реализацию? — Да.
Вызов конструктора при создании экземпляра обязателен? — Да.
Прототипы
Прототип отличается от класса тем, что:
- Это уже готовый к использованию объект, не нуждающийся в инстанцировании. Он может иметь собственное состояние (state). Можно сказать что прототип является классом и экземпляром объединенными в одну сущность, грубо говоря, Singleton’ом.
- Вызов конструктора при создании объекта (клонировании прототипа) не обязателен.
Суть прототипного ООП сама по себе очень простая. Даже проще чем классического. Сложности в JS возникают из-за попытки сделать его похожим на то, как это реализовано в Java: в Java создание новых объектов производится с помощью оператора new, применяемого к классу. В JS — аналогично. Но, т.к. JS вроде как прототипный язык, и классов в нем не должно быть по определению, было введено понятие функция-конструктор. Беда в том, что синтаксиса для нормального описания связки конструктор-прототип в JavaScript’e нет. В итоге имеем море библиотек, исправляющих это досадное упущение.
В прототип-ориентированном подходе нет оператора new, а создание новых объектов производится путем клонирования уже существующих.
Наследование
Итак, суть прототипного (делегирующего) наследования состоит в том, что один объект может ссылаться на другой, что делает его прототипом. Если при обращении к свойству/методу оно не будет найдено в самом объекте, поиск продолжится в прототипе, а далее в прототипе прототипа и т.д.
var $duck = { name: "", quack: function(){ return this.name +" Duck: Quack-quack!"; } }; var donald = { __proto__: $duck, name: "Donald" }; var daffy = { __proto__: $duck, name: "Daffy" }; console.log( donald.quack() ); // Donald Duck: Quack-quack! console.log( daffy.quack() ); // Daffy Duck: Quack-quack! console.log( $duck.isPrototypeOf(donald) ); // true
daffy и donald используют один общий метод quack(), который предоставляет им прототип $duck. С прототипной точки зрения donald и daffy являются клонами объекта $duck, а с класс-ориентированной — “экземплярами класса” $duck.
Eсли же добавить/изменить некоторые методы непосредственно в объекте donald (или daffy), тогда его можно будет считать еще и “наследником класса” $duck.
Не забываем, что свойство __proto__ не стандартизировано, и использовать его можно только для дебага. Официально манипулировать свойством __proto__ возможно методами Object.create и Object.getPrototypeOf, появившимися в ECMAScript 5:
var donald = Object.create($duck, { name: {value: "Donald"} }); var daffy = Object.create($duck, { name: {value: "Daffy"} });
Инициализация
В отличии от класс-ориентированного подхода, наличие конструктора и его вызов при создании объекта на базе прототипа (клонировании) не обязателен.
Как же тогда инициализировать свойства объекта?
Простые, не калькулируемые значения по умолчанию для свойств можно сразу присвоить прототипу:
var proto = { name: "Unnamed" };
А если нужно использовать калькулируемые значения, то вместе с ECMA Script 5 нам на помощь приходит:
Ленивая (отложенная) инициализация
Ленивая инициализация это техника, позволяющая инициализировать свойство при первом к нему обращении:
var obj = { get lazy(){ console.log("Инициализация свойства lazy..."); // Вычисляем значение: var value = "Лениво инициализированное свойство " + this.name; // Переопределяем свойство, для того чтобы при следующем // обращении к нему, оно не вычислялось заново: Object.defineProperty(this, 'lazy', { value: value, writable: true, enumerable: true }); console.log("Инициализация окончена."); return value; }, // оставляем возможность инициализировать свойство // самостоятельно, в обход функции-инициализатора // (если это не будет влиять на согласованность объекта): set lazy(value){ console.log("Установка свойства lazy..."); Object.defineProperty(this, 'lazy', { value: value, writable: true, enumerable: true }); }, name: "БезИмени" }; console.log( obj.lazy ); // Инициализация свойства lazy... // Лениво инициализированное свойство БезИмени console.log( obj.lazy );// Инициализатор не запускается снова // Лениво инициализированное свойство БезИмени obj.lazy = "Переопределено";// Сеттер не запускается, т.к. свойство уже инициализировано console.log( obj.lazy ); // Переопределено
К плюсам этой техники можно отнести:
- Разбиение конструктора на более мелкие методы-аксессоры “автоматически”, как предотвращение появлению длинных конструкторов (см. длинный метод).
- Прирост в производительности, т.к. не используемые свойства инициализироваться не будут.
Сравнительная таблица
Прототип | Класс (ECMA Script 5) | Класс (ECMA Script 6) |
---|---|---|
Описание типа данных («класса») | ||
|
|
|
Наследование | ||
|
|
|
Создание объектов-экземпляров | ||
|
|
|
Список использованной литературы:
Dr. Axel Rauschmayer — Myth: JavaScript needs classes
Antero Taivalsaari — Classes vs. prototypes: some philosophical and historical observations [PDF]
Mike Anderson — Advantages of prototype-based OOP over class-based OOP
ссылка на оригинал статьи http://habrahabr.ru/post/175029/
Добавить комментарий