JavaScript: 使用闭包创建私有成员





0/5 (0投票)
在本教程中,我想与您分享如何将此技术用于您自己的项目,以及它对主要浏览器在性能和内存方面的影响。
我最近开发了 Angular Cloud Data Connector,它使 Angular 开发人员能够使用云数据,特别是 Azure 移动服务,使用像 IndexedDB 这样的 Web 标准。我一直在尝试为 JavaScript 开发人员创建一种将私有成员嵌入到对象中的方法。在这种特定情况下,我的技术是使用我称之为“闭包空间”的东西。在本教程中,我想与您分享如何将此技术用于您自己的项目,以及它对主要浏览器在性能和内存方面的影响。
但在深入研究之前,让我先分享一下为什么您可能需要私有成员,以及“模拟”私有成员的替代方法。
如果您想讨论这篇文章,请随时在 Twitter 上给我发消息:@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 函数都可以看到。这个变量将被保存在 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>
我创建了 100 万个对象,所有对象都有一个属性成员。然后我进行了三个测试:
- 对属性进行 100 万次随机访问。
- 对“闭包空间”版本进行 100 万次随机访问。
- 对常规 get/set 版本进行 100 万次随机访问。
这是结果的表格和图表。
我们可以看到闭包空间版本始终比常规版本更快,并且根据浏览器,“它可能是一个令人印象深刻的优化”。
Chrome 的性能不如我预期。可能存在一个 bug,为了确保这一点,我联系了 Google 团队来弄清楚这里发生了什么。此外,如果您想测试它在 Microsoft Edge - 将与 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 < sampleSize; index++) { var entity = {}; var currentValue = "hello world (" + index + ")"; createProperty(entity, "property", currentValue); entities.push(entity); }
然后我运行了所有这三个代码,并启动了嵌入式内存分析器(这里以 F12 工具为例)。
这是我在我的电脑上获得的结果。
在闭包空间和常规方法之间,只有 Chrome 对闭包空间版本有稍微好一点的结果。IE11 和 Firefox 使用的内存稍多一些,但这些浏览器相对可比 - 用户可能不会注意到现代浏览器之间的差异。
更多关于 JavaScript 的实践
这可能让您有点惊讶,但微软在许多开源 JavaScript 主题上有大量的免费学习资源,我们的使命是创建更多资源, 随着 Microsoft Edge 的到来。查看我的
或我们的团队的学习系列
- 让您的 HTML/JavaScript 更快的实用性能技巧(一个 7 部分系列,涵盖响应式设计、休闲游戏到性能优化)。
- 现代 Web 平台 JumpStart(HTML、CSS 和 JS 的基础知识)。
- 使用 HTML 和 JavaScript 开发通用 Windows 应用 JumpStart(利用您已创建的 JS 来构建应用)。
还有一些免费工具: Visual Studio Community、 Azure 试用版,以及适用于 Mac、Linux 或 Windows 的跨浏览器测试工具。
结论
正如您所见,闭包空间属性可以是“创建真正私有数据的一个好方法”。您可能需要处理内存消耗的小幅增加,但就我而言,这是相当合理的(而且以这个价格,您可以获得比使用常规方法更好的性能改进)。
对了,如果您想自己尝试一下,请在此处查找所有使用的代码。这里有一个关于 Azure Mobile Services 的很好的“操作指南”在此。
本文是微软 Web 开发技术系列的一部分。我们很乐意与您分享Microsoft Edge 及其新渲染引擎。在 @ modern.IE 上获取免费虚拟机或在您的 Mac、iOS、Android 或 Windows 设备上远程测试。