Статьи

JavaScript: использование закрывающего пространства для создания реальных частных членов

В недавнем проекте я обсуждал с @johnshew способ, которым разработчики JavaScript могут встраивать приватные элементы в объект. Моя техника для этого конкретного случая — использовать то, что я называю «пробелом».

Но прежде чем углубиться в это, позвольте мне представить вам, почему вам может понадобиться частный член, а также другой способ «симулировать» частного участника.

Не стесняйтесь пинговать меня в твиттере, если вы хотите обсудить эту статью: @deltakosh

Зачем использовать частные члены

Когда вы создаете объект с помощью JavaScript, вы можете определить значения членов. Если вы хотите контролировать доступ для чтения / записи на них, вам нужны средства доступа, которые можно определить так:

var entity = {};

entity._property = "hello world";
Object.defineProperty(entity, "property", {
    get: function () { return this._property; },
    set: function (value) {
        this._property = value;
    },
    enumerable: true,
    configurable: true
});

Делая это, вы имеете полный контроль над операциями чтения и записи. Проблема в том, что член _property все еще доступен и может быть изменен напрямую.

Именно поэтому вам нужен более надежный способ определения закрытых членов, доступ к которым могут получить только функции объекта.

Использование пробела

Хитрость здесь в том, чтобы использовать закрытое пространство. Это пространство памяти создается для вас браузером каждый раз, когда внутренняя функция имеет доступ к переменным из области видимости внешней функции. Иногда это может быть сложно, но для нашей темы это идеально.

Итак, давайте немного изменим предыдущий код, чтобы использовать эту функцию:

var createProperty = function (obj, prop, currentValue) {
    Object.defineProperty(obj, prop, {
        get: function () { return currentValue; },
        set: function (value) {
            currentValue = value;
        },
        enumerable: true,
        configurable: true
    });
}

var entity = {};

var myVar = "hello world";
createProperty(entity, "property", myVar);

В этом примере CreateProperty функция имеет CurrentValue переменную, получить и установить функции могут видеть. Эта переменная будет сохранена в закрытом пространстве функций get и set. Только эти две функции теперь могут видеть и обновлять переменную currentValue ! Миссия выполнена !

Единственное предупреждение, которое мы имеем здесь, это то, что исходное значение ( myVar ) все еще доступно Итак, вот еще одна версия для еще более надежной защиты:

var createProperty = function (obj, prop) {
    var currentValue = obj[prop];
    Object.defineProperty(obj, prop, {
        get: function () { return currentValue; },
        set: function (value) {
            currentValue = value;
        },
        enumerable: true,
        configurable: true
    });
}

var entity = {
    property: "hello world"
};

createProperty(entity, "property");

Таким образом, даже исходное значение уничтожается. Итак, миссия полностью выполнена!

Оценка производительности

Давайте теперь посмотрим на производительность.

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

Чтобы проверить, не слишком ли дорогой подход к закрытию, по сравнению с обычным способом, я написал небольшой тест

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
</head>
<style>
    html {
        font-family: "Helvetica Neue", Helvetica;
    }
