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

From JazzTeamWiki
Jump to: navigation, search

ООП в Javascript: наследование

  • Javascript - очень гибкий язык. В отличие от Java, PHP, C++ и многих других языков, где наследование можно делать одним способом - в javascript таких способов много.
  • На уровне языка реализовано наследование на прототипах. С помощью некоторых трюков можно сделать (хотя и не так удобно, как в Java/C++) наследование на классах, объявить приватные свойства объекта и многое другое.

Создание объекта. Функция-конструктор

  • Любая функция, кроме некоторых встроенных, может создать объект.Для этого ее нужно вызвать через директиву new.Например, функция Animal в примере ниже создаст новый объект.
function Animal(name) {
   this.name = name
   this.canWalk = true
}
var animal = new Animal("скотинка")
  • Во время работы функции, вызванной директивой new, новосоздаваемый объект доступен как this, так что можно проставить любые свойства.
  • В этом примере был создан объект класса Animal, и ему добавлены свойства name и canWalk. Получилось вот так:
animal.name = 'скотинка'
animal.canWalk = true

Наследование через прототип

  • В javascript базовое наследование основано не на классах. То есть, нет такого, что классы наследуют друг от друга, а объект класса-потомка получает общие свойства.
  • Вместо этого объекты наследуют от объектов без всяких классов. Наследование на классах можно построить(эмулировать), опираясь на базовое наследование javascript.
  • Разберем подробнее, что такое наследование от объектов и как оно работает.
  • Сделаем класс, наследующий от Animal - назовем его Rabbit.Для этого сначала объявим функцию Rabbit.
function Rabbit(name) {
   this.name = name
}
  • Пока что она просто создает объекты Rabbit. Поставим свойство prototype, чтобы новые объекты имели прототип animal (мы объявили этот объект чуть выше):
Rabbit.prototype = animal
  • А теперь - создадим пару кроликов.
big = new Rabbit('Chuk')
small = new Rabbit('Gek')
alert(big.name)  // Chuk
alert(small.name) // Gek
alert(big.canWalk) // true
// в Firefox можно еще так
if (big.__proto__) {  // в Firefox __proto__ это Prototype
   alert(big.__proto__.name) // скотинка
}
  • Свойство name хранится прямо в объектах Rabbit, а canWalk берется из прототипа animal.Так как у обоих кроликов один прототип, то его изменение тут же отразится на обоих.
alert(big.canWalk)  // true
// поменяли в прототипе
animal.canWalk = false
alert(big.canWalk)  // false
alert(small.canWalk)  // false

Перекрытие свойств родителя

  • Запишем свойство canWalk напрямую в объект Rabbit:
animal.canWalk = false
small.canWalk = true
alert(big.canWalk)  // false
alert(small.canWalk)  // true
  • У разных кроликов получилось разное значение canWalk, независимое от родителя.Таким образом мы реализовали перекрытие (override) свойств родительского объекта.

Начало цепочки наследования

  • Наверху цепочки всегда находится объект встроенного класса Object.
  • Так получается из-за того, что по умолчанию свойство prototype функции равно пустому объекту new Object().
// Animal.prototype не указан явно, по умолчанию:
Animal.prototype = {}

Получается такая картинка: http://javascript.ru/files/upload/2007/09/mwsnap020.jpg

  • Это хорошо, потому что у класса Object есть ряд полезных функций: toString(), hasOwnProperty()... А, например в Firefox, есть даже функция toSource(), которая дает исходный код, т.е "полный дамп" объекта.
  • Благодаря тому, что вверху цепочки наследования стоит Object, все остальные объекты имеют доступ к этому функционалу.

Методы объекта

  • При вызове метода - он имеет доступ ко всем данным "своего" объекта.
  • Для этого в javascript (как, впрочем, и во многих других языках) используется ключевое слово this.
  • Например мы хотим добавить всем объектам класса Animal функцию перемещения. Для этого запишем в Animal.prototype метод move. Каждый его вызов будет изменять расстояние distance:
Animal.prototype.move = function(n) {
    this.distance = n
    alert(this.distance)
}
  • Теперь если мы сделаем новый объект, то он сможет передвигаться:
var animal = new Animal("животное")
animal.move(3)   // => 3
animal.move(4)   // => 4
...
  • При вызове animal.move, интерпретатор находит нужный метод в прототипе animal: Animal.prototype.move и выполняет его, устанавливая this в "текущий" объект.

this в javascript В javascript this работает не так, как в PHP, C, Java.

Значение this ставится на этапе вызова функции и может быть различным, в зависимости от контекста.
Подробнее это описано в статье как javascript работает с this.
  • Точно также смогут вызывать move и объекты класса Rabbit, так как их прототипом является animal.
  • Альтернативный подход заключается в добавлении методов объекту в его конструкторе.
  • Объявление move в классе Animal при таком подходе выглядело бы вот так:
function Animal(n) {
   // конструируем объект
   .....
   // добавляем методы
   this.move = function(n) {
       this.distance = n
       alert(this.distance)
   }
}
  • В наиболее распространенных javascript-библиотеках используется первый подход, т.е добавление методов в прототип.

Свойства-объекты или "иногда прототип это зло"

  • Объявление всех свойств в прототипе может привести к незапланированному разделению одного и того же свойства разными объектами.
  • Например, объявим объект класса хомяк(Hamster). Метод found набирает еду за щеки, набранное хранит в массиве food.
