Паттерн JavaScript псевдо-класс (pseudo-classical)

от автора

В паттерне, объект создается в конструкторе, а его методы объявляются в прототипе.

Данный паттерн используется во фреймворках, таких как Google Closure Library. Нативные объекты JavaScript также используют данный паттернт.

Объявление Pseudo-class

Термит Pseudo-class выбран потому, что в JavaScript нет как таковых классов, как в других языка как C, Java, PHP и др., но данный паттерн близок к определению класса.

Псевдо-класс состоит из функции конструктора и методов.

Например, псевдо-класс Animal состоит за одного метода sit и 2 свойств:

function Animal(name) {   this.name = name }  Animal.prototype = {    canWalk: true,   sit: function() {     this.canWalk = false     alert(this.name + ' sits down.')   } }  var animal = new Animal('Pet') // (1)  alert(animal.canWalk) // true  animal.sit()             // (2)  alert(animal.canWalk) // false 

  1. Когда вызывается new Animal(name), объект получается ссылку __proto__ на Animal.prototype (см. правую часть схемы).
  2. Метод animal.sit изменяет animal.canWalk в экземпляре, поэтому наше животное больше не может ходить, а другие могут.

image

Схема псевдо-класса:

  1. Методы и свойства по умолчанию определяются в прототипе.
  2. Методы в prototype используют this, который указывает на текущий объект так как значение this зависит от контекста вызова. Поэтому в animal.sit() this относиться к animal.

Наследование

Давайте создадим новый класс, который будет наследоваться от Animal, например Rabbit:

function Rabbit(name) {   this.name = name }  Rabbit.prototype.jump = function() {   this.canWalk = true   alert(this.name + ' jumps!') }  var rabbit = new Rabbit('John') 

Как видим, кролик имеет такую же структуру как и Animal — метод определен в прототипе.

Для наследования от Animal, необходимо сделать так Rabbit.prototype.__proto__ == Animal.prototype. Это естественное требование, так как если метод не найден в Rabbit.prototype, то его будем искать в методах родителя Animal.prototype.

Вот так, например:

image

Чтобы реализовать это, мы должны создать сначала пустой объект Rabbit.prototype наследуемы от Animal.prototype и после этого добавить методы.

function Rabbit(name) {   this.name = name }  Rabbit.prototype = inherit(Animal.prototype)  Rabbit.prototype.jump = function() { ... } 

Где inherit создает пустой объект с указанным __proto__:

function inherit(proto) {   function F() {}   F.prototype = proto   return new F } 

Вот что получилось в конце:

// Animal  function Animal(name) {   this.name = name }  // Animal methods Animal.prototype = {    canWalk: true,   sit: function() {     this.canWalk = false     alert(this.name + ' sits down.')   } }  // Rabbit function Rabbit(name) {   this.name = name }  // inherit Rabbit.prototype = inherit(Animal.prototype)  // Rabbit methods Rabbit.prototype.jump = function() {   this.canWalk = true   alert(this.name + ' jumps!') }  // Usage var rabbit = new Rabbit('Sniffer')  rabbit.sit()   // Sniffer sits. rabbit.jump()  // Sniffer jumps! 

Не используйте new Animal для наследования

Хорошо известный, но не правильный способ наследования, это когда вместо Rabbit.prototype = inherit(Animal.prototype) люди делают следующее:

// inherit from Animal Rabbit.prototype = new Animal() 

Как результат, мы получаем new Animal в прототипе. Наследование работает, так как new Animal естественно, наследует Animal.prototype.

… Но кто сказал, что new Animal() может быть вызван без name? Конструктор может строго требовать аргументов и умереть без них.

На самом деле, проблеме более концептуальная чем эта. Мы не хотим создавать Animal. Мы всего лишь хотим наследовать от него.

Вот почему Rabbit.prototype = inherit(Animal.prototype) более предпочтителен. Аккуратный наследство без побочных эффектов.

Вызов конструктора суперкласса

Конструктор суперкласса теперь вызывается автоматически. Мы можем вызвать его ручками с помощью Animal.apply() для текущего объекта:

