Статьи

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

Эта статья является частью серии технологий веб-разработки от Microsoft. Спасибо за поддержку партнеров, которые делают возможным использование SitePoint.

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

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

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

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

Когда вы создаете объект с помощью 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 имеет переменную createProperty которую могут видеть функции get и set . Эта переменная будет сохранена в закрывающем пространстве функций 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>Benchmark</title> <style> html { font-family: 'Helvetica Neue', Helvetica; } </style> </head> <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> 

Я создаю один миллион объектов, все с членом свойства. Затем я делаю три теста:

  • Один миллион случайных доступов к собственности

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

  • Один миллион случайных обращений к обычной версии get / set

Вот таблица и диаграмма, детализирующая результат:

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

Диаграмма производительности браузера, показывающая закрытое пространство, почти всегда быстрее, чем обычная версия

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

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

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

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

След памяти

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

Код ссылки

 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 &amp;amp;lt; sampleSize; index++) { var entity = {}; var currentValue = 'hello world (' + index + ')'; createProperty(entity, 'property', currentValue); entities.push(entity); } 

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

Internet Explorer встроенный профилировщик памяти

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

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

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

Больше практического опыта с JavaScript

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

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

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

Вывод

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

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

Эта статья является частью серии технологий веб-разработки от Microsoft. Мы рады поделиться с вами Project Spartan и его новым механизмом рендеринга . Получите бесплатные виртуальные машины или проведите удаленное тестирование на устройстве Mac, iOS, Android или Windows на сайте modern.IE .