Статьи

Частные данные для объектов в JavaScript

JavaScript не поставляется с выделенными средствами для управления частными данными для объекта. В этом посте описаны пять методов обхода этого ограничения:

  1. Экземпляр конструктора — приватные данные в среде конструктора
  2. Объект Singleton — приватные данные в среде обтекания объектов IIFE
  3. Любой объект — приватные данные в свойствах с помеченными именами
  4. Любой объект — приватные данные в свойствах с реализованными именами
  5. Единственный метод — приватные данные в среде метода-обертки IIFE

В следующих разделах каждый метод объясняется более подробно.

Необходимые знания: хотя все объясняется относительно медленно, вы, вероятно, должны быть знакомы со средами и IIFE [1], а также с наследованием и конструкторами [2] .

Экземпляр конструктора — приватные данные в среде конструктора

Этот подход работает следующим образом: когда вызывается конструктор, создаются две вещи: экземпляр конструктора и среда. Экземпляр должен быть инициализирован конструктором. Среда содержит параметры конструктора и локальные переменные. Каждая функция (которая включает методы), созданная внутри конструктора, сохранит ссылку на среду — среду, в которой она была создана. Благодаря этой ссылке он всегда будет иметь доступ к среде, даже после завершения конструктора. Окружающая среда будет жить до тех пор, пока на нее есть ссылка. Эта комбинация функции и среды называется
замыканием , потому что среда «закрывается» над свободной функцией
переменные, переменные, которые не являются локальными для него. Таким образом, среда конструктора — это хранилище данных, которое не зависит от экземпляра и связано с ним только потому, что оно создается одновременно. Чтобы соединить их, должны быть функции, которые живут в обоих мирах. Используя
терминологию Крокфорда , экземпляр может иметь три вида данных, связанных с ним:

  1. Открытые свойства: данные, хранящиеся в экземпляре, являются общедоступными.
  2. Личные данные: данные, хранящиеся в среде, доступны только конструктору и функциям, созданным внутри него.
  3. Привилегированные методы: частные функции могут получать доступ к общедоступным свойствам, но общедоступные методы обычно не могут обращаться к частным данным Таким образом, нам нужны специальные привилегированные методы — функции, созданные в конструкторе, которые добавляются в экземпляр. Привилегированные методы являются общедоступными и поэтому могут просматриваться непривилегированными методами, но они также имеют доступ к частным данным.

В следующих разделах эти три вида объясняются более подробно.

1.1 Публичные свойства

Помните, что, учитывая конструктор Constr, существует два вида общедоступных свойств , доступных каждому. Во-первых, свойства прототипа хранятся в объекте, который является прототипом всех экземпляров, его свойства являются общими для них. Этот объект также доступен через Constr.prototype. Свойства прототипа обычно являются методами.

Constr.prototype.publicMethod = ...;

Во-вторых, свойства экземпляра уникальны для каждого экземпляра. Они добавляются в конструктор и обычно «поля» (содержат данные, а не методы).

function Constr(...) {
    this.publicField = ...;
}

 

1.2 Личные данные

Среда конструктора состоит из параметров и локальных переменных. Они доступны только из конструктора и, таким образом, являются частными для экземпляра.

    function Constr(...) {
        var that = this; // hand to private functions

        var privateValue = ...;

        function privateFunction(...) {
            privateValue = ...;
            that.publicField = ...;
            that.publicMethod(...);
        }
    }

Если вам не нравится, что = этот обходной путь, выше, у вас есть возможность использовать bind (caveat: потребляет больше памяти):

    function Constr(...) {
        var privateValue = ...;

        var privateFunction = function (...) {
            privateValue = ...;
            this.publicField = ...;
            this.publicMethod(...);
        }.bind(this);
    }

 

1.3 Привилегированные методы

Private data is so safe from outside access that prototype methods can’t access it. But then how else would you use it, after leaving the constructor? The answer are privileged methods: Functions created in the constructor are added as instance-specific methods. They can thus access the private data and be seen by prototype methods.

    function Constr(...) {
        this.privilegedMethod = function (...) {
            ...
        };
    }

1.4 Analysis

  • Not very elegant: Mediating access to private data via privileged methods introduces an unnecessary indirection. Privileged methods and private functions both destroy the separation of concerns between the constructor (setting up instance data) and its prototype property (methods).
  • Completely safe: There is no way to access the environment’s data from outside. Which makes this solution secure if you need to guarantee that (e.g. for security-critical code). On the other hand, private data not being accessible to the outside can also be an inconvenience: Sometimes you want to unit-test private functionality. And some temporary quick fixes depend on the ability to access private data. This kind of quick fix cannot be predicted, so no matter how good your design is, the need can arise.
  • Possibly slower: Accessing properties in the prototype chain is highly optimized in current JavaScript engines. Accessing values in the closure might be slower. But these things change constantly, so you’ll have to measure, should this really matter for your code.
  • Memory consumption: Keeping the environment around and putting privileged methods in instances costs memory. Again: Be sure it really matters for your code and measure.

 

2. Singleton object – private data in environment of object-wrapping IIFE

If you work with singletons, the technique of putting private data in an environment can still be used. But, as there is no constructor, you’ll have to wrap an immediately-invoked function expression (IIFE, [1]) around the singleton to get such an environment.

    var obj = function () {  // open IIFE
        
        // public
        var that = {
            publicMethod: function (...) {
                privateValue = ...;
                privateFunction(...);
            },
            publicField: ...
        };

        // private
        var privateValue = ...;
        function privateFunction(...) {
            privateValue = ...;
            that.publicField = ...;
            that.publicMethod(...);
        }
        
        return that;
    }(); // close IIFE

