Мастер-классы по Javascript Екатеринбург Ростов-на-Дону Москва Узнать больше...
Содержание (скрыть) Содержание (показать)

Обобщение и сравнение паттернов

  1. Функциональный паттерн
    1. Ограничения функцинального паттерна
    2. Достоинства
    3. Недостатки
  2. «Псевдоклассы»
    1. Достоинства
    2. Недостатки
  3. Резюме

Разных ООП-подходов, по-сути, всего два.

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

Функциональный паттерн

  • Назначение методов и свойств идёт напрямую в объект.
  • Наследование осуществляется через вызов конструктора родителя.

Например:

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 = ...
}


Комментарии

  1. Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  2. Если ваш комментарий касается задачи -- откройте её в отдельном окне и напишите там.
  3. Комментарии без смысла, с рекламой или не о статье вообще - удаляются.
Наверх

Содержание

Реклама

Нашли опечатку?

Нашли опечатку на сайте? Что-то кажется странным?
Выделите соответствующий текст и нажмите Ctrl+Enter!

Последние Комментарии

Помоги другим!

Помоги другим узнать о хорошей статье!