Разных ООП-подходов, по-сути, всего два.
В предыдущих статьях было развёрнутое описание, что как и почему. Здесь же они описаны кратко, в целях обобщения и сравнения — когда лучше использовать один или другой.
Функциональный паттерн
- Назначение методов и свойств идёт напрямую в объект.
- Наследование осуществляется через вызов конструктора родителя.
Например:
functon Animal() {
var self = this;
...
function private() { // *!*приватный метод*/!*
alert(self); // <-- обращение к объекту через self
}
this.public = function() { // *!*публичный метод*/!*
// тоже через self для удобства переопределения и рефакторинга
alert(self);
}
}
Потомок:
functon Rabbit() {
var self = this;
Animal.apply(this, arguments); // *!*наследование*/!*
...
var oldPublic = this.public;
this.public = function() { // *!*переопределение*/!*
oldPublic();
}
}
Ограничения функцинального паттерна
Функциональный паттерн обладает важным архитектурным ограничением.
Конструктор-наследник получает контроль лишь после полного выполнения конструктора родителя:
function MenuHighlight(menuId) {
Menu.apply(this, arguments); // создать родителя
... добавить свои методы ...
... переопределить методы родителя ...
}
Каждый наследник сначала вызывает конструктор родителя, и тем самым создает целиком родительский объект, а уже затем делает что-то сам.
Посмотрим, для примера, иерархию Mouse -> JerryMouse -> ColoredJerryMouse:
function Mouse() {
this.init = ..
...
}
function JerryMouse() {
Mouse.apply(this, arguments);
this.init = ...
...
}
function ColoredJerryMouse() {
JerryMouse.apply(this, arguments);
this.init = ...
...
}
При создании new ColoredJerryMouse конструкторы вызываются в следующем порядке:

На каждом этапе объект полностью готов.
Получается, что если я в потомке пожелаю переопределить какой-то метод (this.init), то это случится уже после того, как родительский объект создан. Конструктор родителя не сможет «подхватить» это изменение.
Это потому, что в функциональном паттерне объявление методов объединено с инициализацией.
Это его основное ограничение по сравнению с классами, в которых эта задача решается легко:
function Mouse() {
this.init();
}
Mouse.prototype.init = ...
function JerryMouse() {
this.init();
}
JerryMouse.prototype.init = ...
function ColoredJerryMouse() {
this.init();
}
ColoredJerryMouse.prototype.init = ...
При создании new ColoredJerryMouse() — вызовется именно init из ближайшего прототипа, т.е. переопределённый.
Красивые приватные переменные и методы через var/function в реальной жизни остаются «не у дел», как только появляется наследование. Ведь их приходится делать защищёнными, переписывать через this._var/this._function.
В результате мы видим, что при наследовании у функционального паттерна фактически нет преимуществ перед «псевдоклассами», а наоборот — есть важный недостаток.
Поэтому, как правило, функциональный паттерн используется для задач, в которых наследование не требуется.
Достоинства
- Приватные методы — удобно и полезно.
- Кроме того, все современные минификаторы JavaScript заменяют имена локальных переменных и функций на более короткие. В результате получается более лёгкий код.
- Такой метод довольно-таки красив. Всё в одной функции, не нужно обёртки. Вызовы приватных методов осуществляются без
this.
Недостатки
- Объект создаётся вместе со всеми методами и свойствами. Это дольше и более затратно по памяти. Впрочем, это имеет значение только если объектов создаётся много.
- Оператор
instanceofне может проверить родительский конструктор.var rabbit = new Rabbit(); alert(rabbit instanceof Rabbit); // true alert(rabbit instanceof Animal); // false
Так происходит потому, что технически
Animalне имеет отношения к объекту. Это просто одна из функций, которые вызывались в коде конструктораRabbit. - Потомок может что-либо переопределить лишь после инициализации родителя. Это побочное следствие того, что инициализация объединена с объявлением методов. Красивого способа для обхода этого нет.
«Псевдоклассы»
Все методы и некоторые свойства по умолчанию находятся в prototype конструктора.
«Псевдокласс» или, как коротко говорят, «класс» состоит из конструктора и методов/свойств в его прототипе:
// --------- *!*Класс-Родитель*/!* ------------
function Animal(name) {
this.name = name;
}
// Методы родителя
Animal.prototype.run= function() {
alert(this + " бежит!")
}
Animal.prototype.toString = function() {
return this.name;
}
// --------- *!*Класс-потомок*/!* -----------
function Rabbit(name) {
Rabbit.parent.constructor.apply(this, arguments);
}
// *!*Унаследовать*/!*
extend(Rabbit, Animal);
// Методы потомка
Rabbit.prototype.run = function() {
Rabbit.parent.run.apply(this); // метод родителя вызвать
alert(this + " подпрыгивает!");
};
var rabbit = new Rabbit('Кроль');
rabbit.run();
Можно использовать и дополнительные обёртки, например функцию copy для копирования свойств в прототип, чтобы сделать синтаксис красивее.
Достоинства
- Полная поддержка наследования, включая
instanceofи защищённые методы. - Создание объекта — очень быстрое, так как всё необходимое уже находится в прототипе.
Не замедляет ли выполнение то, что все методы находятся в прототипе? Ведь их нужно искать, подниматься по цепочке __proto__…
К счастью, нет, не замедляет. Современные браузеры (кроме IE<9) кэшируют местонахождение метода, так что все вызовы после первого будут очень быстрыми.
Недостатки
- Нужна ООП-обёртка. Как минимум, функция
extend, а возможно и что-то ещё. - Все методы — либо публичные, либо защищенные, приватных нет. Обращение к ним везде через
this. Это ведёт к более длинному коду, по сравнению с функциональным подходом. - Сложные свойства необходимо назначать в конструкторе, чтобы не получилось как в задаче Хомяки с __proto__.
Резюме
Псевдоклассы отлично поддерживают наследование и instanceof. Они требуют меньше памяти, быстрее в создании — в общем, сплошные преимущества.
Можно сказать, что это предпочтительный подход при построении иерархии наследования.
Но и функциональный подход вполне применим. Оно особенно удобен, когда наследование не планируется — в этом случае его красивые приватные свойства (через var) и методы можно использовать по-максимуму.
Например, для описания объектов, которые создаются в единственном экземпляре:
var GlobalManager = new function() {
var privateVar;
function privateMethod() {
...
}
this.public = ...
}
Комментарии
- Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
- Если ваш комментарий касается задачи -- откройте её в отдельном окне и напишите там.
- Комментарии без смысла, с рекламой или не о статье вообще - удаляются.