Как стать автором
Обновить

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

Время на прочтение6 мин
Количество просмотров1.4K
В общем-то способ создавать члены класса, которые будут недоступны извне, в 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. Их не стоит использовать в качестве идентификаторов полей объекта.
Теги:
Хабы:
Всего голосов 11: ↑5 и ↓6-1
Комментарии9

Публикации

Истории

Работа

Ближайшие события