function Rabbit(name) {   Animal.apply(this, arguments) } 

Данный код исполняет конструктор Animal в контексте текущего объекта и он задает name экземпляра.

Переопределение методов (полиморфизм)

Чтобы переопределить родительский метод, замените его в дочернем прототипе:

Rabbit.prototype.sit = function() {   alert(this.name + ' sits in a rabbity way.') } 

При вызове rabbit.sit() sit ищется по цепочке rabbit -> Rabbit.prototype -> Animal.prototype и находит его в Rabbit.prototype не доходя до Animal.prototype.

Конечно, мы может переопределить его иначе — напрямую в объекте:

rabbit.sit = function() {   alert('A special sit of this very rabbit ' + this.name) } 

Вызов родительского метода после переопределения

После переопределения метода, нам по-прежнему может понадобиться вызвать метод родителя. Это возможно если мы напрямую обратимся к прототипу родителя.

Rabbit.prototype.sit = function() {   alert('calling superclass sit:')   Animal.prototype.sit.apply(this, arguments) } 

Все родительские методы вызываются с помощью apply/call куда передается текущий объект как this. Простой вызов Animal.prototype.sit() будет использовать Animal.prototype как this.

Sugar: removing direct reference to parent

В предыдущем примере, мы вызывали родительский класс напрямую. Как конструктор: Animal.apply..., или метод: Animal.prototype.sit.apply....

На самом деле, мы не должны делать этого. При рефракторинге может быть изменено имя либо добавлен промежуточных класс в иерархии.

Обычно, языки программирования позволяют вызвать родительские методы с помощью специальный ключевых слов, как, например, parent.method() или super().

Но в JavaScript такого нет, но мы может смоделировать это.

Следующая функция расширяет наследование и также задает родителя и конструктор без прямой ссылки на него:

function extend(Child, Parent) {   Child.prototype = inherit(Parent.prototype)   Child.prototype.constructor = Child   Child.parent = Parent.prototype } 

Вот так вот можно использовать:

function Rabbit(name) {   Rabbit.parent.constructor.apply(this, arguments) // super constructor }  extend(Rabbit, Animal)  Rabbit.prototype.run = function() {     Rabbit.parent.run.apply(this, arguments) // parent method     alert("fast") } 

В результате, мы можем переименовать Animal или создать промежуточных класс GrassEatingAnimal и изменения затронут только Animal и extend(...).

Приватные и защищенные методы (инкапсуляция)

Защищённые (protected) методы и свойства поддерживаются с договоренностью об именование. Методы, начинающиеся с подчеркивания ‘_’, не должны вызваться из вне (на деле могут).

image

Приватные (private) методы не поддерживаются.

Статические (static) методы и свойства

Статические методы и свойства определяются в конструкторе:

function Animal() {    Animal.count++ } Animal.count = 0  new Animal() new Animal()  alert(Animal.count) // 2 

Итого

Вот и наш супер-мега-ООП фреймворк:

function extend(Child, Parent) {   Child.prototype = inherit(Parent.prototype)   Child.prototype.constructor = Child   Child.parent = Parent.prototype } function inherit(proto) {   function F() {}   F.prototype = proto   return new F } 

Использование:

// --------- the base object ------------ function Animal(name) {   this.name = name }  // methods Animal.prototype.run = function() {   alert(this + " is running!") }  Animal.prototype.toString = function() {   return this.name }   // --------- the child object ----------- function Rabbit(name) {   Rabbit.parent.constructor.apply(this, arguments) }  // inherit extend(Rabbit, Animal)  // override Rabbit.prototype.run = function() {   Rabbit.parent.run.apply(this)   alert(this + " bounces high into the sky!") }  var rabbit = new Rabbit('Jumper') rabbit.run() 

В наш фреймворк можно добавить чуток сахарку, например, функцию, которая копирую свойства из одного объекта в другой:

mixin(Animal.prototype, { run: ..., toString: ...}) 

Но на самом деле вам не очень-то и нужно использовать этот ООП паттерн. Всего лишь две функции справятся с этим.

ссылка на оригинал статьи http://habrahabr.ru/post/210596/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *