Простой способ сделать «дешёвый» Private в JS

В общем-то способ создавать члены класса, которые будут недоступны извне, в JavaScript существует и называется замыканием. Но при таком подходе есть один серьёзный минус — создаётся много экземпляров одной и той же функции, что не есть хорошо для ресурсов.

Я немного поразмышлял, как сделать всё просто и в то же время эффективно. Сначала я нашёл решение, которое скорее экономит ресурсы, чем избавляет о «перерасхода». Вот оно:

  1. var Class_privateMethod = function (_this_privateVar)
  2. {
  3.     alert(_this_privateVar);
  4.     return null;
  5. }
  6.  
  7. var Class_publicMethod = function (_this_privateVar)
  8. {
  9.     return Class_privateMethod.call(this, _this_privateVar);
  10. }
  11.  
  12. var Class = function ()
  13. {
  14.     var privateVar = 'Hello, world!';
  15.     this.publicMethod = function () { return Class_publicMethod.call(this, privateVar); }
  16. }
* This source code was highlighted with Source Code Highlighter.


Здесь мы экономим ресурсы за счёт того, что создаваемая каждый раз в конструкторе функция состоит всего из одной строки — вызова настоящего тела метода. Но функции продолжают плодиться, пость они и стали меньше.

Как ещё можно реализовать закрытые члены? Можно вынести их в элемент какого-нибудь глобального массива, создаваемый для каждого объекта. Но объект нужно как-то идентифицировать. Но сделать такой идентификатор в виде поля объекта нельзя — поле можно изменить (а мы, напоминаю, пытаемся закрыть методы — иначе можно просто определить для них какой-нибудь префикс типа l_privateMethod и успокоиться). Никакой уникальной характеристики, кроме адреса в памяти и/или номера в списке интерпретатора, у объекта нет, а к этим характеристикам мы доступ получить не можем. Объект не может являться индексом в массиве… или стоп! Может, если это объект Number! Таким образом мы приходим к весьма простому но эффективному решению:

  1. var ObjectServer = function ()
  2. {
  3.     var objects = new Array(), CURRENT_ID = 1;
  4.     
  5.     var result = function (_id)
  6.     {
  7.         if(!(_id instanceof Number))
  8.         {
  9.             return;
  10.         }
  11.         if(objects[_id].external != _id)
  12.         {
  13.             return;
  14.         }
  15.         return objects[_id];
  16.     }
  17.     
  18.     result.create = function (_class)
  19.     {
  20.         var object = new _class();
  21.         object.external = new Number(CURRENT_ID);
  22.         for(var key in _class.external)
  23.         {
  24.             object.external[key] = _class.external[key];
  25.         }
  26.         objects[CURRENT_ID ++] = object;
  27.         return object.external;
  28.     }
  29.     
  30.     result.destroy = function (_id)
  31.     {
  32.         if(!(_id instanceof Number))
  33.         {
  34.             return;
  35.         }
  36.         if(objects[_id].external != _id)
  37.         {
  38.             return;
  39.         }
  40.         delete objects[_id];
  41.     }
  42.     
  43.     return result;
  44. }
  45.  
  46. var objs = ObjectServer();
* This source code was highlighted with Source Code Highlighter.


Теперь настоящие объекты у нас хранятся в массиве objects сервера объектов, а вовне отдаётся только публичная (external) часть. Чтобы создать объект на основе функции-конструктора класса, нужно написать objs.create, чтобы удалить — objs.destroy (кстати появляется шикарная возможность включить сюда деструкторы, если немного доработать решение — ссылка на объект будет храниться в массиве до тех пор, пока мы её явно не удалим). Чтобы обратиться к закрытой части объекта, нужно воспользоваться самой функцией objs:

objs(myObject).privateMember;

Внешние методы объявляются как члены подъобъекта external так же, как закрытые — как члены подобъекта prototype. Единственное ограничение — параметров у конструктора быть не должно, т.к. нельзя писать new constructor.call(...).

Вот как можно использовать этот механизм для реализации некоторого модуля:

  1. var global = this;
  2.  
  3. (function(){
  4.  
  5. var objs = ObjectServer(); // Функция ObjectServer определена где-то снаружи.
  6.  
  7. var Class = function ()
  8. {
  9.     this.privateVar = 'I\'m a private variable.';
  10. }
  11.  
  12. Class.prototype.privateMethod = function ()
  13. {
  14.     alert(this.privateVar);
  15.     alert(this.external.publicPrimitiveVar);
  16.     this.external.publicCompoundVar.push(this.privateVarDefinedInConstructor);
  17.     alert(this.external.publicCompoundVar);
  18. }
  19.  
  20. Class.external = new Object();
  21.  
  22. Class.external.publicPrimitiveVar = 10;
  23.  
  24. Class.external.publicMethod = function ()
  25. {
  26.     objs(this).privateMethod();
  27.     objs(this).privateVar = 'Changed!';
  28.     objs(this).privateMethod();
  29. }
  30.  
  31. global.Class = function (_arg)
  32. {
  33.     var result = objs.create(Class);
  34.     result.publicCompoundVar = new Array('a', 'b', 'c');
  35.     objs(result).privateVarDefinedInConstructor = _arg;
  36.     return result;
  37. }
  38.  
  39. })();
  40.  
  41. var object1 = Class('d');
  42. var object2 = Class('e');
  43. object2.publicPrimitiveVar = 50;
  44. object1.publicMethod();
  45. object2.publicMethod();
  46. alert(object1.privateMethod);
  47. object2 = new Number(object2 * 1);
  48. object2.publicMethod();
* This source code was highlighted with Source Code Highlighter.


Вызвав извне global.Class (или просто Class — это имя глобального пространства) мы получим публичный интерфейс класса, в то время как закрытый интерфейс останется только в анналах сервера объектов — посмотрите сами, alert(object1.privateMethod); отобразит undefined. Главное не забывать определять сервер объектов внутри модуля, чтобы он не был доступен извне. Функцию создания сервера можно определить один раз где-нибудь снаружи в наборе утилитарных функций. И можно не опасаться, что кто-то переопределит номер объекта — сервер откажет такому запросу (см. две последние стоки).

Надеюсь, что способ вам пригодится, благо он прост как три копейки и позволяет не расходовать память и время зазря (ну разве что на указатели на функции). Если кто-то его модифицирует, чтобы можно было делать ещё и защищённые (protected) члены для переклички между разными модулями, то будет вообще замечательно. Ну а я на сём кланяюсь! Спасибо за ваше внимание!

UPD 02.12.09

При помощи вот такой модификации можно делать функции-конструкторы с параметрами (т.е. objs.create(Class, 10, 20)):

  1. ObjectServer.create = function (_class)
  2. {
  3.     var constructor = new Function();
  4.     constructor.prototype = _class.prototype;
  5.     var object = new constructor();
  6.     object.constructor = _class;
  7.     object.external = new Number(this.CURRENT_ID);
  8.     for(var key in _class.external)
  9.     {
  10.         object.external[key] = _class.external[key];
  11.     }
  12.     this.objects[this.CURRENT_ID ++] = object;
  13.     _class.apply(object, Array.prototype.slice.call(arguments, 1));
  14.     return object.external;
  15. }
* This source code was highlighted with Source Code Highlighter.


UPD 02.12.09

Спасибо dsCode за совет, я исправил недостаток библиотеки в части копирования external-методов. Теперь выигрыш по сравнению со стандартным решением по времени составляет около 6000% на том же тесте (см. комментарии) :^) (0,864 против 0,013, т.е. в ~ 60 раз). Библиотеку можно найти здесь:
библиотека JOS
пример
P.S. Теперь нужно запомнить ещё два ключевых слова: external и id. Их не стоит использовать в качестве идентификаторов полей объекта.
–1
2 декабря 2009, 12:09
7
akaj 9,0

комментарии (9)

+1
kriomant #
Здесь мы экономим ресурсы за счёт того, что создаваемая каждый раз в конструкторе функция состоит всего из одной строки — вызова настоящего тела метода.

Весьма странное понимание проблемы. Код функций никогда не копируется, поэтому от их размера ничего не зависит. Повышенное потребление памяти связано с тем, что каждый экземпляр функции хранит свое окружение (closure), т.е. ссылки на переменные из внешних областей видимости.

В данном же случае хранение ссылок на переменные заменяется копированием методов приватного объекта в публичный, что сравнимо по объему. Так что весьма сомнительно, что это даст какой-нибудь выигрыш. Тесты прогоняли?

0
dsCode #
> Код функций никогда не копируется, поэтому от их размера ничего не зависит.

