JavaScript принято считать прототип-ориентированным языком программирования. Но, как ни странно, этим подходом практически никто не пользуется: большинство популярных JS-фреймворков явно или неявно оперируют классами.
В этой статье я хочу рассказать об альтернативном способе программирования на 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
Автор: Quadratoff