文章目錄
最佳化 JavaScript 代碼
作者: Gregory Baker, GMail 軟體工程師 和 Erik Arvidsson, Google Chrome 軟體工程師
需要的經驗: JavaScript 相關工作知識
用戶端指令碼能讓你的應用更加地動態和活躍, 但是瀏覽器對代碼的解析可能造成效率問題, 而這種效能差異在用戶端之間也不盡相同. 這裡我們討論和給出一些最佳化你的 JavaScript 代碼的提示和最佳實務.
使用字串
字串串連操作會對 Internet Explorer 6 和 7 的垃圾收集帶來很大的影響. 儘管這個問題在 Internet Explorer 8 裡面得到解決 -- 字串串連在 IE8 和其它非 IE 瀏覽器(如 Chrome)中稍微更有效率一點 -- 如果你的使用者中有很大一部分在使用 Internet Explorer 6 或 7, 你就需要非常注意你構建字串的方式了.
有如下範例程式碼:
複製代碼 代碼如下:var veryLongMessage =
'This is a long string that due to our strict line length limit of' +
maxCharsPerLine +
' characters per line must be wrapped. ' +
percentWhoDislike +
'% of engineers dislike this rule. The line length limit is for ' +
' style purposes, but we don't want it to have a performance impact.' +
' So the question is how should we do the wrapping?';
比起用串連的方式, 嘗試使用 join(): 複製代碼 代碼如下:var veryLongMessage =
['This is a long string that due to our strict line length limit of',
maxCharsPerLine,
' characters per line must be wrapped. ',
percentWhoDislike,
'% of engineers dislike this rule. The line length limit is for ',
' style purposes, but we don't want it to have a performance impact.',
' So the question is how should we do the wrapping?'
].join();
相似的, 用串連的方式在條件陳述式和迴圈中構建字串是很低效的. 錯誤的方式: 複製代碼 代碼如下:var fibonacciStr = '前 20 個斐波那契數 ';
for (var i = 0; i < 20; i++) {
fibonacciStr += i + ' = ' + fibonacci(i) + '
';
}
正確的方法: 複製代碼 代碼如下:var strBuilder = ['前 20 個斐波那契數:'];
for (var i = 0; i < 20; i++) {
strBuilder.push(i, ' = ', fibonacci(i));
}
var fibonacciStr = strBuilder.join('');
構建通過輔助函數產生的字串
通過傳遞字串構建器(可以是數組或者輔助類)到函數中構建長字串, 以避免出現存放臨時結果的字串.
例如, 假定 buildMenuItemHtml_ 需要用文字串和變數構建一個字串, 並且會在內部使用一個字串構建器, 與其使用: 複製代碼 代碼如下:var strBuilder = [];
for (var i = 0; i < menuItems.length; i++) {
strBuilder.push(this.buildMenuItemHtml_(menuItems[i]));
}
var menuHtml = strBuilder.join();
不如用: 複製代碼 代碼如下:var strBuilder = [];
for (var i = 0; i < menuItems.length; i++) {
this.buildMenuItem_(menuItems[i], strBuilder);
}
var menuHtml = strBuilder.join();
定義類的方法
下面的代碼效率不高, 因為每次構造 baz.Bar 的執行個體時, 都會為 foo 建立一個新函數和閉包(closure): 複製代碼 代碼如下:baz.Bar = function() {
// 建構函式代碼
this.foo = function() {
// 方法代碼
};
}
推薦的方式為: 複製代碼 代碼如下:baz.Bar = function() {
// 建構函式代碼
};
baz.Bar.prototype.foo = function() {
// 方法代碼
};
用這種方式, 無論構造了多少個 baz.Bar 執行個體, 只會建立一個函數給 foo, 同時不會建立任何閉包.
初始化執行個體變數
將帶有實值型別(非引用的)的初始化值(例如類型為數字, 布爾值, null, undefined 或字串的值)的變數聲明/初始化代碼直接放在 prototype 原型中. 這可以避免每次調用建構函式時不必要地運行初始化代碼. (這個方法無法應用到初始化值由構造器參數決定或構造時狀態不確定的執行個體變數上.)
例如, 比起寫: 複製代碼 代碼如下:foo.Bar = function() {
this.prop1_ = 4;
this.prop2_ = true;
this.prop3_ = [];
this.prop4_ = 'blah';
};
不如寫: 複製代碼 代碼如下:foo.Bar = function() {
this.prop3_ = [];
};
foo.Bar.prototype.prop1_ = 4;
foo.Bar.prototype.prop2_ = true;
foo.Bar.prototype.prop4_ = 'blah';
謹慎地使用閉包(closure)
閉包是 JavaScript 中一個強大而有用的特性; 但是, 它們也有不好的地方, 包括:
它們是最常見的記憶體流失源頭.
建立一個閉包比建立一個沒有閉包的內嵌函式明顯要慢, 比起重用一個靜態函數則更慢. 例如: 複製代碼 代碼如下:function setupAlertTimeout() {
var msg = '要顯示的訊息';
window.setTimeout(function() { alert(msg); }, 100);
}
比下面的代碼慢: 複製代碼 代碼如下:function setupAlertTimeout() {
window.setTimeout(function() {
var msg = '要顯示的訊息';
alert(msg);
}, 100);
}
更比下面的代碼慢: 複製代碼 代碼如下:function alertMsg() {
var msg = '要顯示的訊息';
alert(msg);
}
function setupAlertTimeout() {
window.setTimeout(alertMsg, 100);
}
他們增加了範圍鏈(scope chain)的層級. 當瀏覽器解析屬性時, 範圍鏈的每一個層級都必須被檢查一次. 在下面的例子中: 複製代碼 代碼如下:var a = 'a';
function createFunctionWithClosure() {
var b = 'b';
return function () {
var c = 'c';
a;
c;
};
}
var f = createFunctionWithClosure();
f();
當 f 被調用時, 引用 a 比引用 b 慢, 它們都比引用 c 要慢.
查看 IE+JScript Performance Recommendations Part 3: JavaScript Code inefficiencies 獲得更多有關在 IE 中使用閉包的資訊.
避免使用
with
在你的代碼中避免使用 with
. 它對效能有非常壞的影響, 因為它修改了範圍鏈, 讓尋找在其它範圍的變數變得代價高昂.
避免瀏覽器記憶體流失
記憶體流失對 Web 應用程式而言是個很普遍的問題, 它會帶來嚴重的效能問題. 當瀏覽器的記憶體使用量上升時, 你的 Web 應用程式, 連同使用者系統的其他部分, 都會變慢. Web 應用程式最常見的記憶體流失原因是: 在 JavaScript 指令碼引擎和瀏覽器 DOM 的 C++ 對象實現間的循環參考(例如, 在 JavaScript 指令碼引擎和 Internet Explorer 的 COM 基礎架構間, 或者 JavaScript 引擎和 Firefox 的 XPCOM 基礎架構間).
下面是避免記憶體流失的一些經驗法則:
使用一個事件系統來附加事件處理函數
最常見的循環參考模式 [ DOM 元素 --> 事件處理函數 --> 閉包範圍 --> DOM ] 在 這篇 MSDN 的 Blog 文章中討論過了. 為避免這個問題, 可以使用一個經過嚴格測試的事件系統來附件事件處理函數, 例如 Google doctype, Dojo, or JQuery.
另外, 在 IE 中使用內聯(inline)的事件處理函數會導致另外一類泄漏. 這不是通常的循環參考泄漏, 而是記憶體中臨時匿名指令碼對象的泄漏. 詳情請查看 理解和解決 IE 泄漏模式(Understanding and Solving Internet Explorer Leak Patterns) 的 "DOM 插入順序泄漏模型(DOM Insertion Order Leak Model)" 一節, 另外在 JavaScript Kit 教程 中還有一個例子.
避免使用擴充(expando)屬性
擴充屬性是附加到 DOM 元素上的任意 JavaScript 屬性, 也是循環參考的常見原因. 你能夠在使用擴充屬性時不導致記憶體流失, 但是很容易不小心就引入一個泄漏. 這個泄漏的模式是 [ DOM 元素 --> 擴充屬性 --> 中間對象 --> DOM 元素 ]. 最好的方法就是避免使用它們. 如果你要使用它們, 就只使用簡單的實值型別. 如果你要非簡單的類型, 那麼在不再需要擴充屬性時將它設為空白(null). 參見 理解和解決 IE 泄漏模式(Understanding and Solving Internet Explorer Leak Patterns) 中的 "循環參考(Circular References)" 一節.