</style>
<body>
    <div id="results">Computing...</div>
    <script>
        var results = document.getElementById("results");
        var sampleSize = 1000000;
        var opCounts = 1000000;

        var entities = [];

        setTimeout(function () {
            // Creating entities
            for (var index = 0; index < sampleSize; index++) {
                entities.push({
                    property: "hello world (" + index + ")"
                });
            }

            // Random reads
            var start = new Date().getTime();
            for (index = 0; index < opCounts; index++) {
                var position = Math.floor(Math.random() * entities.length);
                var temp = entities[position].property;
            }
            var end = new Date().getTime();

            results.innerHTML = "<strong>Results:</strong><br>Using member access: <strong>" + (end - start) + "</strong> ms";
        }, 0);

        setTimeout(function () {
            // Closure space =======================================
            var createProperty = function (obj, prop, currentValue) {
                Object.defineProperty(obj, prop, {
                    get: function () { return currentValue; },
                    set: function (value) {
                        currentValue = value;
                    },
                    enumerable: true,
                    configurable: true
                });
            }
            // Adding property and using closure space to save private value
            for (var index = 0; index < sampleSize; index++) {
                var entity = entities[index];

                var currentValue = entity.property;
                createProperty(entity, "property", currentValue);
            }

            // Random reads
            var start = new Date().getTime();
            for (index = 0; index < opCounts; index++) {
                var position = Math.floor(Math.random() * entities.length);
                var temp = entities[position].property;
            }
            var end = new Date().getTime();

            results.innerHTML += "<br>Using closure space: <strong>" + (end - start) + "</strong> ms";
        }, 0);

        setTimeout(function () {
            // Using local member =======================================
            // Adding property and using local member to save private value
            for (var index = 0; index < sampleSize; index++) {
                var entity = entities[index];

                entity._property = entity.property;
                Object.defineProperty(entity, "property", {
                    get: function () { return this._property; },
                    set: function (value) {
                        this._property = value;
                    },
                    enumerable: true,
                    configurable: true
                });
            }

            // Random reads
            var start = new Date().getTime();
            for (index = 0; index < opCounts; index++) {
                var position = Math.floor(Math.random() * entities.length);
                var temp = entities[position].property;
            }
            var end = new Date().getTime();

            results.innerHTML += "<br>Using local member: <strong>" + (end - start) + "</strong> ms";
        }, 0);

    </script>
</body>
</html>

Я создаю 1 миллион объектов, все из которых принадлежат участнику. Затем я делаю три теста:

  • Сделайте 1 миллион случайных доступов к собственности
  • Сделайте 1 миллион случайных обращений к версии «закрытого пространства»
  • Сделайте 1 миллион случайных обращений к обычной версии get / set

Вот таблица и диаграмма с результатом:

образ

образ

Мы можем заметить, что версия для закрытого пространства всегда быстрее, чем обычная версия, и в зависимости от браузера это может быть действительно впечатляющей оптимизацией .

Производительность Chrome кажется действительно странной. Там может быть ошибка. Чтобы быть уверенным, я связался с командой Google, чтобы выяснить, что здесь происходит

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

образ

След памяти

Мы также должны проверить, не использует ли этот метод слишком много памяти. Для оценки памяти я написал эти три небольших фрагмента кода:

Код ссылки

var sampleSize = 1000000;

var entities = [];

// Creating entities
for (var index = 0; index < sampleSize; index++) {
    entities.push({
        property: "hello world (" + index + ")"
    });
}

Обычный способ

var sampleSize = 1000000;

var entities = [];

// Adding property and using local member to save private value
for (var index = 0; index < sampleSize; index++) {
    var entity = {};

    entity._property = "hello world (" + index + ")";
    Object.defineProperty(entity, "property", {
        get: function () { return this._property; },
        set: function (value) {
            this._property = value;
        },
        enumerable: true,
        configurable: true
    });

    entities.push(entity);
}

Закрытие космической версии

var sampleSize = 1000000;

var entities = [];

var createProperty = function (obj, prop, currentValue) {
    Object.defineProperty(obj, prop, {
        get: function () { return currentValue; },
        set: function (value) {
            currentValue = value;
        },
        enumerable: true,
        configurable: true
    });
}

// Adding property and using closure space to save private value
for (var index = 0; index < sampleSize; index++) {
    var entity = {};

    var currentValue = "hello world (" + index + ")";
    createProperty(entity, "property", currentValue);

    entities.push(entity);
}

Затем я запустил все эти три кода и запустил встроенный профилировщик памяти (пример здесь с использованием инструментов F12):

образ

Вот результаты, которые я получил на своем компьютере:

образ

Между закрывающим пространством и обычным способом только Chrome имеет лучшие результаты для закрывающего пространства. IE и Firefox используют немного больше памяти.

Вывод

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

И, кстати, если вы хотите попробовать это самостоятельно, пожалуйста, найдите весь код, используемый здесь .