function A() {
var /* number */ x = 5;
this.getX = function() { return x;}
this.setX = function(newX) {
if (typeof x == 'number')
x = newX;
else
throw new Error('Param is not a number!');
}
}
var y = new A();
alert(y.getX()); // Вернёт 5
alert(y.x); // Вернёт 'undefined'
Вроде бы, выглядит логично, не правда ли? Вот только есть одна проблема, на которую в своё время обратил моё внимание участник форума Vingrad с ником AKS: каждый объект содержит весь набор методов своего класса, и если объектов будет достаточно много, то этот метод приведёт к очень неэкономному расходованию памяти.JavaScript относится к языкам с ООП, основанным на прототипах (prototype-based OOP). В данном случае это значит, что у объектов с большим количеством методов, что бы сэкономить память, рекомендуется эти методы выносить в общий для них прототип. Это легко сделать, отказавшись от инкапсуляции и сделав эти поля открытыми, что бы методы прототипа каждый раз могли их считывать в зависимости от того, у какого объекта они вызваны. Но как добиться этого, не отказываясь от инкапсуляции для этих полей?
Т.е. существует противоречие между инкапсуляцией на основе замыканий и принципами ООП на базе прототипов.
Материалов на данную тему мне ещё нигде не попадалось, по-этому я решил сам заняться разработкой этой проблемы. Данный цикл статей посвящается описанию моих попыток разрешения этого противоречия.
Итак, наиболее удобным контейнером для методов класса является, конечно, его прототип (в нашем случае -
A.prototype
). На него ссылаются все объекты данного класса и, если вызываемый метод не находится в них, то следует вызов метода из их прототипа. Так что задача состоит в том, что бы позволить методам прототипа работать с закрытыми полями объектов, из которых эти методы вызываются.Объект-прототип является чужим для того замыкания, в котором находятся поля. Он связан с ним неявной ссылкой (в FF она носит название "
__proto__
"), но она не поможет ему считать закрытые переменные. Так что же делать?С другой стороны, объекту, который не содержит методов для работы со своими закрытыми полями, иметь эти поля и необязательно. Что он будет с ними делать, кроме как передавать и извлекать их из прототипа, если всё равно все методы для работы с ними находятся в прототипе? Ничего. Т.е. на самом деле они ему не нужны - ему нужен только персональный механизм, позволяющий загружать свой набор закрытых полей в прототип и затем сохранять их из прототипа.
Поэтому решение может состоять в том, что бы создать для каждого объекта замыкание, которое будет иметь доступ к переменным в прототипе и обладать собственными (дублирующими их) свойствами и сможет осуществлять подмену. Что бы это замыкание имело доступ к переменным замыкания прототипа, оно должно быть внутренним по отношению к нему, а объекты класса могут просто иметь на него ссылку.
Тогда какими должны быть требования к этому вспомогательному замыканию? Оно должно уметь делать две вещи:
- Устанавливать в закрытые переменные прототипа значения закрытых переменных объекта;
- Записывать в закрытые переменные объекта значения закрытых переменных прототипа (после того, как прототип произвёл с ними некоторые действия).
- метод "get" выполняет взятие значений переменных из объекта, при этом самим переменным объекта неплохо было бы присвоить значение null, что бы не иметь дублирующихся ссылок;
- метод "set" устанавливает значения обратно в объект, об-null-яя аналогичные переменные в прототипе.
Итак, что нам в итоге нужно:
- метод для генерации внутреннего замыкания в прототипе объекта, который будет возвращать объект с двумя методами: get и set, которые, соответственно, будут устанавливать в прототип значения закрытых полей из этого объекта и в объект - из прототипа;
- механизм вызова для методов прототипа, гарантирующий, что перед выполнением будут установлены значения закрытых полей.
Итак, метод генерации privateState-объекта внутри прототипа может выглядеть примерно так:
// Переменная прототипа, в которую будем помещать значение private-поля
// объекта каждый раз, когда будем выполнять какой-либо его метод.
var x;
// Функция, являющаяся конструктором provateState-объекта
this.createPrivateState = function() {
//"_x" - переменная для хранения private-поля объекта
var _x;
return {
get: function() {
//Присвоение переменной прототипа значения private-поля объекта
x = _x;
// Об-null-ение переменной объекта на время, пока её значение
// находится в прототипе, во избежание дублирующейся информации
_x = null;
},
set: function() {
// Возвращение значения private-полю объекта после использования
_x = x;
//Об-null-ение переменной прототипа
x = null;
}
}
}
При этом нам нужно так же продумать механизм вызова методов. В наиболее простейшем варианте мы могли бы сделать это так:this.method1 = function() {
this.privateState.get();
//здесь располагается код метода, работающего с private-полями...
this.privateState.set();
}
Тогда в конструкторе объекта мы могли бы просто вызывать метод createPrivateState() прототипа:function A() {
this.privateState = this.createPrivateState();
}
A.prototype = new function() {
var x;
this.createPrivateState = function(){...} //Описан выше
this.method1 = function() {...} //Описан выше
//Другие методы...
}
A._className = "A"; //Записываем в специальное свойство класса его имя
A.prototype.constructor = A; //Присваиваем прототипу ссылку на конструктор
Однако для промышленного применения этот метод не годится по ряду причин. Во-первых, мы не учли ситуации, когда один метод вызывает другой метод того же объекта (что делается довольно часто), во-вторых мы не учли, что в методе может происходить вызов метода другого объекта этого же класса. И, наконец, механизм вызова, состоящий из двух строчек вначале и в конце объявления метода выглядит излишне-громоздким. Тому, как наиболее элегантно справиться с этими и более мелкими проблемами, будут посвящены следующие статьи цикла "Особенности инкапсуляции на основе замыканий". :)Другие части:
Часть 2: Механизм вызова методов.
Часть 3: Внутренние вызовы.
Часть 4: Наследование.
Комментариев нет:
Отправить комментарий