Зависит от типа функции. Стандарт предусматривает оптимизацию в виде объединённых объектов, но там должны соблюдаться определённые условия. Новая версия стандарта (ECMA-262-5, текущий драфт) определяет внутреннее свойство [[Code]], которое может реюзаться между (объединёнными) функциями, но в текущей версии стандарта об этом ничего не сказано (хотя, на практике, я думаю, реализации делают оптимизацию, включая предложенную с объединёнными объектами). Но, повторю, зависит это от типа функций. Разбирал это в пятой части статей об ECMAscript.
0
akaj #
kriomant, в принципе вы правы, создаётся новое замыкание (а дублирование кода действительно зависит от браузера). Но я, очевидно, немного неверно сформулировал мысль: обратите внимание, что все private-методы выносятся за пределы определения класса, и, следовательно, сам класс о них ничего не знает и никак их не дублирует — а как правило таких методов больше, чем публичных, работающих с ними (это уже просто практика), которые и создаются в конструкторе. Тесты показывают, что выигрыш действительно есть — где-то в 1,8 раза (например, 0,768 против 0,454 для создания 1000 объектов с 1000-ей закрытых и 1000-й открытых методов).

А вот с приведённым решением с сервером объектов, ради которого статья и затевалась, получается интереснее — это решение работает приблизительно в 7 раз быстрее во всех браузерах, кроме Firefox (там медленнее)… но только при первом прогоне — потом (при обновлении страницы) и лис прекрасно обрабатывает его.

Такая вот получается картина.
0
akaj #
Простите, забыл упомянуть: ну а по памяти, я думаю, выигрыш очевиден.
+1
dsCode #
Интересное решение, своеобразное ;). А какова основная цель? Публичные (external) свойства копируются, что увеличивает расход ресурсов.

> if(objects[_id].Eexternal

Опечатка.

> И можно не опасаться, что кто-то переопределит номер объекта — сервер откажет такому запросу (см. две последние стоки)

Ну блин, ещё бы, совершенно новый независимый объект Number создаётся, никак не связанный с данной обёрткой (не имеющий расширенных, скопированных свойств) ;)

В целом, не считая недочётов, что extenral stuff копируется в каждый новый объект, а не наследуется, а также, не считая того, что в некоторых реализациях, всё-таки, можно получить доступ к инкапсулированному var-у «objs» и обратиться к нужному «скрытому» объекту, — идея своеобразно-интересная ;)

P.S.: немного затрагивал тему инкапсуляции (с позиции восприятия) в седьмой части статей об ECMAscript.
0
akaj #
> Ну блин, ещё бы, совершенно новый независимый объект Number

На самом деле индексом в массиве объектов является примитив, так что проверка нужна.

> extenral stuff копируется в каждый новый объект, а не наследуется

Я сейчас как раз ищу решение, как его унаследовать. :^) Это, я надеюсь, позволит избавиться от перерасхода ресурсов и от тормозов в лисе при первом прогоне. Спасибо за совет.

> в некоторых реализациях, всё-таки, можно получить доступ к инкапсулированному var-у «objs»

А как? Чесслово, интересно. Может, это позволит сделать что-нибудь интересное. :^)
+1
dsCode #
> На самом деле индексом в массиве объектов является примитив, так что проверка нужна.

Да нет, я имею в виду, Вы пишите:

# object2 = new Number(object2 * 1);
# object2.publicMethod();



И можно не опасаться, что кто-то переопределит номер объекта — сервер откажет такому запросу (см. две последние стоки)


И так понятно, что «object2» теперь совершенно независимый (новый) объект и у него нет никакого метода «publicMethod», зачем писать про то, что «сервер откажет», если object2 никак не связан с сервером?

Ну и (просто для справки) — ключами объекта (включая массивы) являются всегда строки, т.е. не просто примитив, а именно примитив-строка.

> А как? Чесслово, интересно

Например, в Spidermonkey до версии 1.7, eval-у можно передать вызывающий контекст, тем самым получая доступ к внутреннему объекту переменных.

function A() {
  var x = 10; // "private"
  this.getX = function () {
    return x;
  };
}

var a = new A;
a.getX(); // 10

eval('x = 20', a.getX);
a.getX(); // 20
0
akaj #
Благодарю за совет — я доработал библиотеку. Выигрыш в 60 раз без расходования памяти. :^)

> т.е. не просто примитив, а именно примитив-строка
Я знаю, но в данном случае проверяется всё равно на instanceof Number. :^)

> И можно не опасаться, что кто-то переопределит номер объекта — сервер откажет такому запросу (см. две последние стоки)

Да, я здесь лишнего написал. Извините.

> можно передать вызывающий контекст

От таких вещей, к сожалению, защититься не получится. По крайней мере, сейчас я не знаю — как.
0
akaj #
Да, и спасибо за указание на опечатку — исправил.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.