Статьи

JavaScript: как встраивать приватных членов в объект

Недавно я разработал Angular Cloud Data Connector , который позволяет разработчикам Angular использовать облачные данные, в частности мобильные службы Azure , с использованием веб-стандартов, таких как индексированная БД. Я пытался создать способ для разработчиков JavaScript для встраивания частных членов в объект.

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

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

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

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

01
02
03
04
05
06
07
08
09
10
11
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 все еще доступен и может быть изменен напрямую.

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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 имеет переменную createProperty которую могут видеть функции get и set. Эта переменная будет сохранена в закрытом пространстве функций get и set. Только эти две функции теперь могут видеть и обновлять переменную currentValue ! Миссия выполнена!

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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”);

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

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

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

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

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
<!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.

Вот таблица и график результатов:

Таблица результатов для IE11 Chrome 36 и Firefox 31
График результатов для IE11 Chrome 36 и Firefox 31

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

Производительность Chrome ниже, чем я ожидал. Там может быть ошибка, поэтому, чтобы быть уверенным, я связался с командой Google, чтобы выяснить, что здесь происходит. Также, если вы хотите проверить, как это работает в Microsoft Edge – новом браузере Microsoft , который будет поставляться по умолчанию с Windows 10 – вы можете скачать его здесь .

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

Диаграмма сравнения пространства прямого доступа и обычного пути

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

01
02
03
04
05
06
07
08
09
10
var sampleSize = 1000000;
 
var entities = [];
 
// Creating entities
for (var index = 0; index < sampleSize; index++) {
    entities.push({
        property: “hello world (” + index + “)”
    });
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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);
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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):

Пример встроенного профилировщика памяти с использованием инструментов F12

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

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

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

Это может вас немного удивить, но у Microsoft есть много бесплатных обучающих программ по многим темам с открытым исходным кодом JavaScript, и мы нацелены на то, чтобы создать намного больше с приходом Microsoft Edge . Проверьте мои собственные:

Или серия обучения нашей команды:

И некоторые бесплатные инструменты: сообщество Visual Studio , пробная версия Azure и инструменты кросс-браузерного тестирования для Mac, Linux или Windows.

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

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

Эта статья является частью серии технологий веб-разработки от Microsoft. Мы рады поделиться с вами Microsoft Edge и новым механизмом рендеринга EdgeHTML . Получите бесплатные виртуальные машины или проведите удаленное тестирование на устройстве Mac, iOS, Android или Windows @ http://dev.modern.ie/ .

Изучите JavaScript: полное руководство

Мы создали полное руководство, которое поможет вам изучить JavaScript , независимо от того, начинаете ли вы как веб-разработчик или хотите изучать более сложные темы.