В предыдущих статьях данного цикла (1, 2, 3 части) мной была пошагово продемонстрирована разработка "базовой модели", которая позволяет реализовать структуру, предоставляющую возможность экономно создавать экземпляры классов сложных объектов, разруливая проблему совмещения инкапсуляции с ООП на основе прототипов. Конечно, само по себе решение этой проблемы довольно-таки важно, но истинную мощь "базовая модель" получит только тогда, когда будет дополнена механизмами наследования, поскольку одноуровневые объектные структуры весьма ограничены в рамках дизайна больших приложений (на которые и нацелена "базовая модель").
Итак, давайте разберёмся, что реально означает наследование одного класса от другого на уровне работы кода?
Если смотреть извне, то это означает, что любой код, нормально работающий с объектом унаследованного класса, должен продолжать нормально работать с объектом его потомка. Т.е. у объекта потомка присутствуют все методы объекта родителя и они внешне (т.е. на уровне типов возвращаемых значений, принимаемых параметров и возбуждаемых исключений) ведут себя так же.
Если же смотреть на наследование изнутри, то наследник:
- Должен в конструкторе первым делом вызывать конструктор класса-предка.
- Всегда иметь ссылку на экземпляр созданного в конструкторе класса-предка в переменной
super
. - Некоторые public-методы могут быть переопределены и добавлены новые. То же самое в отношении public-полей.
- Существует возможность открывать потомкам доступ к некоторым функциям и полям (модификатор
protected
в Java). - Только те исключения (и их потомков), которые мог вызывать переопределённый метод, может вызывать метод переопределяющий. Либо последний исключений может не вызывать вовсе.
super
в JavaScript является зарезервированным и поэтому его использовать не получится, довольствуясь лишь _super
, что вобщем-то даже имеет положительную сторону - будет сразу восприниматься в качестве особой переменной.Ну а по поводу 4 пункта, честно признаюсь, сколько ни думал о том, как это реализовать с применением замыканий, так ничего путного и не надумал. Возможно, я придумаю, как можно хитро вывернуться, всё-таки реализовав это, но пока давайте про этот пункт забудем и реализуем наследование без него, считая, что такого модификатора у нас нет.
Принципиально важным, на мой взгляд, является возможность реализации наследования так, что бы не замыкать её только на объекты, созданные в соответствии с базовой моделью, а дать возможность использовать в отношении любых конструкторов пользовательских объектов.
Итак, как нам нужно теперь модифицировать наш код, что бы стало возможным использование наследования? Для начала давайте посмотрим на схему:
Мы видим, что прототип ссылается неявной ссылкой (доступной в FireFox по имени
__proto__
и недоступной в других браузерах - по-этому я называл её здесь именно так) на объект-прототип класса-предка, а на экземпляр ссылается только ссылка _super
.Первое изменение коснётся механизма создания прототипа. Т.к. разработчики спецификации ECMAScript не оставили нам возможности напрямую контролировать ссылку
__proto__
, мы вынуждены хитрить, что бы заставить её ссылаться на то, что нам будет нужно.В Базовой модели конструктор прототипа выглядел так:
A.prototype = new function() {/*здесь код конструктора прототипа*/}
A.prototype.constructor = A;
Таким образом, у нас создавалась безымянная (анонимная) функция и тут же использовалась в качестве конструктора нового объекта-прототипа для класса A
. Затем, что бы не нарушать принятые в JavaScript договорённости относительно взаимных ссылок между конструкторами и прототипами, мы выставляли свойству constructor
прототипа ссылку на класс.Теперь же, учитывая, что мы не можем напрямую контролировать ссылку
__proto__
, единственная возможность для нас прописать в неё то значение, которое нам нужно, состоит в том, что бы функция, создающая объект, который должен ссылаться посредством __proto__
туда, куда нам надо, должна до создания объекта ссылаться туда свойством prototype
:function X() {}
function Y() {this.z = 5;}
var x1 = new X;
X.prototype = new Y;
var x2 = new X;
alert(x1.z); //Выведет 'undefined'
alert(x2.z); //Выведет '5';
Т.е. получается, что функция, создающая для нас прототип, теперь не сможет быть безымянной. А как не хочется засорять пространство имён ещё одной переменной... По-этому я предлагаю немного схитрить - присвоить прототипу сначала эту функцию, а потом, произведя все нужные изменения, присвоить ему уже её результат. Выглядеть это будет вот так:A.prototype = function() {/*...*/}
A.prototype.prototype = B.prototype; // B - конструктор, от которого мы наследуем A
A.prototype = new A.prototype;
A.prototype.constructor = A;
На выходе получим прототип, ссылающийся ссылкой __proto__
на прототип конструктора-предка.Теперь осталось разобраться со ссылкой
_super
. Она будет обладать похожим поведением на ссылку _this
- так же будет переменной, имеющий двойника - переменную __super
, являющуюся полем объекта. Так же, как и ссылку __this
, её не будет смысла очищать в методе privateState.get()
и, соответственно, наполнять в privateState.set()
. Так же она будет константой для пользователя.Кроме того, она будет присваиваться вызову конструктора-предка вначале метода pcreatePivateState и в нём же из неё будут извлекаться все свойства и присваиваться ссылке
__this
- если они, конечно, не будут переопределены в классе-потомке.Теперь соберём всё вместе и протестируем. Вот, какая получается расширенная модель:
function B() {
var y = 15;
this.getY = function(){
return y;
}
this.setY = function(_y) {
y = _y;
return this;
}
}
/**
* @author Vyacheslav Lapin
* @version 0.01 09.04.2009 21:10:04
*
* @constructor
* @extends B
*
*
* @param {number} x +
*/
function A(x) {
if (this.constructor !== arguments.callee)
return new arguments.callee(x);
this.privateState = this.createPrivateState(x);
}
A.prototype = function() {
var /** @type {A} */ _this,
/** @type {B} */ _super,
/**
* @static
* @type {Array<A>}
*/
__callInstances = [];
this.createPrivateState = function(__x) {
//Переменные для хранения private-полей объекта
var /** @type {number} */ _x = __x,
/**
* @constant
* @type {A}
*/
__this = this,
/**
* @constant
* @type {B}
*/
__super = new B(); // Вызываем конструктор класса-предка
this.createPrivateState = null;
// Отдельный функциональный блок для того, что бы не заводить в
// closure ещё одно поле - переменную i
(function() {
//Здесь мы перечисляем свойства экземпляра класса-предка
for (var /** @type {string} */ i in __super)
// Проверяем, собственное это свойство экземпляра super-класса
// или унаследованное? Ведь унаследованные свойства нам не нужны -
// мы и так их унаследуем по цепочке прототипов, если они не будут
// переопределены в нашем прототипе, а тогда - унаследуем
// переопределённые, что нас вполне устроит.
if (__super.hasOwnProperty(i)
// Так же проверяем, есть ли такие же в прототипе
&& !this.constructor.prototype.hasOwnProperty(i)
// или в самом объекте?
&& !this.hasOwnProperty(i)
)
try {
this[i] = __super[i];
} catch (/** @type {Error} */ e) { }
})();
return {
get: function() {
if (_this) {
__callInstances.push(_this);
_this.privateState.set();
}
x = _x;
//...
_this = __this;
_super = __super;
_x =
//...
null;
},
set: function() {
_x = x;
//...
x =
//...
_this =
_super = null;
if (__callInstances.length
&& __callInstances[__callInstances.length - 1] !== __this)
__callInstances.pop().privateState.get();
}
}
}
/**
* @param {Arguments} args +
*/
function methodCaller(args) {
this.privateState.get();
var result = args.callee.apply(_this, args);
this.privateState.set();
return result;
}
//--------------------------------------------------------------------------
var /** @type {number} */ x;
//...
/**
* @return {number}
*/
this.getX = function() {
if (_this !== this) return methodCaller.call(this, arguments);
return x;
}
/**
* @param {number} _x
* @return {A} this
*/
this.setX = function(_x) {
if (_this !== this) return methodCaller.call(this, arguments);
x = _x;
return this;
}
/**
* @override
*/
this.setY = function(_y) {
if (_this !== this) return methodCaller.call(this, arguments);
alert('Hello from A!');
_super.setY(_y);
return this;
}
}
/** @type {string} */ A._className = "A";
A.prototype.prototype = B.prototype;
A.prototype = new A.prototype;
A.prototype.constructor = A;
// Импортируем в конструктор-потомок так же статические поля
// конструктора-предка- свойства класса
(function() {
for (var /** @type{string} */ i in B)
if (!(i in A))
try {
A[i] = B[i];
} catch (/** @type {Error} */ e) { }
})();
//test
var a = new A(10);
alert(a.getY()); //Выводит '15'
alert(a.setY(20).getX()); // Выводит сначала 'Hello from A!', затем 10
alert(a.getY()); //Выводит '20'
Вот и всё. Можно теперь делать любые сложные деревья наследования в JavaScript в полном соответствии с принципами инкапсуляции.В дальнейших статьях цикла мы поговорим о том, как упростить разработку конструкторов по базовой и расширенной модели, поскольку у меня нет иллюзий о том, что вручную писать такой код не слишком-то удобно.
P.S. Напоследок хочу предостеречь вас от попытки на радостях наследовать от системных конструкторов - к сожалению, с ними может не пройти выражение:
__this[i] = __super[i];
Дело в том, что они не являются в полном смысле JavaScript`овыми объектами и для них такой возможности часто не предусмотрено. Например, мне не удалось наследовать от XMLHttpRequest
- там это выражение вылетало с ошибкой даже при том, что я обромил его блоком try/catch
:try {__this[i] = __super[i];} catch(/** @type {Error} */ e) {}
Всё равно, даже такой бронебойный код вываливался с ошибкой в браузере. По крайней мере в FF это дело у меня не прошло. Так что ещё раз повторюсь - расширенная модель предназначена для наследования от родных JavaScript-конструкторов объектов. Будьте осторожны!Другие части:
Часть 1: Основа Базовой модели
Часть 2:Механизм вызова методов
Часть 3: Внутренние вызовы
Комментариев нет:
Отправить комментарий