![]() |
![]() |
|
||
![]() |
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [2 декабря 2004 г.] |
|
|
Нет ничего более постоянного, чем временное. Народная мудрость |
Если помните, предыдущая набла закончилась полгода назад на том, что при программировании на 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" говорит нам, что программа заработала. Однако какой ценой? Свойство
Какие выводы можно сделать из примера?
|
Новый объект в 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;
} |
![]() |
| ||||||||||||||||||||||||||||
| Дмитрий Котеров | 2 декабря 2004 г. ©1999-2012 | | Контакт | Вернуться к оглавлению |