JavaScript最佳實踐:效能
注意作用域
避免全域性查詢
一個例子:
function updateUI(){
var imgs = document.getElementByTagName("img");
for(var i=0, len=imgs.length; i<len; i++){
imgs[i].title = document.title + " image " + i;
}
var msg = document.getElementById("msg");
msg.innnerhtml = "Update complete.";
}
該函式可能看上去完全正常,但是它包含了三個對於全域性document物件的引用。如果在頁面上有多個圖片,那麼for迴圈中的document引用就會被執行多次甚至上百次,每次都會要進行作用域鏈查詢。通過建立一個指向document物件的區域性變數,就可以通過限制一次全域性查詢來改進這個函式的效能:
function updateUI(){
var doc = document;
var imgs = doc.getElementByTagName("img");
for(var i=0, len=imgs.length; i<len; i++){
imgs[i].title = doc.title + " image " + i;
}
var msg = doc.getElementById("msg");
msg.innnerhtml = "Update complete.";
}
這裡,首先將document物件存在本地的doc變數中;然後在餘下的程式碼中替換原來的document。與原來的版本相比,現在的函式只有一次全域性查詢,肯定更快。
選擇正確方法
1.避免不必要的屬性查詢
獲取常量值是非常高效的過程
var value = 5;
var sum = 10 + value;
alert(sum);
該程式碼進行了四次常量值查詢:數字5,變數value,數字10和變數sum。
在JavaScript中訪問陣列元素和簡單的變數查詢效率一樣。所以以下程式碼和前面的例子效率一樣:
var value = [5,10];
var sum = value[0] + value[1];
alert(sum);
物件上的任何屬性查詢都比訪問變數或者陣列花費更長時間,因為必須在原型鏈中對擁有該名稱的屬性進行一次搜素。屬性查詢越多,執行時間就越長。
var values = {first: 5, second: 10};
var sum = values.first + values.second;
alert(sum);
這段程式碼使用兩次屬性查詢來計算sum的值。進行一兩次屬性查詢並不會導致顯著的效能問題,但是進行成百上千次則肯定會減慢執行速度。
注意獲取單個值的多重屬性查詢。例如:
var query = window.location.href.substring(window.location.href.indexOf("?"));
在這段程式碼中,有6次屬性查詢:window.location.href.substring()有3次,window.location.href.indexOf()又有3次。只要數一數程式碼中的點的數量,就可以確定查詢的次數了。這段程式碼由於兩次用到了window.location.href,同樣的查詢進行了兩次,因此效率特別不好。
一旦多次用到物件屬性,應該將其儲存在區域性變數中。之前的程式碼可以如下重寫:
var url = window.locaiton.href;
var query = url.substring(url.indexOf("?"));
這個版本的程式碼只有4次屬性查詢,相對於原始版本節省了33%。
一般來講,只要能減少演算法的複雜度,就要儘可能減少。儘可能多地使用區域性變數將屬性查詢替換為值查詢,進一步獎,如果即可以用數字化的陣列位置進行訪問,也可以使用命名屬性(諸如NodeList物件),那麼使用數字位置。
2.優化迴圈
一個迴圈的基本優化步驟如下所示。
(1)減值迭代——大多數迴圈使用一個從0開始、增加到某個特定值的迭代器。在很多情況下,從最大值開始,在迴圈中不斷減值的迭代器更加高效。
(2)簡化終止條件——由於每次迴圈過程都會計算終止條件,所以必須保證它儘可能快。也就是說避免屬性查詢或其他操作。
(3)簡化迴圈體——迴圈是執行最多的,所以要確保其最大限度地優化,確保其他某些可以被很容易移除迴圈的密集計算。
(4使用後測試迴圈——最常用for迴圈和while迴圈都是前測試迴圈。而如do-while這種後測試迴圈,可以避免最初終止條件的計算,因此執行更快。
以下是一個基本的for迴圈:
for(var i=0; i < value.length; i++){
process(values[i]);
}
這段程式碼中變數i從0遞增到values陣列中的元素總數。迴圈可以改為i減值,如下所示:
for(var i=value.length -1; i >= 0; i--){
process(values[i]);
}
終止條件從value.length簡化成了0。
迴圈還能改成後測試迴圈,如下:
var i=values.length -1;
if (i> -1){
do{
process(values[i])
}while(--i>=0) //此處有個勘誤,書上終止條件為(--i>0),經測試,(--i>=0)才是正確的
}
此處最主要的優化是將終止條件和自減操作符組合成了單個語句,迴圈部分已經優化完全了。
記住使用“後測試”迴圈時必須確保要處理的值至少有一個,空陣列會導致多餘的一次迴圈而“前測試”迴圈則可以避免。
3.展開迴圈
當迴圈的次數是確定的,消除迴圈並使用多次函式呼叫往往更快。假設values數組裡面只有3個元素,直接對每個元素呼叫process()。這樣展開迴圈可以消除建立迴圈和處理終止條件的額外開銷,使程式碼執行更快。
//消除迴圈
process(values[0]);
process(values[1]);
process(values[2]);
如果迴圈中的迭代次數不能事先確定,那可以考慮使用一種叫做Duff裝置的技術。Duff裝置的基本概念是通過計算迭代的次數是否為8的倍數將一個迴圈展開為一系列語句。
Andrew B.King提出了一個更快的Duff裝置技術,將do-while迴圈分成2個單獨的迴圈。以下是例子:
var iterations = Math.floor(values.length / 8);
var leftover = values.length % 8;
var i = 0;
if(leftover>0){
do{
process(values[i++]);
}while(--leftover > 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(--iterations > 0);
在這個實現中,剩餘的計算部分不會在實際迴圈中處理,而是在一個初始化迴圈中進行除以8的操作。當處理掉了額外的元素,繼續執行每次呼叫8次process()的主迴圈。
針對大資料集使用展開迴圈可以節省很多時間,但對於小資料集,額外的開銷則可能得不償失。它是要花更多的程式碼來完成同樣的任務,如果處理的不是大資料集,一般來說不值得。
4.避免雙重解釋
當JavaScript程式碼想解析KavaScript的時候就會存在雙重解釋懲罰。當使用eval()函式或者是Function建構函式以及使用setTimeout()傳一個字串引數時都會發生這種情況。
//某些程式碼求值——避免!!
eval("alert('Hello world!')");
//建立新函式——避免!!
var sayHi = new Function("alert('Hello world!')");
//設定超時——避免!!
setTimeout("alert('Hello world!')", 500);
在以上這些例子中,都要解析包含了JavaScript程式碼的字串。這個操作是不能在初始的解析過程中完成的,因為程式碼是包含在字串中的,也就是說在JavaScript程式碼執行的同時必須新啟動一個解析器來解析新的程式碼。例項化一個新的解析器有不容忽視的開銷,所以這種程式碼要比直接解析慢得多。
//已修正
alert('Hello world!');
//建立新函式——已修正
var sayHi = function(){
alert('Hello world!');
};
//設定一個超時——已修正
setTimeout(function(){
alert('Hello world!');
}, 500);
如果要提高程式碼效能,儘可能避免出現需要按照JavaScript解析的字串。
5.效能的其他注意事項
(1)原生方法較快
(2)Switch語句較快
(3)位運算子較快
最小化語句數
1.多個變數宣告
//4個語句——很浪費
var count = 5;
var color = "blue";
var values = [1,2,3];
var now = new Date();
//一個語句
var count = 5,
color = "blue",
values = [1,2,3],
now = new Date();
2.插入迭代值
當使用迭代值的時候,儘可能合併語句。
var name = values[i];
i++;
前面這2句語句各只有一個目的:第一個從values陣列中獲取值,然後儲存在name中;第二個給變數i增加1.這兩句可以通過迭代值插入第一個語句組合成一個語句。
var name = values[i++];
3.使用陣列和物件字面量
//用4個語句建立和初始化陣列——浪費
var values = new Array();
values[0] = 123;
values[1] = 456;
values[2] = 789;
//用4個語句建立和初始化物件——浪費
var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.sayName = function(){
alert(this.name);
};
這段程式碼中,只建立和初始化了一個數組和一個物件。各用了4個語句:一個呼叫建構函式,其他3個分配資料。其實可以很容易地轉換成使用字面量的形式。
//只有一條語句建立和初始化陣列
var values = [13,456,789];
//只有一條語句建立和初始化物件
var person = {
name : "Nicholas",
age : 29,
sayName : function(){
alert(this.name);
}
};
重寫後的程式碼只包含兩條語句,減少了75%的語句量,在包含成千上萬行JavaScript的程式碼庫中,這些優化的價值更大。
只要有可能,儘量使用陣列和物件的字面量表達方式來消除不必要的語句。
優化DOM互動
1.最小化現場更新
一旦你需要訪問的DOM部分是已經顯示的頁面的一部分,那麼你就是在進行一個現場更新。現場更新進行得越多,程式碼完成執行所花的事件就越長。
var list = document.getElementById('myList'),
item,
i;
for (var i = 0; i < 10; i++) {
item = document.createElement("li");
list.appendChild(item);
item.appendChild(document.createTextNode("Item" + i));
}
這段程式碼為列表添加了10個專案。新增每個專案時,都有2個現場更新:一個新增li元素,另一個給它新增文字節點。這樣新增10個專案,這個操作總共要完成20個現場更新。
var list = document.getElementById('myList'),
fragment = document.createDocumentFragment(),
item,
i;
for (var i = 0; i < 10; i++) {
item = document.createElement("li");
fragment.appendChild(item);
item.appendChild(document.createTextNode("Item" + i));
}
list.appendChild(fragment);
在這個例子中只有一次現場更新,它發生在所有專案都建立好之後。文件片段用作一個臨時的佔位符,放置新建立的專案。當給appendChild()傳入文件片段時,只有片段中的子節點被新增到目標,片段本身不會被新增的。
一旦需要更新DOM,請考慮使用文件片段來構建DOM結構,然後再將其新增到現存的文件中。
2.使用innerHTML
有兩種在頁面上建立DOM節點的方法:使用諸如createElement()和appendChild()之類的DOM方法,以及使用innerHTML。對於小的DOM更改而言,兩種方法效率都差不多。然而,對於大的DOM更改,使用innerHTML要比使用標準DOM方法建立同樣的DOM結構快得多。
當把innerHTML設定為某個值時,後臺會建立一個HTML解析器,然後使用內部的DOM呼叫來建立DOM結構,而非基於JavaScript的DOM呼叫。由於內部方法是編譯好的而非解釋執行的,所以執行快得多。
var list = document.getElementById("myList");
html = "";
i;
for (i=0; i < 10; i++){
html += "<li>Item " + i +"</li>";
}
list.innerHTML = html;
使用innerHTML的關鍵在於(和其他的DOM操作一樣)最小化呼叫它的次數。
var list = document.getElementById("myList");
i;
for (i=0; i < 10; i++){
list.innerHTML += "<li>Item " + i +"</li>"; //避免!!!
}
這段程式碼的問題在於每次迴圈都要呼叫innerHTML,這是極其低效的。呼叫innerHTML實際上就是一次現場更新。構建好一個字串然後一次性呼叫innerHTML要比呼叫innerHTML多次快得多。
3.使用事件代理(根據第13章的概念,我認為此處應為“事件委託”更為妥當)
4.注意HTMLCollection
任何時候要訪問HTMLCollection,不管它是一個屬性還是一個方法,都是在文件上進行一個查詢,這個查詢開銷很昂貴。
var images = document.getElementsByTagName("img"),
image,
i,len;
for (i=0, len=images.length; i < len; i++){
image = images[i];
//處理
}
將length和當前引用的images[i]存入變數,這樣就可以最小化對他們的訪問。發生以下情況時會返回HTMLCollection物件:
- 進行了對getElementsByTagName()的呼叫;
- 獲取了元素的childNodes屬性;
- 獲取了元素的attributes屬性;
- 訪問了特殊的集合,如document.forms、document.images等。
廣州vi設計公司 http://www.maiqicn.com 我的007辦公資源網 https://www.wode007.com