В паттерне, объект создается в конструкторе, а его методы объявляются в прототипе.
Данный паттерн используется во фреймворках, таких как 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
- Когда вызывается
new Animal(name)
, объект получается ссылку__proto__
наAnimal.prototype
(см. правую часть схемы). - Метод
animal.sit
изменяетanimal.canWalk
в экземпляре, поэтому наше животное больше не может ходить, а другие могут.
Схема псевдо-класса:
- Методы и свойства по умолчанию определяются в прототипе.
- Методы в
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
.
Вот так, например:
Чтобы реализовать это, мы должны создать сначала пустой объект 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) методы и свойства поддерживаются с договоренностью об именование. Методы, начинающиеся с подчеркивания '_', не должны вызваться из вне (на деле могут).
Приватные (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