function Hamster() {}
   Hamster.prototype = {
       food: [],
       found: function(something) {
       this.food.push(something)
   }
}
  • Создадим двух хомячков: speedy и lazy и накормим первого:
speedy = new Hamster()
lazy = new Hamster()
speedy.found("apple")
speedy.found("orange")
alert(speedy.food.length) // 2
alert(lazy.food.length) // 2 (!??)

Как видно - второй хомяк тоже оказался накормленным! В чем дело?

  • Причина заключается в том, что food не является элементарным значением.
  • Если при простом присвоении hamster.property="..." меняется свойство property непосредственно в объекте hamster, то при вызове hamster.food.push(...) - яваскрипт сначала находит свойство food - а так, как в hamster его нет, то оно берется из прототипа Hamster.prototype, а затем вызывает для него метод push.
  • На каком бы хомяке не вызывался hamster.food.push(..) - свойство food будет браться одно и то же, из общего прототипа всех хомяков.

Мы получили пример "статического свойства класса". Да, оно бывает полезно. Например, мы можем разделять общую информацию между всеми хомяками посетителя.*Но в данном случае такое ни к чему. Чтобы разделить данные, неэлементарные свойства обычно присваивают в конструкторе:

function Hamster() {
   this.food = []
}
Hamster.prototype = {
   food: [], // просто для информации
   found: function(something) {
       this.food.push(something)
   }
}
  • Теперь у каждого объекта-хомяка будет свой собственный массив food.
  • Свойство food в прототипе оставлено как комментарий. Оно не используется, но может быть полезно для удобства документирования.

Наследование на классах. Функция extend

  • Рабочий вариант наследования на классах, в общем-то, готов.
  • Для того чтобы объект класса Rabbit унаследовал от класса Animal - нужно
    • Описать Animal
    • Описать Rabbit
    • Унаследовать кролика от объекта Animal:
Rabbit.prototype = new Animal()
  • Однако, у такого подхода есть два недостатка:
    • Для наследования создается совершенно лишний объект new Animal()
    • Конструктор Animal должен предусматривать этот лишний вызов для и при необходимости делать такое "недоживотное", годное лишь на прототип.
  • К счастью, можно написать такую функцию, которая будет брать два класса и делать первый потомком второго:
function extend(Child, Parent) {
    var F = function() { }
    F.prototype = Parent.prototype
    Child.prototype = new F()
    Child.prototype.constructor = Child
    Child.superclass = Parent.prototype
}
  • Использовать ее для наследования можно так:
// создали базовый класс
function Animal(..) { ... }
// создали класс
// и сделали его потомком базового
function Rabbit(..)  { ... }
extend(Rabbit, Animal)
// добавили в класс Rabbit методы и свойства
Rabbit.prototype.run = function(..) { ... }
// все, теперь можно создавать объекты
// класса-потомка и использовать методы класса-родителя
rabbit = new Rabbit(..)
rabbit.animalMethod()
  • Функция очень удобная и работает "на ура".
  • Она не создает лишних объектов и в качестве бонуса записывает класс-родитель в свойство потомка superclass - это удобно для вызова родительских методов в конструкторе и при перекрытии методов.

Как оно работает?

Есть разные мнения, кто придумал функцию extend, но популяризацией она обязана Дугласу Крокфорду.
Как и почему она все-таки работает - может быть неочевидно даже опытным javascript-специалистам.

Вызов родительских методов

Конструктор


  • В механизме наследования, разобранном выше, есть одно белое пятно. Это - конструктор.
  • Хотелось бы, чтобы конструкторы всех родителей вызывались по порядку до конструктора самого объекта.
  • С наследованием через extend - это очень просто.
  • Вызов конструктора родителя с теми же аргументами, что были переданы осуществляется так:
function Rabbit(..)  {
    ...
    Rabbit.superclass.constructor.apply(this, arguments)
    ...
}
  • Конечно же, аргументы можно поменять, благо apply дает возможность вызвать функцию с любыми параметрами вместо arguments в примере.

Любые методы


  • Аналогично можно вызвать и любой другой метод родительского класса:
Rabbit.superclass.run.apply(this, ...)

Почему не this.constructor?


  • В этих примерах везде в явном виде указано имя класса Rabbit, хотя можно бы попробовать указать this.constructor, который должен указывать на Rabbit, т.к объект принадлежит этому классу.
  • Если так поступить, то будет ошибка при цепочке наследования классов из 3 элементов типа foo -> bar -> zot.
  • Проиллюстрируем ее на примере:
function foo() {}
foo.prototype.identify = function() {
    return "I'm a foo";
}
function bar() {}
extend(bar, foo)
bar.prototype.identify = function() {
    return "I'm a bar and " +
    this.constructor.superclass.identify.apply(this, arguments);
}
function zot() {}
extend(zot, bar)
zot.prototype.identify = function() {
    return "I'm a zot and " +
    this.constructor.superclass.identify.apply(this, arguments);
}
f = new foo();
alert(f.identify()); // "I'm a foo"
b = new bar();
alert(b.identify()); // "I'm a bar and I'm a foo"
z = new zot();
alert(z.identify()); // stack overflow


Источник

Информация взята с сайта http://javascript.ru/tutorial/object/inheritance