最近,我开发一个项目?Angular Cloud Data Connector, 帮助Angular开发者使用云数据,特别是?Azure移动服务, 使用WEB标准,像索引数据库(indexed DB)。我尝试建立一种方式,使得JavaScript开发者能将私有成员嵌入到一个对象中。
我解决这个问题的技术用到了我命名的闭包空间(closure space)。在这篇入门文章中,我要分享的是如何在你的项目中用它,及它对主流浏览器的性能和内存的影响。
在深入学习前,咱们先说下,你为什么需要用到私有成员(private members), 还有一种替代方式来模拟私有成员。
如果你想点评本文,尽情推(twitter)我: @deltakosh。
1. 为何要用私有成员(Private Members)
当你用JavaScript 创建一个对象时,可以声明值成员(value members)。 如果你打算控制对它们的读/写访问操作,可以如下声明:
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 成员仍然可以直接访问和修改。
这也就是为何我们需要更加稳定可靠的方式,声明私有成员,它智能通过对象的方法来访问。
2. 使用闭包空间(Closure Space)
解决方法是使用闭包空间。每当内部函数 (inner fanction) 访问来自外部函数作用域的变量时,浏览器为你分配一段内存空间。有时很取巧,不过就我们的题目来讲,这算是一个完美的解决方案。
我们在上个代码版本中添加这个特性: 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 变量! 任务完成!
唯一需要警惕 caveat,警告,注意)的是源值 (myVar) 仍可访问。下面给出另一个更健壮的版本(保护 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");
采用该函数, 即便源值都销毁(destructed,注:意思是不能直接赋值)了。到此大功告成了!
3.?性能考虑Performance Considerations
现在咱们看看性能。
很明显,比起一个简单的变量,闭包空间,甚或(对象)属性要慢的多,且更消耗资源。这就是本文更多关注普通方式和闭包空间机制差异的原因。
为证明闭包空间机制并不比标准方式更消耗资源,?我写了下面代码做个基准测试:
<!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百万次随机访问常规get/set实现版本。
测试结果参见下面表格和图表:
我们发现,闭包空间实现总是快于常规实现,根据浏览器的不同,还可以做进一步的性能优化。
Chrome 上的性能表现低于预期。或许存在 bug,因此,为确认(存在 bug),我联系了 Google 项目组,描述发生的症状。还有,如果你打算测试在 Microsoft Edge —微软新发布的浏览器,在windows10 中默认安装—中的性能表现,你可以点击下载?。
然而,如果仔细研究,你会发现,使用闭包空间或属性比直接访问变量成员要10倍左右。?因此,使用要恰当且谨慎。
4.?内存占用(Memory Footprint)
我们也得验证该技术不会消耗过多内存。为测试内存占用基准情况,我写了下面代码段:
直接属性引用版本(Reference Code)
var?sampleSize?=?1000000; ?var?entities?=?[];? //?Creating?entities for?(var?index?=?0;?index?<?sampleSize;?index++)?{ ????entities.push({ ????????????property:?"hello?world?("?+?index?+?")" });}
常规方式版本(Regular Way,get/set)
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); }
闭包空间版本(Closure Space Version)
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上占用内存反而增多,但是浏览器的比较结果e—对于现代浏览器,用户很可能不会在意这点差别。
更多 JavaScript 实践
或许你会吃惊,微软提供了一批有关开源 Javascript 主题的免费学习材料,?我们正在发起一个任务,关于创建更多?Microsoft Edge 来临 系列。 查看我的文章:
- 基于 HTML5 和 Babylon.JS 开发 WebGL 3D 基础
- 构建单页面应用,基于 ASP.NET 和 AngularJS
- HTML 高级图像技术
或者我们团队系列:
- HTML/JavaScript 性能优化使用技巧 (该系列有7部分,从响应式设计到休闲游戏的性能优化)
- 现代 Web 平台快速起步 ( HTML, CSS, and?JS基础)
- 开发通用的 Windows Apps,使用 HTML 和 JavaScript 快速起步 (使用你自己的JS构建app)
以及一些免费工具:Visual Studio 社区,Azure 试用版和跨浏览器测试工具用于 Mac, Linux, 或者 Windows。
结论(Conclusion)
如你所见,对于创建真正的私有数据来讲,闭包空间属性(机制)是一个很棒的做法。或许你得面对内存消耗小幅度增加(问题),但就我的看法,这却很合理?(这个代价可以换取相对于常规方法更高的性能增长)。
随带说一句, 如果你要自己动手试试,所以代码可以在?here下载。?推荐一篇不错的文章,?“how-to” on Azure Mobile Services here。