注意作用域
避免全局查找
一個(gè)例子:
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.";
}
該函數(shù)可能看上去完全正常,但是它包含了三個(gè)對于全局document對象的引用。如果在頁面上有多個(gè)圖片,那么for循環(huán)中的document引用就會被執(zhí)行多次甚至上百次,每次都會要進(jìn)行作用域鏈查找。通過創(chuàng)建一個(gè)指向document對象的局部變量,就可以通過限制一次全局查找來改進(jìn)這個(gè)函數(shù)的性能:
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。與原來的版本相比,現(xiàn)在的函數(shù)只有一次全局查找,肯定更快。
選擇正確方法
1.避免不必要的屬性查找
獲取常量值是非常高效的過程
var value = 5;
var sum = 10 + value;
alert(sum);
該代碼進(jìn)行了四次常量值查找:數(shù)字5,變量value,數(shù)字10和變量sum。
在JavaScript中訪問數(shù)組元素和簡單的變量查找效率一樣。所以以下代碼和前面的例子效率一樣:
var value = [5,10];
var sum = value[0] + value[1];
alert(sum);
對象上的任何屬性查找都比訪問變量或者數(shù)組花費(fèi)更長時(shí)間,因?yàn)楸仨氃谠玩溨袑碛性撁Q的屬性進(jìn)行一次搜素。屬性查找越多,執(zhí)行時(shí)間就越長。
var values = {first: 5, second: 10};
var sum = values.first + values.second;
alert(sum);
這段代碼使用兩次屬性查找來計(jì)算sum的值。進(jìn)行一兩次屬性查找并不會導(dǎo)致顯著的性能問題,但是進(jìn)行成百上千次則肯定會減慢執(zhí)行速度。
注意獲取單個(gè)值的多重屬性查找。例如:
var query = window.location.href.substring(window.location.href.indexOf("?"));
在這段代碼中,有6次屬性查找:window.location.href.substring()有3次,window.location.href.indexOf()又有3次。只要數(shù)一數(shù)代碼中的點(diǎn)的數(shù)量,就可以確定查找的次數(shù)了。這段代碼由于兩次用到了window.location.href,同樣的查找進(jìn)行了兩次,因此效率特別不好。
一旦多次用到對象屬性,應(yīng)該將其存儲在局部變量中。之前的代碼可以如下重寫:
var url = window.locaiton.href;
var query = url.substring(url.indexOf("?"));
這個(gè)版本的代碼只有4次屬性查找,相對于原始版本節(jié)省了33%。
一般來講,只要能減少算法的復(fù)雜度,就要盡可能減少。盡可能多地使用局部變量將屬性查找替換為值查找,進(jìn)一步獎(jiǎng),如果即可以用數(shù)字化的數(shù)組位置進(jìn)行訪問,也可以使用命名屬性(諸如NodeList對象),那么使用數(shù)字位置。
2.優(yōu)化循環(huán)
一個(gè)循環(huán)的基本優(yōu)化步驟如下所示。
(1)減值迭代——大多數(shù)循環(huán)使用一個(gè)從0開始、增加到某個(gè)特定值的迭代器。在很多情況下,從最大值開始,在循環(huán)中不斷減值的迭代器更加高效。
(2)簡化終止條件——由于每次循環(huán)過程都會計(jì)算終止條件,所以必須保證它盡可能快。也就是說避免屬性查找或其他操作。
(3)簡化循環(huán)體——循環(huán)是執(zhí)行最多的,所以要確保其最大限度地優(yōu)化,確保其他某些可以被很容易移除循環(huán)的密集計(jì)算。
(4使用后測試循環(huán)——最常用for循環(huán)和while循環(huán)都是前測試循環(huán)。而如do-while這種后測試循環(huán),可以避免最初終止條件的計(jì)算,因此運(yùn)行更快。
以下是一個(gè)基本的for循環(huán):
for(var i=0; i < value.length; i++){
process(values[i]);
}
這段代碼中變量i從0遞增到values數(shù)組中的元素總數(shù)。循環(huán)可以改為i減值,如下所示:
for(var i=value.length -1; i >= 0; i--){
process(values[i]);
}
終止條件從value.length簡化成了0。
循環(huán)還能改成后測試循環(huán),如下:
var i=values.length -1;
if (i> -1){
do{
process(values[i])
}while(--i>=0) //此處有個(gè)勘誤,書上終止條件為(--i>0),經(jīng)測試,(--i>=0)才是正確的
}
此處最主要的優(yōu)化是將終止條件和自減操作符組合成了單個(gè)語句,循環(huán)部分已經(jīng)優(yōu)化完全了。
記住使用“后測試”循環(huán)時(shí)必須確保要處理的值至少有一個(gè),空數(shù)組會導(dǎo)致多余的一次循環(huán)而“前測試”循環(huán)則可以避免。
3.展開循環(huán)
當(dāng)循環(huán)的次數(shù)是確定的,消除循環(huán)并使用多次函數(shù)調(diào)用往往更快。假設(shè)values數(shù)組里面只有3個(gè)元素,直接對每個(gè)元素調(diào)用process()。這樣展開循環(huán)可以消除建立循環(huán)和處理終止條件的額外開銷,使代碼運(yùn)行更快。
//消除循環(huán)
process(values[0]);
process(values[1]);
process(values[2]);
如果循環(huán)中的迭代次數(shù)不能事先確定,那可以考慮使用一種叫做Duff裝置的技術(shù)。Duff裝置的基本概念是通過計(jì)算迭代的次數(shù)是否為8的倍數(shù)將一個(gè)循環(huán)展開為一系列語句。
Andrew B.King提出了一個(gè)更快的Duff裝置技術(shù),將do-while循環(huán)分成2個(gè)單獨(dú)的循環(huán)。以下是例子:
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);
在這個(gè)實(shí)現(xiàn)中,剩余的計(jì)算部分不會在實(shí)際循環(huán)中處理,而是在一個(gè)初始化循環(huán)中進(jìn)行除以8的操作。當(dāng)處理掉了額外的元素,繼續(xù)執(zhí)行每次調(diào)用8次process()的主循環(huán)。
針對大數(shù)據(jù)集使用展開循環(huán)可以節(jié)省很多時(shí)間,但對于小數(shù)據(jù)集,額外的開銷則可能得不償失。它是要花更多的代碼來完成同樣的任務(wù),如果處理的不是大數(shù)據(jù)集,一般來說不值得。
4.避免雙重解釋
當(dāng)JavaScript代碼想解析KavaScript的時(shí)候就會存在雙重解釋懲罰。當(dāng)使用eval()函數(shù)或者是Function構(gòu)造函數(shù)以及使用setTimeout()傳一個(gè)字符串參數(shù)時(shí)都會發(fā)生這種情況。
//某些代碼求值——避免??!
eval("alert('Hello world!')");
//創(chuàng)建新函數(shù)——避免??!
var sayHi = new Function("alert('Hello world!')");
//設(shè)置超時(shí)——避免??!
setTimeout("alert('Hello world!')", 500);
在以上這些例子中,都要解析包含了JavaScript代碼的字符串。這個(gè)操作是不能在初始的解析過程中完成的,因?yàn)榇a是包含在字符串中的,也就是說在JavaScript代碼運(yùn)行的同時(shí)必須新啟動一個(gè)解析器來解析新的代碼。實(shí)例化一個(gè)新的解析器有不容忽視的開銷,所以這種代碼要比直接解析慢得多。
//已修正
alert('Hello world!');
//創(chuàng)建新函數(shù)——已修正
var sayHi = function(){
alert('Hello world!');
};
//設(shè)置一個(gè)超時(shí)——已修正
setTimeout(function(){
alert('Hello world!');
}, 500);
如果要提高代碼性能,盡可能避免出現(xiàn)需要按照J(rèn)avaScript解析的字符串。
5.性能的其他注意事項(xiàng)
(1)原生方法較快
(2)Switch語句較快
(3)位運(yùn)算符較快
最小化語句數(shù)
1.多個(gè)變量聲明
//4個(gè)語句——很浪費(fèi)
var count = 5;
var color = "blue";
var values = [1,2,3];
var now = new Date();
//一個(gè)語句
var count = 5,
color = "blue",
values = [1,2,3],
now = new Date();
2.插入迭代值
當(dāng)使用迭代值的時(shí)候,盡可能合并語句。
var name = values[i];
i++;
前面這2句語句各只有一個(gè)目的:第一個(gè)從values數(shù)組中獲取值,然后存儲在name中;第二個(gè)給變量i增加1.這兩句可以通過迭代值插入第一個(gè)語句組合成一個(gè)語句。
var name = values[i++];
3.使用數(shù)組和對象字面量
//用4個(gè)語句創(chuàng)建和初始化數(shù)組——浪費(fèi)
var values = new Array();
values[0] = 123;
values[1] = 456;
values[2] = 789;
//用4個(gè)語句創(chuàng)建和初始化對象——浪費(fèi)
var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.sayName = function(){
alert(this.name);
};
這段代碼中,只創(chuàng)建和初始化了一個(gè)數(shù)組和一個(gè)對象。各用了4個(gè)語句:一個(gè)調(diào)用構(gòu)造函數(shù),其他3個(gè)分配數(shù)據(jù)。其實(shí)可以很容易地轉(zhuǎn)換成使用字面量的形式。
//只有一條語句創(chuàng)建和初始化數(shù)組
var values = [13,456,789];
//只有一條語句創(chuàng)建和初始化對象
var person = {
name : "Nicholas",
age : 29,
sayName : function(){
alert(this.name);
}
};
重寫后的代碼只包含兩條語句,減少了75%的語句量,在包含成千上萬行JavaScript的代碼庫中,這些優(yōu)化的價(jià)值更大。
只要有可能,盡量使用數(shù)組和對象的字面量表達(dá)方式來消除不必要的語句。
優(yōu)化DOM交互
1.最小化現(xiàn)場更新
一旦你需要訪問的DOM部分是已經(jīng)顯示的頁面的一部分,那么你就是在進(jìn)行一個(gè)現(xiàn)場更新?,F(xiàn)場更新進(jìn)行得越多,代碼完成執(zhí)行所花的事件就越長。
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個(gè)項(xiàng)目。添加每個(gè)項(xiàng)目時(shí),都有2個(gè)現(xiàn)場更新:一個(gè)添加li元素,另一個(gè)給它添加文本節(jié)點(diǎn)。這樣添加10個(gè)項(xiàng)目,這個(gè)操作總共要完成20個(gè)現(xiàn)場更新。
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);
在這個(gè)例子中只有一次現(xiàn)場更新,它發(fā)生在所有項(xiàng)目都創(chuàng)建好之后。文檔片段用作一個(gè)臨時(shí)的占位符,放置新創(chuàng)建的項(xiàng)目。當(dāng)給appendChild()傳入文檔片段時(shí),只有片段中的子節(jié)點(diǎn)被添加到目標(biāo),片段本身不會被添加的。
一旦需要更新DOM,請考慮使用文檔片段來構(gòu)建DOM結(jié)構(gòu),然后再將其添加到現(xiàn)存的文檔中。
2.使用innerHTML
有兩種在頁面上創(chuàng)建DOM節(jié)點(diǎn)的方法:使用諸如createElement()和appendChild()之類的DOM方法,以及使用innerHTML。對于小的DOM更改而言,兩種方法效率都差不多。然而,對于大的DOM更改,使用innerHTML要比使用標(biāo)準(zhǔn)DOM方法創(chuàng)建同樣的DOM結(jié)構(gòu)快得多。
當(dāng)把innerHTML設(shè)置為某個(gè)值時(shí),后臺會創(chuàng)建一個(gè)HTML解析器,然后使用內(nèi)部的DOM調(diào)用來創(chuàng)建DOM結(jié)構(gòu),而非基于JavaScript的DOM調(diào)用。由于內(nèi)部方法是編譯好的而非解釋執(zhí)行的,所以執(zhí)行快得多。
var list = document.getElementById("myList");
html = "";
i;
for (i=0; i < 10; i++){
html += "<li>Item " + i +"</li>";
}
list.innerHTML = html;
使用innerHTML的關(guān)鍵在于(和其他的DOM操作一樣)最小化調(diào)用它的次數(shù)。
var list = document.getElementById("myList");
i;
for (i=0; i < 10; i++){
list.innerHTML += "<li>Item " + i +"</li>"; //避免?。?!
}
這段代碼的問題在于每次循環(huán)都要調(diào)用innerHTML,這是極其低效的。調(diào)用innerHTML實(shí)際上就是一次現(xiàn)場更新。構(gòu)建好一個(gè)字符串然后一次性調(diào)用innerHTML要比調(diào)用innerHTML多次快得多。
3.使用事件代理(根據(jù)第13章的概念,我認(rèn)為此處應(yīng)為“事件委托”更為妥當(dāng))
4.注意HTMLCollection
任何時(shí)候要訪問HTMLCollection,不管它是一個(gè)屬性還是一個(gè)方法,都是在文檔上進(jìn)行一個(gè)查詢,這個(gè)查詢開銷很昂貴。
var images = document.getElementsByTagName("img"),
image,
i,len;
for (i=0, len=images.length; i < len; i++){
image = images[i];
//處理
}
將length和當(dāng)前引用的images[i]存入變量,這樣就可以最小化對他們的訪問。發(fā)生以下情況時(shí)會返回HTMLCollection對象:
- 進(jìn)行了對getElementsByTagName()的調(diào)用;
- 獲取了元素的childNodes屬性;
- 獲取了元素的attributes屬性;
- 訪問了特殊的集合,如document.forms、document.images等。
(總結(jié)自《JavaScript高級程序設(shè)計(jì)》(第三版))