JavaScript最佳實踐:效能

Daredevil發表於2016-05-06

注意作用域

避免全域性查詢

一個例子:

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等。

相關文章