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

в 10:44, , рубрики: javascript, паттерны проектирования

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

Данный паттерн используется во фреймворках, таких как 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: ...})

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

Автор: Arkasha

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js