Наследование в JavaScript
Нет ничего более постоянного, чем временное.
Народная мудрость
Если помните, предыдущая набла закончилась полгода назад на том, что при программировании на JavaScript очень неплохо использовать прототипы объектов. Сейчас настало время уточнить данный термин, и заодно показать, как его применять еще эффективнее.
В JavaScript каждый объект может иметь асоциацию с другим
Секреты прототипов
В Интернете масса литературы, описывающей, что такое
Продемонстрируем «классическое» применение прототипов для реализации наследования в JavaScript.
Листинг 1
<pre><script>
//**
//** Базовый "класс" Car (Машина).
//**
function Car() {
document.writeln("Вызван конструктор Car().");
}
// Определяем новый метод "класса" Car.
Car.prototype.drive = function() {
document.writeln("Вызван Car.drive()");
}
//**
//** Производный "класс" Zaporojets (Запорожец - тоже Машина).
//**
function Zaporojets() {
document.writeln("Вызван конструктор Zaporojets().");
}
// Говорим, что прототип Car - "класс" Zaporojets.
Zaporojets.prototype = new Car();
// Определяем новый метод "класса" Zaporojets.
Zaporojets.prototype.crack = function() {
document.writeln("Вызван Zaporojets.crack()");
}
//**
//** Основная программа.
//**
document.writeln("Программа запущена.");
// Создаем объект производного "класса" Zaporojets.
var vehicle = new Zaporojets();
vehicle.drive(); // (*) вызывается функция базового объекта
// Создаем еще один объект того же класса.
var other = new Zaporojets();
vehicle.crack(); // функция производного объекта
</script></pre>
Запустив данный пример, можно заметить, что с точки зрения "обычного" ООП результат выглядит несколько необычно:
Листинг 2
Вызван конструктор Car().
Программа запущена.
Вызван конструктор Zaporojets().
Вызван Car.drive()
Вызван конструктор Zaporojets().
Вызван Zaporojets.crack()
В объектно-ориентированных языках с поддержкой классов (C++, Java, PHP, Perl, Python и т. д.) конструкторы базовых классов обычно вызываются непосредственно внутри конструкторов производных. В JavaScript, как было уже сказано в предыдущей набле, классов нет, есть только объекты. Здесь мы видим совершенно другую картину: конструктор
К сожалению, невозможно задать прототип для некоторого объекта, не создав предварительно объект базового класса. Если вы хотите присвоить
Подобное поведение, конечно, следует из того, как написана программа. Действительно, мы создали объект
Вывод: в JavaScript «стандартное» наследование реализуется совсем не так, как в других, «класс-ориентированных» языках программирования. Понятие «конструктора» в
Чем не являются прототипы?
Как и в дзене, чтобы лучше понять, что собой представляет некоторый термин, иногда бывает полезно уяснить, чем он точно не является. В тридцать девятой набле было сказано, что с каждым объектом (или, что то же самое, хэшем) может быть ассоциирован свой собственный хэш-прототип, просматриваемый интерпретатором в случае отсутствия некоторого свойства текущего объекта. Основываясь на этом, вы могли, обрадовавшись, тут же кинуться писать примерно следующий код:
Листинг 3
var obj = {
// В самом объекте свойства prop нет.
// Зато у него есть прототип...
prototype: {
// ...в котором данное свойство определяется...
prop: 101
}
// ...так что в итоге интерпрететор должен считать его.
}
// Проверим?
alert("Значение свойства: " + obj.prop); // What a...
Увы и ах: данный пример не работает, выдавая: "Значение свойства: undefined". А следовательно, присваивание свойству
Модифицируем теперь код программы:
Листинг 4
var obj = {
// В самом объекте свойства prop нет.
}
// Пробуем обратиться к прототипу по-другому.
obj.constructor.prototype.prop = 101;
// Проверим?
alert("Значение свойства: " + obj.prop);
// Он в этом-то объекте свойства быть не должно...
var newObj = {}; // пустой хэш
alert("Пустота: " + newObj.prop); // А это еще откуда?!
Результат "Значение свойства: 101" говорит нам, что программа заработала. Однако какой ценой? Свойство
Какие выводы можно сделать из примера?
Оператор new и obj.constructor
Новый объект в JavaScript может быть создан только одним способом: применением оператора
Листинг 5
var vehicle = new Car(); // создание нового объекта
var hash = {}; // сокращенная запись для new Object()
var array = []; // сокращенная запись для new Array()
Немногие над этии задумываются, но первый оператор примера полностью эквивалентен такому коду:
Листинг 6
var vehicle = new window.Car(); // можно и так...
var vehicle = new self.Car(); // в браузере self==window
или даже такому:
Листинг 7
var clazz = self.Car; // ссылка на функцию Car()
var vehicle = new clazz(); // неявное создание!
Он также функционально не отличается от следующего примера:
Листинг 8
// Создание объекта стандартным способом.
self.Car = function() { alert("Car") }
var vehicle = new self.Car();
Ну что, понравилось? Начали улавливать закономерности? Вот еще примеры:
Листинг 9
// Создаем "класс" на лету.
var clazz = function() { alert("Динамическая!") }
var obj = new clazz();
// А можно и без промежуточной переменной.
var obj = new (function() { alert("Wow!") })();
Иными словами, справа от
Так вот, после создания объекта интерпретатор присваивает его свойству
Листинг 10
// Создаем "класс" на лету.
var clazz = function() { alert("Динамическая!") }
var obj = new clazz();
alert(obj.constructor == clazz); // выводит true!
Но позвольте, ведь справа от
Листинг 11
var clazz = {}; // clazz.constructor == self.Object
var obj = new clazz(); // не работает!
Что же можно использовать с оператором
Оказывается, что свойство
Теперь вы понимаете, почему JavaScript не рассматривает элемент
Итак, вывод: прототипы объектов доступны по цепочке
Заставляем конструкторы базовых классов работать
Данная набла имеет циклический характер, и сейчас, хорошо понимая, как работают прототипы и конструкторы, мы снова возвращаемся к самому первому примеру. Речь пойдет о создании базового и производных объектов в стиле «класс-ориентированного» программирования.
Итак, перед нами стоят следующие задачи:
- Заставить конструкторы базовых объектов вызываться при создании производных.
- Научиться получать доступ к методам, переопределенным в производных объектах под тем же именем.
Если программировать на «чистом» JavaScript, данные две задачи выливаются в довольно громоздкий код. Чтобы каждый раз его не писать, я предлагаю вам использовать совсем небольшую библиотечку, обеспечивающую удобное применение рассматриваемых подходов. С ее использованием создание производных классов выглядит весьма просто:
Листинг 12
<script src="Oop.js"></script>
<pre><script>
// Базовый "класс".
Car = newClass(null, {
constructor: function() {
document.writeln("Вызван конструктор Car().");
},
drive: function() {
document.writeln("Вызван Car.drive()");
}
});
// Производный "класс".
Zaporojets = newClass(Car, {
constructor: function() {
document.writeln("Вызван конструктор Zaporojets().");
this.constructor.prototype.constructor.call(this);
},
crack: function() {
document.writeln("Вызван Zaporojets.crack()");
},
drive: function() {
document.writeln("Вызван Zaporojets.drive()");
return this.constructor.prototype.drive.call(this);
}
});
document.writeln("Программа запущена.");
// Создаем объект производного "класса".
var vehicle = new Zaporojets();
vehicle.drive(); // вызывается функция базового объекта
// Создаем еще один объект того же класса.
var vehicle = new Zaporojets();
vehicle.crack(); // функция производного объекта
</script></pre>
Результат работы данного примера кардинально отличается от того, что было приведено в начале наблы.
Листинг 13
Программа запущена.
Вызван конструктор Zaporojets().
Вызван конструктор Car().
Вызван Zaporojets.drive()
Вызван Car.drive()
Вызван конструктор Zaporojets().
Вызван конструктор Car().
Вызван Zaporojets.crack()
Как видите, все работает так, как и ожидает программист на «класс-ориентированном» языке: конструктор
Листинг 14
// Вызов конструктора базового объекта.
this.constructor.prototype.constructor.call(this);
// Вызов переопределенного метода базового объекта.
this.constructor.prototype.drive.call(this);
// У стандартного метода call() можно указывать
// дополнительные аргументы (после this), которые
// будут переданы функции-члену объекта.
Библиотека
Листинг 15
//
// Create proper-derivable "class".
//
// Version: 1.2
//
function newClass(parent, prop) {
// Dynamically create class constructor.
var clazz = function() {
// Stupid JS need exactly one "operator new" calling for parent
// constructor just after class definition.
if (clazz.preparing) return delete(clazz.preparing);
// Call custom constructor.
if (clazz.constr) {
this.constructor = clazz; // we need it!
clazz.constr.apply(this, arguments);
}
}
clazz.prototype = {}; // no prototype by default
if (parent) {
parent.preparing = true;
clazz.prototype = new parent;
clazz.prototype.constructor = parent;
clazz.constr = parent; // BY DEFAULT - parent constructor
}
if (prop) {
var cname = "constructor";
for (var k in prop) {
if (k != cname) clazz.prototype[k] = prop[k];
}
if (prop[cname] && prop[cname] != Object)
clazz.constr = prop[cname];
}
return clazz;
}