譯者按:本人第一次翻譯外文,言語難免有些晦澀,但盡量表達了作者的原意,未經過多的潤色,歡迎批評指正。另本文篇幅較長、資訊量大,可能難以消化,歡迎留言探討細節問題。本文主要關注V8的效能優化,部分內容並不適用於所有JS引擎。最後,轉載請註明出處: )
========================譯文分割線==================== =======
許多JavaScript引擎,如Google的V8引擎(被Chrome和Node所用),是專門為需要快速執行的大型JavaScript應用所設計的。如果你是開發者,並且關心記憶體使用情況與頁面效能,你應該了解使用者瀏覽器中的JavaScript引擎是如何運作的。無論是V8,SpiderMonkey的(Firefox)的Carakan(Opera),Chakra(IE)或其他引擎,這樣做可以幫助你更多更多
(IE)或其他引擎,這樣做可以幫助你更好地優化你的應用程式
。這並不是說應該專門為某一瀏覽器或引擎做優化,千萬別這麼做。什麼是引擎無法優化的,垃圾回收器(GC)是否能回收我所期望的東西
載入快速的網站就像是一輛快速的跑車,需要用到特別客製化的零件. 圖片來源:
dHybridcars.
那麼,JavaScript在V8裡是如何運作的?
垃圾回收
垃圾回收是記憶體管理的一種形式
,其實就是一個收集器的概念,嘗試回收不再被使用的物件所佔用的記憶體。在JavaScript這種垃圾回收語言中,應用程式中仍在被引用的物件不會被清除。手動消除物件引用在大多數情況下是沒有必要的。透過簡單地把變數放在需要它們的地方(理想情況下,盡可能是局部作用域,即它們被使用的函數裡而不是函數外層),一切將運作地很好。
垃圾回收器嘗試回收記憶體. 圖片來源: Valtteri Mäki.
在JavaScript中,是不可能強制進行垃圾回收的。你不應該這麼做,因為垃圾收集過程是由運行時控制的,它知道什麼是最好的清理時機。
網路上有許多關於JavaScript記憶體回收的討論都談到delete這個關鍵字,雖然它可以被用來刪除物件(map)中的屬性(key),但有部分開發者認為它可以用來強制「消除引用」。建議盡可能避免使用delete,在下面的例子中delete o.x 的弊大于利,因为它改变了o的隐藏类,并使它成为一个"慢对象"。
var o = { x: 1 }; delete o.x; // true o.x; // undefined
你會很容易地在流行的JS庫中找到引用刪除——這是具有語言目的性的。這裡要注意的是避免在執行時修改”hot”物件的結構。 JavaScript引擎可以偵測出這種「hot」的對象,並嘗試對其進行最佳化。如果物件在生命週期中其結構沒有較大的改變,引擎將會更容易優化對象,而delete操作實際上會觸發這種較大的結構改變,因此不利於引擎的最佳化。
對於null是如何運作也是有誤解的。將一個物件參考設為null,並沒有使物件變“空”,只是將它的參考設為空而已。使用o.x= null比使用delete會更好些,但可能也不是很有必要。
var o = { x: 1 }; o = null; o; // null o.x // TypeError
如果此引用是當前物件的最後引用,那麼該物件將被作為垃圾回收。如果此引用不是當前物件的最後引用,則該物件是可存取的且不會被垃圾回收。
另外要注意的是,全域變數在頁面的生命週期裡是不被垃圾回收器清理的。無論頁面開啟多久,JavaScript運行時全域物件作用域中的變數都會一直存在。
var myGlobalNamespace = {};
全域物件只會在重新整理頁面、導覽到其他頁面、關閉標籤頁或退出瀏覽器時才會被清理。函數作用域的變數會在超出作用域時被清理,也就是退出函數時,已經沒有任何引用,這樣的變數就被清理了。
為了讓垃圾回收器儘早收集盡可能多的對象,不要hold著不再使用的對象。這裡有幾件事要記住:
接下來,我們來談談函數。正如我們已經說過,垃圾收集的工作原理,是透過回收不再是存取的記憶體區塊(物件)。為了更好地說明這一點,這裡有一些例子。
function foo() { var bar = new LargeObject(); bar.someCall(); }
當foo返回時,bar指向的物件將會被垃圾收集器自動回收,因為它已沒有任何存在的引用了。
對比一下:
function foo() { var bar = new LargeObject(); bar.someCall(); return bar; } // somewhere else var b = foo();
現在我們有一個引用指向bar對象,這樣bar對象的生存週期就從foo的呼叫一直持續到呼叫者指定別的變數b(或b超出範圍)。
當你看到一個函數,返回一個內部函數,該內部函數將獲得範圍外的存取權,即使在外部函數執行之後。這是一個基本的閉包 —— 可以在特定的上下文中設定的變數的表達式。例如:
function sum (x) { function sumIt(y) { return x + y; }; return sumIt; } // Usage var sumA = sum(4); var sumB = sumA(3); console.log(sumB); // Returns 7
在sum呼叫上下文中產生的函數物件(sumIt)是無法被回收的,它被全域變數(sumA)所引用,並且可以透過sumA(n)呼叫。
讓我們來看看另一個例子,這裡我們可以存取變數largeStr嗎?
var a = function () { var largeStr = new Array(1000000).join('x'); return function () { return largeStr; }; }();
是的,我們可以透過a()來存取largeStr,所以它沒有被回收。下面這個呢?
var a = function () { var smallStr = 'x'; var largeStr = new Array(1000000).join('x'); return function (n) { return smallStr; }; }();
我們不能再造訪largeStr了,它已經是垃圾回收候選人了。 【譯者註:因為largeStr已不存在外部引用了】
最糟的内存泄漏地方之一是在循环中,或者在setTimeout()/ setInterval()中,但这是相当常见的。思考下面的例子:
var myObj = { callMeMaybe: function () { var myRef = this; var val = setTimeout(function () { console.log('Time is running out!'); myRef.callMeMaybe(); }, 1000); } };
如果我们运行myObj.callMeMaybe();来启动定时器,可以看到控制台每秒打印出“Time is running out!”。如果接着运行myObj =
null,定时器依旧处于激活状态。为了能够持续执行,闭包将myObj传递给setTimeout,这样myObj是无法被回收的。相反,它引用到myObj的因为它捕获了myRef。这跟我们为了保持引用将闭包传给其他的函数是一样的。
同样值得牢记的是,setTimeout/setInterval调用(如函数)中的引用,将需要执行和完成,才可以被垃圾收集。
永远不要优化代码,直到你真正需要。现在经常可以看到一些基准测试,显示N比M在V8中更为优化,但是在模块代码或应用中测试一下会发现,这些优化真正的效果比你期望的要小的多。
做的过多还不如什么都不做. 图片来源: Tim Sheerman-Chase.
比如我们想要创建这样一个模块:
这个问题有几个不同的因素,虽然也很容易解决。我们如何存储数据,如何高效地绘制表格并且append到DOM中,如何更优地处理表格事件?
面对这些问题最开始(天真)的做法是使用对象存储数据并放入数组中,使用jQuery遍历数据绘制表格并append到DOM中,最后使用事件绑定我们期望地点击行为。
注意:这不是你应该做的
var moduleA = function () { return { data: dataArrayObject, init: function () { this.addTable(); this.addEvents(); }, addTable: function () { for (var i = 0; i < rows; i++) { $tr = $('<tr></tr>'); for (var j = 0; j < this.data.length; j++) { $tr.append('<td>' + this.data[j]['id'] + '</td>'); } $tr.appendTo($tbody); } }, addEvents: function () { $('table td').on('click', function () { $(this).toggleClass('active'); }); } }; }();
这段代码简单有效地完成了任务。
但在这种情况下,我们遍历的数据只是本应该简单地存放在数组中的数字型属性ID。有趣的是,直接使用DocumentFragment和本地DOM方法比使用jQuery(以这种方式)来生成表格是更优的选择,当然,事件代理比单独绑定每个td具有更高的性能。
要注意虽然jQuery在内部使用DocumentFragment,但是在我们的例子中,代码在循环内调用append并且这些调用涉及到一些其他的小知识,因此在这里起到的优化作用不大。希望这不会是一个痛点,但请务必进行基准测试,以确保自己代码ok。
对于我们的例子,上述的做法带来了(期望的)性能提升。事件代理对简单的绑定是一种改进,可选的DocumentFragment也起到了助推作用。
var moduleD = function () { return { data: dataArray, init: function () { this.addTable(); this.addEvents(); }, addTable: function () { var td, tr; var frag = document.createDocumentFragment(); var frag2 = document.createDocumentFragment(); for (var i = 0; i < rows; i++) { tr = document.createElement('tr'); for (var j = 0; j < this.data.length; j++) { td = document.createElement('td'); td.appendChild(document.createTextNode(this.data[j])); frag2.appendChild(td); } tr.appendChild(frag2); frag.appendChild(tr); } tbody.appendChild(frag); }, addEvents: function () { $('table').on('click', 'td', function () { $(this).toggleClass('active'); }); } }; }();
接下来看看其他提升性能的方式。你也许曾经在哪读到过使用原型模式比模块模式更优,或听说过使用JS模版框架性能更好。有时的确如此,不过使用它们其实是为了代码更具可读性。对了,还有预编译!让我们看看在实践中表现的如何?
moduleG = function () {}; moduleG.prototype.data = dataArray; moduleG.prototype.init = function () { this.addTable(); this.addEvents(); }; moduleG.prototype.addTable = function () { var template = _.template($('#template').text()); var html = template({'data' : this.data}); $tbody.append(html); }; moduleG.prototype.addEvents = function () { $('table').on('click', 'td', function () { $(this).toggleClass('active'); }); }; var modG = new moduleG();
事实证明,在这种情况下的带来的性能提升可以忽略不计。模板和原型的选择并没有真正提供更多的东西。也就是说,性能并不是开发者使用它们的原因,给代码带来的可读性、继承模型和可维护性才是真正的原因。
更复杂的问题包括高效地在canvas上绘制图片和操作带或不带类型数组的像素数据。
在将一些方法用在你自己的应用之前,一定要多了解这些方案的基准测试。也许有人还记得JS模版的shoot-off和随后的扩展版。你要搞清楚基准测试不是存在于你看不到的那些虚拟应用,而是应该在你的实际代码中去测试带来的优化。
详细介绍了每个V8引擎的优化点在本文讨论范围之外,当然这里也有许多值得一提的技巧。记住这些技巧你就能减少那些性能低下的代码了。
function add(x, y) { return x+y; } add(1, 2); add('a','b'); add(my_custom_object, undefined);
更多内容可以去看Daniel Clifford在Google I/O的分享 Breaking the JavaScript Speed Limit with V8。 Optimizing For V8 — A Series也非常值得一读。
JavaScript中对象和数组之间只有一个的主要区别,那就是数组神奇的length属性。如果你自己来维护这个属性,那么V8中对象和数组的速度是一样快的。
对于应用程序开发人员,对象克隆是一个常见的问题。虽然各种基准测试可以证明V8对这个问题处理得很好,但仍要小心。复制大的东西通常是较慢的——不要这么做。JS中的for..in循环尤其糟糕,因为它有着恶魔般的规范,并且无论是在哪个引擎中,都可能永远不会比任何对象快。
当你一定要在关键性能代码路径上复制对象时,使用数组或一个自定义的“拷贝构造函数”功能明确地复制每个属性。这可能是最快的方式:
function clone(original) { this.foo = original.foo; this.bar = original.bar; } var copy = new clone(original);
使用模块模式时缓存函数,可能会导致性能方面的提升。参阅下面的例子,因为它总是创建成员函数的新副本,你看到的变化可能会比较慢。
另外请注意,使用这种方法明显更优,不仅仅是依靠原型模式(经过jsPerf测试确认)。
使用模块模式或原型模式时的性能提升
这是一个原型模式与模块模式的性能对比测试:
// Prototypal pattern Klass1 = function () {} Klass1.prototype.foo = function () { log('foo'); } Klass1.prototype.bar = function () { log('bar'); } // Module pattern Klass2 = function () { var foo = function () { log('foo'); }, bar = function () { log('bar'); }; return { foo: foo, bar: bar } } // Module pattern with cached functions var FooFunction = function () { log('foo'); }; var BarFunction = function () { log('bar'); }; Klass3 = function () { return { foo: FooFunction, bar: BarFunction } } // Iteration tests // Prototypal var i = 1000, objs = []; while (i--) { var o = new Klass1() objs.push(new Klass1()); o.bar; o.foo; } // Module pattern var i = 1000, objs = []; while (i--) { var o = Klass2() objs.push(Klass2()); o.bar; o.foo; } // Module pattern with cached functions var i = 1000, objs = []; while (i--) { var o = Klass3() objs.push(Klass3()); o.bar; o.foo; } // See the test for full details
接下来说说数组相关的技巧。在一般情况下,不要删除数组元素,这样将使数组过渡到较慢的内部表示。当索引变得稀疏,V8将会使元素转为更慢的字典模式。
数组字面量非常有用,它可以暗示VM数组的大小和类型。它通常用在体积不大的数组中。
// Here V8 can see that you want a 4-element array containing numbers: var a = [1, 2, 3, 4]; // Don't do this: a = []; // Here V8 knows nothing about the array for(var i = 1; i <= 4; i++) { a.push(i); }
将混合类型(比如数字、字符串、undefined、true/false)的数据存在数组中绝不是一个好想法。例如var arr = [1, “1”, undefined, true, “true”]
正如我们所看到的结果,整数的数组是最快的。
当你使用稀疏数组时,要注意访问元素将远远慢于满数组。因为V8不会分配一整块空间给只用到部分空间的数组。取而代之的是,它被管理在字典中,既节约了空间,但花费访问的时间。
不要预分配大数组(如大于64K的元素),其最大的大小,而应该动态分配。在我们这篇文章的性能测试之前,请记住这只适用部分JavaScript引擎。
空字面量与预分配数组在不同的浏览器进行测试
Nitro (Safari)对预分配的数组更有利。而在其他引擎(V8,SpiderMonkey)中,预先分配并不是高效的。
// Empty array var arr = []; for (var i = 0; i < 1000000; i++) { arr[i] = i; } // Pre-allocated array var arr = new Array(1000000); for (var i = 0; i < 1000000; i++) { arr[i] = i; }
在Web应用的世界中,速度就是一切。没有用户希望用一个要花几秒钟计算某列总数或花几分钟汇总信息的表格应用。这是为什么你要在代码中压榨每一点性能的重要原因。
图片来源: Per Olof Forsberg.
理解和提高应用程序的性能是非常有用的同时,它也是困难的。我们推荐以下的步骤来解决性能的痛点:
下面推荐的一些工具和技术可以协助你。
有很多方式来运行JavaScript代码片段的基准测试其性能——一般的假设是,基准简单地比较两个时间戳。这中模式被jsPerf团队指出,并在SunSpider和Kraken的基准套件中使用:
var totalTime, start = new Date, iterations = 1000; while (iterations--) { // Code snippet goes here } // totalTime → the number of milliseconds taken // to execute the code snippet 1000 times totalTime = new Date - start;
在这里,要测试的代码被放置在一个循环中,并运行一个设定的次数(例如6次)。在此之后,开始日期减去结束日期,就得出在循环中执行操作所花费的时间。
然而,这种基准测试做的事情过于简单了,特别是如果你想运行在多个浏览器和环境的基准。垃圾收集器本身对结果是有一定影响的。即使你使用window.編寫高效能JavaScript(譯)_javascript技巧这样的解决方案,也必须考虑到这些缺陷。
不管你是否只运行基准部分的代码,编写一个测试套件或编码基准库,JavaScript基准其实比你想象的更多。如需更详细的指南基准,我强烈建议你阅读由Mathias Bynens和John-David Dalton提供的Javascript基准测试。
Chrome开发者工具为JavaScript分析有很好的支持。可以使用此功能检测哪些函数占用了大部分时间,这样你就可以去优化它们。这很重要,即使是代码很小的改变会对整体表现产生重要的影响。
Chrome開發者工具的分析面板
分析過程開始取得程式碼效能基線,然後以時間軸的形式體現。這將告訴我們程式碼需要多長時間運行。 「Profiles」標籤給了我們一個更好的視角來了解應用程式中發生了什麼。 JavaScript CPU分析檔案展示了多少CPU時間被用於我們的程式碼,CSS選擇器分析檔案展示了多少時間花費在處理選擇器上,堆快照顯示多少記憶體正被用於我們的物件。
利用這些工具,我們可以分離、調整和重新分析來衡量我們的功能或操作效能最佳化是否真的起到了效果。
「Profile」標籤展示了程式碼效能資訊。
一個很好的分析介紹,閱讀Zack Grossbart的 JavaScript Profiling With The Chrome Developer Tools。
提示:在理想情況下,若想確保你的分析並未受到已安裝的應用程式或擴充功能的任何影響,可以使用--user-data-dir <empty_directory>
標誌來啟動Chrome。在大多數情況下,這種方法優化測試應該是足夠的,但也需要你更多的時間。這是V8標誌能有所幫助的。
在Google內部,Chrome開發者工具被Gmail等團隊大量使用,用來幫助發現和排除記憶體洩漏。
Chrome開發者工具中的記憶體統計
記憶體統計出我們團隊所關心的私人記憶體使用、JavaScript堆的大小、DOM節點數量、儲存清理、事件監聽計數器和垃圾收集器正要回收的東西。推薦閱讀Loreena Lee的「3快照」技術。該技術的要點是,在你的應用程式中記錄一些行為,強制垃圾回收,檢查DOM節點的數量有沒有恢復到預期的基線,然後分析三個堆的快照來確定是否有記憶體洩漏。
單頁面應用程式(例如AngularJS,Backbone,Ember)的記憶體管理是非常重要的,它們幾乎永遠不會刷新頁面。這意味著內存洩漏可能相當明顯。行動終端上的單頁面應用程式充滿了陷阱,因為設備的記憶體有限,並在長期運行Email客戶端或社交網路等應用程式。 能力愈大責任愈重。
有很多辦法解決這個問題。在Backbone中,請確保使用dispose()來處理舊視圖和參考(目前在Backbone(Edge)中可用)。這個函數是最近加上的,移除新增到視圖「event」物件中的處理函數,以及透過傳給view的第三個參數(回呼上下文)的model或collection的事件監聽器。 dispose()也會被視圖的remove()調用,處理當元素被移除時的主要清理工作。 Ember 等其他的函式庫當偵測到元素被移除時,會清理監聽器以避免記憶體洩漏。
Derick Bailey的一些明智的建議:
与其了解事件与引用是如何工作的,不如遵循的标准规则来管理JavaScript中的内存。如果你想加载数据到的一个存满用户对象的Backbone集合中,你要清空这个集合使它不再占用内存,那必须这个集合的所有引用以及集合内对象的引用。一旦清楚了所用的引用,资源就会被回收。这就是标准的JavaScript垃圾回收规则。
在文章中,Derick涵盖了许多使用Backbone.js时的常见内存缺陷,以及如何解决这些问题。
Felix Geisendörfer的在Node中调试内存泄漏的教程也值得一读,尤其是当它形成了更广泛SPA堆栈的一部分。
当浏览器重新渲染文档中的元素时需要 重新计算它们的位置和几何形状,我们称之为回流。回流会阻塞用户在浏览器中的操作,因此理解提升回流时间是非常有帮助的。
回流时间图表
你应该批量地触发回流或重绘,但是要节制地使用这些方法。尽量不处理DOM也很重要。可以使用DocumentFragment,一个轻量级的文档对象。你可以把它作为一种方法来提取文档树的一部分,或创建一个新的文档“片段”。与其不断地添加DOM节点,不如使用文档片段后只执行一次DOM插入操作,以避免过多的回流。
例如,我们写一个函数给一个元素添加20个div。如果只是简单地每次append一个div到元素中,这会触发20次回流。
function addDivs(element) { var div; for (var i = 0; i < 20; i ++) { div = document.createElement('div'); div.innerHTML = 'Heya!'; element.appendChild(div); } }
要解决这个问题,可以使用DocumentFragment来代替,我们可以每次添加一个新的div到里面。完成后将DocumentFragment添加到DOM中只会触发一次回流。
function addDivs(element) { var div; // Creates a new empty DocumentFragment. var fragment = document.createDocumentFragment(); for (var i = 0; i < 20; i ++) { div = document.createElement('a'); div.innerHTML = 'Heya!'; fragment.appendChild(div); } element.appendChild(fragment); }
可以参阅 Make the Web Faster,JavaScript Memory Optimization 和 Finding Memory Leaks。
为了帮助发现JavaScript内存泄漏,谷歌的开发人员((Marja Hölttä和Jochen Eisinger)开发了一种工具,它与Chrome开发人员工具结合使用,检索堆的快照并检测出是什么对象导致了内存泄漏。
一个JavaScript内存泄漏检测工具
有完整的文章介绍了如何使用这个工具,建议你自己到内存泄漏探测器项目页面看看。
如果你想知道为什么这样的工具还没集成到我们的开发工具,其原因有二。它最初是在Closure库中帮助我们捕捉一些特定的内存场景,它更适合作为一个外部工具。
Chrome支持直接通过传递一些标志给V8,以获得更详细的引擎优化输出结果。例如,这样可以追踪V8的优化:
"/Applications/Google Chrome/Google Chrome" --js-flags="--trace-opt --trace-deopt"
Windows用户可以这样运行 chrome.exe –js-flags=”–trace-opt –trace-deopt”
在开发应用程序时,下面的V8标志都可以使用。
V8的處理腳本以*(星號)標識最佳化的函數,用~(波浪號)表示未最佳化的函數。
如果你有興趣了解更多關於V8的標誌和V8的內部是如何運作的,強烈建議 閱讀Vyacheslav Egorov的excellent post on V8 internals。
高精度時間(HRT)是一個提供不受系統時間和用戶調整影響的亞毫秒級高精度時間接口,可以把它當做是比new Date 和Date.now()更精準的度量方法。這對我們編寫基準測試幫助很大。
高精度時間(HRT)提供了當前亞毫秒級的時間精度
目前HRT在Chrome(穩定版)中是以window.編寫高效能JavaScript(譯)_javascript技巧.webkitNow()方式使用,但在Chrome Canary中前綴被丟棄了,這使得它可以透過window.編寫高效能JavaScript(譯)_javascript技巧.now()方式呼叫。 Paul Irish在HTML5Rocks上了關於HRT更多內容的文章。
現在我們知道目前的精準時間,那有可以準確測量頁面效能的API嗎?好吧,現在有個Navigation Timing API可以使用,這個API提供了一種簡單的方式,來獲取網頁在加載呈現給用戶時,精確和詳細的時間測量記錄。可以在console中使用window.編寫高效能JavaScript(譯)_javascript技巧.timing來取得時間資訊:
顯示在控制台中的時間資訊
我們可以從上面的數據獲取很多有用的信息,例如網絡延時為responseEnd – fetchStart,頁面加載時間為loadEventEnd – responseEnd,處理導航和頁面加載的時間為loadEventEnd – navigationStart。
如你所看到的,perfomance.memory的屬性也能顯示JavaScript的記憶體資料使用情況,如總的堆疊大小。
更多Navigation Timing API的細節,閱讀 Sam Dutton的 Measuring Page Load Speed With Navigation Timing。
Chrome中的about:編寫高效能JavaScript(譯)_javascript技巧提供了瀏覽器的效能視圖,記錄了Chrome的所有執行緒、tab頁和進程。
About:Tracing提供了浏览器的性能视图
這個工具的真正用處是允許你捕獲Chrome的運行數據,這樣你就可以適當地調整JavaScript執行,或優化資源加載。
Lilli Thompson有一篇寫給遊戲開發者的使用about:編寫高效能JavaScript(譯)_javascript技巧分析WebGL遊戲的文章,同時也適合JavaScript的開發者。
在Chrome的導覽列裡可以輸入about:memory,同樣十分實用,可以獲得每個tab頁的記憶體使用情況,對定位記憶體洩漏很有幫助。
我們看到,JavaScript的世界中有很多隱藏的陷阱,且並沒有提升效能的銀彈。只有把一些優化方案綜合使用到(現實世界)測試環境,才能獲得最大的效能效益。即便如此,了解引擎是如何解釋和優化程式碼,可以幫助你調整應用程式。
測量,理解,修復。 不斷重複這個過程。
圖片來源: Sally Hunter
謹記專注於最佳化,但為了便利性可以捨棄一些很小的最佳化。例如,有些開發者選擇.forEach和Object.keys代替for和for..in循環,儘管這會更慢但使用更方便。要確保清醒的頭腦,知道什麼優化是需要的,什麼優化是不需要的。
同時注意,雖然JavaScript引擎越來越快,但下一個真正的瓶頸是DOM。回流和重繪的減少也是重要的,所以必要時才再去動DOM。還有就是要關注網絡,HTTP請求是珍貴的,特別是移動終端上,因此要使用HTTP的緩存去減少資源的加載。
記住這幾點可以保證你獲取了本文的大部分信息,希望對你有所幫助!
原文:http://coding.smashingmagazine.com/2012/11/05/writing-fast-memory-efficient-javascript/
作者:Addy Osmani