Nicholas Zakas是一位 JS 大師,Yahoo! 首頁的前端主程。他是《高效能 Javascript》的作者,這本書值得每個程式員去閱讀。
當談到 JS 效能的時候,Zakas差不多就是你要找的,2010年六月他在Google Tech Talk發表了名為《Speed Up Your Javascript》的演講。
但 Javascript 效能最佳化絕不是一種書面的技術,Nicholas 的技術演進列出了10條建議,幫助你寫出高效的 JS 程式碼。
1. 定義區域性變數
當一個變數被取用的時候,JavaScript將在作用域鏈中的不同成員中查詢這個變數。作用域鏈指的是當前作用於下可用變數的集合,它在各種主流瀏覽器中至少包含兩個部分:區域性變數的集合和全域性變數的集合。
簡單地說,如果JavaScript引擎在作用域鏈中搜索的深度越大,那麼操作也就會消耗更多的時間。引擎首先從 this 開始查詢區域性變數,然後是函式引數、本地定義的變數,最後遍歷所有的全域性變數。
因為區域性變數在這條鏈的起端,所以查詢區域性變數總是比查詢全域性變數要塊。所以當你想要不止一次地使用一個全域性變數的時候,你應該將它定義成區域性變數,就像這樣:
var blah = document.getElementById('myID'),
blah2 = document.getElementById('myID2');
改寫成
var doc = document,
blah = doc.getElementById('myID'),
blah2 = doc.getElementById('myID2');
2. 不要使用 with() 陳述句
這是因為 with() 陳述句將會在作用域鏈的開始新增額外的變數。額外的變數意味著,當任何變數需要被訪問的時候,JavaScript引擎都需要先掃描with()陳述句產生的變數,然後才是區域性變數,最後是全域性變數。 So with() essentially gives local variables all the performance drawbacks of global ones, and in turn derails Javascript optimization. 因此with()陳述句同時給區域性變數和全域性變數的效能帶來負面影響,最終使我們最佳化JavaScript效能的計劃破產。
3. 小心使用閉包
雖然你可能還不知道“閉包”,但你可能在不經意間經常使用這項技術。閉包基本上被認為是JavaScript中的new,當我們定義一個即時函式的時候,我們就使用了閉包,比如:
document.getElementById('foo').onclick = function(ev) { };
閉包的問題在於:根據定義,在它們的作用域鏈中至少有三個物件:閉包變數、區域性變數和全域性變數。這些額外的物件將會導致第1和第2個建議中提到的效能問題。
但是我認為Nicholas並不是要我們因噎廢食,閉包對於提高程式碼可讀性等方面還是非常有用的,只是不要濫用它們(尤其在迴圈中)。
4. 物件屬性和陣列元素的速度都比變數慢
談到JavaScript的資料,一般來說有4種訪問方式:數值、變數、物件屬性和陣列元素。在考慮最佳化時,數值和變數的效能差不多,並且速度顯著優於物件屬性和陣列元素。
因此當你多次取用一個物件屬性或者陣列元素的時候,你可以透過定義一個變數來獲得效能提升。(這一條在讀、寫資料時都有效)
雖然這條規則在絕大多數情況下是正確的,但是Firefox在最佳化陣列索引上做了一些有意思的工作,能夠讓它的實際效能優於變數。但是考慮到陣列元素在其他瀏覽器上的效能弊端,還是應該儘量避免陣列查詢,除非你真的只針對於火狐瀏覽器的效能而進行開發。
5. 不要在陣列中挖得太深
另外,程式員應該避免在陣列中挖得太深,因為進入的層數越多,操作速度就越慢。
簡單地說,在巢狀很多層的陣列中操作很慢是因為陣列元素的查詢速度很慢。試想如果操作巢狀三層的陣列元素,就要執行三次陣列元素查詢,而不是一次。
因此如果你不斷地取用 foo.bar, 你可以透過定義 var bar = foo.bar 來提高效能。
6. 避免 for-in 迴圈(和基於函式的迭代)
這是另一條非常教條的建議:不要使用for-in迴圈。
這背後的邏輯非常直接:要遍歷一個集合內的元素,你可以使用諸如for迴圈、或者do-while迴圈來替代for-in迴圈,for-in迴圈不僅僅可能需要遍歷額外的陣列項,還需要更多的時間。
為了遍歷這些元素,JavaScript需要為每一個元素建立一個函式,這種基於函式的迭代帶來了一系列效能問題:額外的函式引入了函式物件被建立和銷毀的背景關係,將會在作用域鏈的頂端增加額外的元素。
7. 在迴圈時將控制條件和控制變數合併起來
提到效能,在迴圈中需要避免的工作一直是個熱門話題,因為迴圈會被重覆執行很多次。所以如果有效能最佳化的需求,先對迴圈開刀有可能會獲得最明顯的效能提升。
一種最佳化迴圈的方法是在定義迴圈的時候,將控制條件和控制變數合併起來,下麵是一個沒有將他們合併起來的例子:
for ( var x = 0; x < 10; x++ ) {
};
當我們要新增什麼東西到這個迴圈之前,我們發現有幾個操作在每次迭代都會出現。JavaScript引擎需要:
#1:檢查 x 是否存在
#2:檢查 x 是否小於 0 (譯者註:我猜這裡是作者的筆誤)
#3:使 x 增加 1
然而如果你只是迭代元素中的一些元素,那麼你可以使用while迴圈進行輪轉來替代上面這種操作:
var x = 9;
do { } while( x-- );
如果你想更深入地瞭解迴圈的效能,Zakas提供了一種高階的迴圈最佳化技巧,使用非同步進行迴圈(碉堡了!)
8. 為HTML集合物件定義陣列
JavaScript使用了大量的HTML集合物件,比如 document.forms,document.images 等等。通常他們被諸如 getElementsByTagName、getElementByClassName 等方法呼叫。
由於大量的DOM selection操作,HTML集合物件相當的慢,而且還會帶來很多額外的問題。正如DOM標準中所定義的那樣:“HTML集合是一個虛擬存在,意味著當底層檔案被改變時,它們將自動更新。”這太可怕了!
儘管集合物件看起來跟陣列很像,他們在某些地方卻區別很大,比如對於特定查詢的結果。當物件被訪問進行讀寫時,查詢需要重新執行來更新所有與物件相關的組分,比如 length。
HTML集合物件也非常的慢,Nicholas說好像在看球的時候對一個小動作進行60倍速慢放。另外,集合物件也有可能造成死迴圈,比如下麵的例子:
var divs = document.getElementsByTagName('div');
for (var i=0; i < divs.length; i++ ) {
var div = document.createElement("div");
document.appendChild(div);
}
這段程式碼造成了死迴圈,因為 divs 表示一個實時的HTML集合,並不是你所期望的陣列。這種實時的集合在新增
解決這個問題的方法是將這些元素定義成陣列,相比只設定 var divs = document.getElementsByTagName(‘div’) 稍微有點麻煩,下麵是Zakas提供的強制使用陣列的程式碼:
function array(items) {
try {
return Array.prototype.concat.call(items);
} catch (ex) {
var i = 0,
len = items.length,
result = Array(len);
while (i < len) {
result[i] = items[i];
i++;
}
return result;
}
}
var divs = array( document.getElementsByTagName('div') );
for (var i=0l i < divs.length; i++ ) {
var div = document.createElement("div");
document.appendChild(div);
}
9. 不要碰DOM!
不使用DOM是JavaScript最佳化中另一個很大的話題。經典的例子是新增一系列的串列項:如果你把每個串列項分別加到DOM中,肯定會比一次性加入所有串列項到DOM中要慢。這是因為DOM操作開銷很大。
Zakas對這個進行了細緻的講解,解釋了由於迴流(reflow)的存在,DOM操作是非常消耗資源的。迴流通常被理解為瀏覽器重新選渲染DOM樹的處理過程。比如說,如果你用JavaScript陳述句改變了一個div的寬度,瀏覽器需要重繪頁面來適應變化。
任何時候只要有元素被新增到DOM樹或者從DOM樹移除,都會引發迴流。使用一個非常方便的JavaScript物件可以解決這個問題——documentFragment,我並沒有使用過,但是在Steve Souders也表示同意這種做法之後我感覺更加肯定了。
DocumentFragment 基本上是一種瀏覽器以非可視方式實現的類似檔案的片段,非視覺化的表現形式帶來了很多優點,最主要的是你可以在 documentFragment 中新增任何結點而不會引起瀏覽器迴流。
10. 修改CSS類,而不是樣式
你也許聽說過:修改CSS類必直接修改樣式會更高效。這歸結於迴流帶來的另一個問題:當佈局樣式發生改變時,會引發迴流。
佈局樣式意味著任何影響改變佈局的變化都會強制引起瀏覽器迴流。比如寬度、高度、字號、浮動等。
但是別誤會我的意思,CSS類並不會避免迴流,但是可以將它的影響最小化。相比每次修改樣式都會引起迴流,使用CSS類一次修改多個樣式,只需要承擔一次迴流帶來的消耗。
因此在修改多個佈局樣式的時候,使用CSS類來最佳化效能是明智的選擇。另外如果你需要在執行時定義很多歌CSS類,在DOM上新增樣式結點也是不錯的選擇。
總結
Nicholas C. Zakas 是JavaScript界的權威。在寫這篇文章的時候,我發現我取用的很多文章也是他寫的——因為太難找到其他更好的文章。
Zakas的技術演進非常棒,他解釋了很多JavaScript最佳化規則的原因,我已奉為聖經。
原文出處:jonraasch.com
譯文出處:伯樂線上 – Delostik
連結:http://web.jobbole.com/82469/