前端效能之js代碼最佳化點
範圍
JavaScript執行時會構建一個範圍鏈,用來進行變數解析,全域執行內容只有一個object變數,定義了js中所有變數和函數。當建立一個函數時,會出現產生一個新的局部範圍,以this、arguments和命名的形參以及其他局部變數和函數初始化。整個範圍鏈的頂端是活動的範圍,變數解析的順序是首先從當前範圍尋找,當尋找到結果後就終止查詢,否則就繼續往上一層範圍查詢,直到全域範圍為止。因此,一個比較大的變數如果在解析的過程中跨越多層範圍,就會比較影響效能。範圍在效能上的改進就是要減少跨越的層級。
使用局部變數
對於讀寫局部變數的速度是最快的,他們存在當前啟用的範圍下,不過這個特性在Google的chrome瀏覽器和Safari4+的瀏覽器上並不明顯,因為他們使用的v8和Nitro引擎的變數解析速度非常快,變數解析的層數的減少在速度上的提升較少。但是其他瀏覽器都是非常
明顯的,特別是IE8、FireFox瀏覽器。
一個好的原則是:將任何不在當前範圍內的變數,且要使用2次及以上的,使用局部變數進行儲存。
範圍變換
with語句在JavaScript中會將當前執行的範圍進行切換,在這個語句塊內可以訪問相應的對象屬性。但實際上,這種方式是在執行內容的範圍鏈前面插入了新的範圍,因此,當開始進入到with語句中時,對於當前執行內容中的局部變數的解析會跨越多個範圍,會造成效能上的損失。因此需要避免使用with語句。
資料訪問
讀取和寫入資料的方式一共有下面四種:
- 字面量
- 變數
- 數組
- 對象屬性
字面量和局部變數基本可以忽略,對這些資料的訪問基本對效能的提升佔比可以不考慮。真正的區別是從數組和對象的屬性中讀取和寫入資料。從這兩個方式擷取資料需要進行位置的搜尋,包括使用索引下標和屬性名稱。在進行搜尋時,最耗時的就是對象的屬性,特別是多級屬性的尋找非常耗時。
針對這個問題,最佳的方法就是使用局部變數將任何使用1次及以上的對象屬性或者數組元素儲存下來。
//原始版本function procee(data){ if(data.count > 0) for(var i = 0; i < data.count; i++) processData(data.item[i]);}//最佳化版本function(){ var cnt = data.count; if(cnt > 0) for(var i = 0; i < cnt; i++) processData(data.item[i]);}
另外,在處理與DOM相關的對象時,使用局部變數儲存特別重要,因為每個與DOM相關的對象的屬性訪問實際上都是一個DOM查詢,需要盡量減少這種大的對象的屬性訪問,必要使用局部變數暫存。
流程式控制制條件陳述式
當使用多個離散值進行比較時,使用if語句可能會造成比較次數較多,因此使用switch比較好。對於if語句可以使用二分法進行分裂也是一種加強方法,同時可以通過統計每個比較值出現的頻率,將較大的頻率的值放在靠前的位置。
對於上述的變更,可以使用數組下標的索引來進行快速選擇,而不用進行一個一個比較。但是這個必須要保證離散值的個數不能太多,而且可以用索引值代替。
迴圈
JavaScript中的for、while和do-while三種迴圈,這些迴圈中的條件中如果存在對象的屬性,要使用局部變數暫存,否則隨著迴圈次數的增加,每次訪問對象屬性會非常耗時。
另一個加快迴圈的技巧就是講迴圈的遍曆順序按照逆序的方式進行,這樣會比順序遍曆節省50%左右的時間(取決於迴圈內處理的複雜性)。
JavaScript中還有一個單獨為了遍曆一個對象的各個屬性使用的for-in迴圈,在遍曆一個位置屬性的對象時,這是非常有效方法,但是這個方法比普通的迴圈慢很多,因為它要遍曆對象的原型和整個原型鏈上的所有屬性,這是非常耗時的。對於已知對象,使用三種普通的迴圈語句代替for-in是非常好的選擇。
打散迴圈:
對於迴圈次數較少的迴圈語句,可以直接重複調用迴圈快語句來消除迴圈語句。但是對於次數較多的迴圈,Tom Duff最先在C語言中提出了打散迴圈的方法,被稱為Duff’s Device。JavaScript語言的第一版發行者Jeff Greenberg改寫了JavaScript版本的Duff’s Device:
var iter = Math.ceil(values.length / 8);var left = values.length % 8;var i = 0;if (left > 0){ do{ process(values[i++]); }while(--left > 0);}do{ process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]); process(values[i++]);}while(--iter > 0);
上述方法將迴圈次數減少到除8的個數,這個數值8是多次實驗得到的最優值。當遍曆對象的個數特別大時,使用上述方法最佳化之後會比普通迴圈快很多。
字串最佳化字串串連
所有瀏覽器在處理20個字元以下,1000個串連操作以下的情形時,都是非常快速的,這種情況下一般不需要考慮。
當字串過長,或者串連操作太多時,就需要考慮效能問題。對於IE瀏覽器,字串串連的運算子“+”會比較損耗效能,因此一般使用如下方式進行最佳化:
var buf = [], i = 0;buf[i++] = Hello;buf[i++] = ;buf[i++] = world;var text = buf.join();
但是,FireFox開始引入瀏覽器對字串操作的最佳化之後,Chrome、Opera、Safari以及IE 8+都對字串操作做了最佳化,因此不必考慮上述方式。特別地,Chrome和Opera最佳化之後的“+”操作符非常的快速。
字串的trim操作
由於JavaScript沒有原生的trim函數,因此一般都是用Regex進行上述操作:
function trim (s){ return s.replace(/^s+|s+$/g, );}
這裡的Regex使用了或者,實際上有兩個模式,同時還有一個g修飾符,表示全域搜尋。為了加快速度,Steven Levithan給出了最佳化的trim函數:
function trim(s){ s = s.replace(/^s+/, ); for(var i = s.length - 1; i >= 0; i--){ if (/S/.test(s.charAt(i))){ s = s.substring(0, i+1); break; } } return s;}
這裡簡化了Regex,同時對於末尾Null 字元使用逐個檢查的方式去除。
上述這些最佳化的前提都是他們使用的頻率非常大時,對效能的提升才比較大。另外,新的ECMAScript 3.1定義了內建的trim函數,速度會非常快。