Public methods can access private data, as long as they are invoked after it has been added to the environment.

 

3. Any object – private data in properties with marked names

For most non-security-critical applications, privacy is more like a hint to clients of an API: “You don’t need to see this”. That’s the core benefit of encapsulation: Hiding complexity. Even though more is going on under the hood, you only need to understand the public part of an API. The idea of a naming convention is to let clients know about privacy by marking the name of a property. A prefixed underscore is often used for this purpose. The following example shows a type StringBuilder whose property _buffer is private, but by convention only.

    function StringBuilder() {
        this._buffer = [];
    }
    StringBuilder.prototype = {
        constructor: StringBuilder,
        add: function (str) {
            this._buffer.push(str);
        },
        toString: function () {
            return this._buffer.join("");
        }
    };

Interaction:

    > var sb = new StringBuilder();
    > sb.add("Hello");
    > sb.add(" world");
    > sb.add("!");
    > sb.toString()
    ’Hello world!’

 

3.1 Analysis

  • Natural coding style: With the popularity of putting private data in environments, JavaScript is the only mainstream programming language that treats private and public data differently. A naming convention avoids this slightly awkward coding style.
  • Property namespace pollution: The more people use IDEs, the more it will be a nuisance to see private properties where you shouldn’t. Naturally, IDEs could adapt to that and recognize naming conventions and when private properties shouldn’t be shown.
  • Private properties can be accessed from outside: Applications include unit tests and quick fixes. But it also gives you more flexibility as to who data should be private too. You can, for example, include subtypes in your private circle, or “friend” functions. With the environment approach, you always limit access to functions created inside the scope of that environment.
  • Name clashes: private names can clash. This is already an issue for subtypes, but it becomes more problematic with some kind of multiple inheritance (e.g. via mixins or traits).

 

4. Any object – private data in properties with reified names

Одна проблема с соглашением об именах для частных свойств заключается в том, что имена могут конфликтовать. Вы можете уменьшить вероятность таких столкновений, используя более длинные имена, например, включающие в себя имя типа. Затем, выше, частное свойство _buffer будет называться _StringBuilder_buffer. Если такое имя слишком длинное на ваш вкус, у вас есть возможность
придать ему новое значение , превратить в нечто (что в буквальном смысле означает овеществление).

    var buffer = "_StringBuilder_buffer";

Если раньше мы использовали имя напрямую, то теперь это значение («вещь», упомянутая выше), хранящееся в буфере переменных. Теперь мы получаем доступ к приватным данным через этот [буфер].

    var StringBuilder = function () {
        var buffer = "_StringBuilder_buffer";
        
        function StringBuilder() {
            this[buffer] = [];
        }
        StringBuilder.prototype = {
            constructor: StringBuilder,
            add: function (str) {
                this[buffer].push(str);
            },
            toString: function () {
                return this[buffer].join("");
            }
        };
        return StringBuilder;
    }();

Мы обернули IIFE вокруг StringBuilder, чтобы буфер переменных оставался локальным и не загрязнял глобальное пространство имен.

4.1 ECMAScript.next и проверенные имена

Предложение ECMAScript.next «
объекты частных имен » продвигает идею усовершенствованных имен на один шаг вперед. Имена теперь также могут быть объектами, так называемыми
частными объектами имен . Будет имя модуля с функцией create (), которая позволяет вам создавать такие объекты:

    var buffer = name.create();

Each invocation of name.create() produces a new name object that is unique – different from any other name object created in this manner. Until the end of this section, we use the term “private property” as an abbreviation for “a property whose name is a private name object”. Compared to string names, name objects have two advantages:

  • Hidden: Private properties don’t show up when examining an object with the usual tools (Object.keys, propName in obj, etc.), they don’t pollute an object’s property name space.
  • Inaccessible: Partially as a consequence of hiding, one can only access a private property if one “has” its name object. That makes it secure: One can control precisely who has access. That control is also an advantage compared to using environments for privacy: You can now grant someone access, e.g. a unit test to check that a private method works properly.

That means: You get elegant code and security while being able to control precisely who sees what. You can, for example, let unit test code see the private name object, but no one else.

 

4.2 Analysis

You get all the advantages of naming conventions, while avoiding name clashes – at the expense of having to manage the reified names.

 

5. Single method – private data in environment of method-wrapping IIFE

Иногда вам нужны только личные данные для одного метода. Затем вы можете использовать ту же технику, что и при совместном использовании среды конструктора: присоединить среду к методу, использовать его для хранения данных, которые должны сохраняться при вызове метода. Для этого просто оберните IIFE вокруг функции, определяющей метод. Например:

    var obj = {
        method: function () {  // open IIFE
            
            // method-private data
            var invocCount = 0;
            
            return function () {
                invocCount++;
                console.log("Invocation #"+invocCount);
                return "result";
            };
        }()  // close IIFE
    };

Взаимодействие:

    > obj.method()
    Invocation #1
    'result'
    > obj.method()
    Invocation #2
    'result'

5.1 Анализ

 

For the use cases where this approach is relevant, managing the private data close to the method that uses it is very convenient. The memory consumption caused by the additional environment may be an issue.

 

6. Conclusion

We have seen that there are several patterns that you can use for keeping data private in JavaScript. Each one has pros and cons, so you need to choose carefully. ECMAScript.next will make things simpler via private name objects. It might even introduce syntactic sugar so that you don’t have to manage them manually.

Upcoming: my book on JavaScript (free online).

 

References

 

  1. JavaScript variable scoping and its pitfalls
  2. JavaScript inheritance by example