Javascript最初是解釋型語言渤滞,現(xiàn)在,主流瀏覽器內(nèi)置的Javascript引擎基本上都實(shí)現(xiàn)了Javascript的編譯執(zhí)行,即使如此揉稚,我們?nèi)孕枰獌?yōu)化自己寫的Javascript代碼,以獲得最佳性能熬粗。
注意作用域
避免全局作用域
在之前的文章Javascript 變量搀玖、作用域和內(nèi)存問題提到過,由于訪問變量需要在作用域鏈上進(jìn)行查找驻呐,相比于局部變量灌诅,訪問全局變量的開銷更大芳来,因此以下代碼:
var person = {
name: "Sue",
hobbies: ["Yoga", "Jogging"]
};
function hobby() {
for(let i=0; i<person.hobbies.length; i++) {
console.log(person.hobbies[i]);
}
}
可以進(jìn)行如下優(yōu)化:
function hobby() {
let hobbies = person.hobbies;
for(let i=0; i<hobbies.length; i++) {
console.log(hobbies[i]);
}
}
把需要頻繁訪問的全局變量賦值到局部變量中,可以減小查找深度猜拾,進(jìn)而優(yōu)化性能即舌。
當(dāng)然,上述優(yōu)化過的代碼仍然有不足的地方挎袜,后面的部分會提到顽聂。
避免使用with
為什么避免使用with
?
-
with
并不是必須的,使用局部變量可以達(dá)到同樣的目的 -
with
創(chuàng)建了自己的作用域盯仪,相當(dāng)于增加了作用域內(nèi)部查找變量的深度
舉一個例子:
function test() {
var innerW = "";
var outerW = "";
with(window) {
innerW = innerWidth;
outerW = outerWidth;
}
return "Inner W: " + innerW + ", Outer W: " + outerW;
}
test()
// "Inner W: 780, Outer W: 795"
上述代碼中紊搪,with
作用域減小了對全局變量window
的查找深度,不過與此同時(shí)磨总,也增加了作用域中局部變量innerW
和outerW
的查找深度嗦明,功過相抵。
因此我們不如使用局部變量替代with
:
function test() {
var w = window;
var innerW = w.innerWidth;
var outerW = w.outerWidth;
return "Inner W: " + innerW + ", Outer W: " + outerW;
}
上述代碼仍然不是最優(yōu)的蚪燕。
算法復(fù)雜度
一下表格列出了幾種算法復(fù)雜度:
復(fù)雜度 | 名稱 | 描述 |
---|---|---|
O(1) | 常數(shù) | 無論多少值娶牌,執(zhí)行時(shí)間恒定,比如使用簡單值或訪問存貯在變量中的值 |
O(lg n) | 對數(shù) | 總執(zhí)行時(shí)間與值的數(shù)量相關(guān)馆纳,但不一定需要遍歷每一個值 |
O(n) | 線性 | 總執(zhí)行時(shí)間與值的數(shù)量線性相關(guān) |
O(n2) | 平方 | 總執(zhí)行時(shí)間與值的數(shù)量相關(guān)诗良,每個值要獲取n次 |
O(1)
如果我們直接使用字面量,或者訪問保存在變量中的值鲁驶,時(shí)間復(fù)雜度為O(1)鉴裹,比如:
var value = 5;
var sum = 10 + value;
上述代碼進(jìn)行了三次常量查找,分別是5钥弯,10径荔,value,這段代碼整體復(fù)雜度為O(1)
訪問數(shù)組也是時(shí)間復(fù)雜度為O(1)的操作脆霎,以下代碼整體復(fù)雜度為O(1):
var values = [1, 2];
var sum = values[0] + values[1];
避免不必要的屬性查找
在對象上訪問屬性是一個O(n)的操作总处,Javascript 面向?qū)ο蟮某绦蛟O(shè)計(jì)(原型鏈與繼承)文中提到過,訪問對象中的屬性時(shí)睛蛛,需要沿著原型鏈追溯查找鹦马,屬性查找越多,執(zhí)行時(shí)間越長忆肾,比如:
var persons = ["Sue", "Jane", "Ben"];
for(let i=0; i<persons.length; i++) {
console.log(persons[i]);
}
上述代碼中荸频,每次循環(huán)都會比較i<persons.length
,為了避免頻繁的屬性查找客冈,可以進(jìn)行如下優(yōu)化:
var persons = ["Sue", "Jane", "Ben"];
for(let i=0, len = persons.length; i<len ; i++) {
console.log(persons[i]);
}
即如果循環(huán)長度在循環(huán)開始時(shí)即可確定旭从,就將要循環(huán)的長度在初始化的時(shí)候聲明為一個局部變量。
優(yōu)化循環(huán)
由于循環(huán)時(shí)反復(fù)執(zhí)行的代碼,動輒上百次遇绞,因此優(yōu)化循環(huán)時(shí)性能優(yōu)化中很重要的部分键袱。
減值迭代
為什么要進(jìn)行減值迭代,我們比較如下兩個循環(huán):
var nums = [1, 2, 3, 4];
for(let i=0; i<nums.length; i++) {
console.log(nums[i]);
}
for(let i=nums.length-1; i>-1; i--) {
console.log(nums[i]);
}
二者有如下區(qū)別:
- 迭代順序不同
- 前者支持動態(tài)增減數(shù)組元素摹闽,后者不支持
- 后者性能優(yōu)于前者蹄咖,前者每次循環(huán)都會計(jì)算
nums.length
,頻繁的屬性查找降低性能
因此付鹿,出于性能的考慮澜汤,如果不在乎順序,迭代長度初始即可確定舵匾,使用減值迭代更優(yōu)俊抵。
簡化終止條件
上述情況,我們也可以不使用減值迭代坐梯,即像上文提到過的徽诲,在初始化時(shí)即將迭代長度賦值給一個局部變量。
簡化循環(huán)體
循環(huán)體應(yīng)最大程度地被優(yōu)化吵血,避免進(jìn)行不必要的密集的計(jì)算
使用while
循環(huán)
為什么使用while
循環(huán)谎替,我們可以比較如下兩個循環(huán):
var len = nums.length;
for(let i=0; i<len; i++) {
console.log(nums[i]);
}
var i = nums.length ;
while(--len > -1) {
console.log(nums[len]);
}
以上兩個循環(huán)有一個很明顯的不同點(diǎn):while
循環(huán)將每次循環(huán)終止條件的判斷和index的自增合并為一個語句,在后續(xù)部分會講解語句數(shù)量與性能優(yōu)化的關(guān)系蹋辅。
展開循環(huán)
由于建立循環(huán)和處理終止條件需要額外的開銷钱贯,因此如果循環(huán)次數(shù)比較少,而且可以確定侦另,我們可以將其展開秩命,比如:
process(nums[0]);
process(nums[1]);
如果迭代次數(shù)不能事先確定,可以使用Duff裝置褒傅,其中比較著名的是Andrew B. King提出的一種Duff技術(shù)弃锐,通過計(jì)算迭代次數(shù)是否為8的倍數(shù)將循環(huán)展開,將“零頭”與“整數(shù)”分成兩個單獨(dú)的do-while循環(huán)殿托,在處理大數(shù)據(jù)集時(shí)優(yōu)化效果顯著:
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);
避免雙重解釋
eval()
Function()
setTimeout()
可以傳入字符串拿愧,Javascript引擎會將其解析成可以執(zhí)行的代碼,意味著碌尔,Javascript執(zhí)行到這里需要額外開一個解釋器來解析字符串,會明顯降低性能券敌,因此:
- 盡量避免使用
eval()
- 避免使用
Function
構(gòu)造函數(shù)唾戚,用一般function
來代替 -
setTimeout()
傳入函數(shù)作為參數(shù)
其他
使用原生方法
原生方法都是用C/C++之類的編譯語言寫出來的,比Javascript快得多待诅。
使用switch
語句
多個if-else可以轉(zhuǎn)換為switch語句叹坦,還可以按照最可能到最不可能排序case
使用位運(yùn)算符
當(dāng)進(jìn)行數(shù)學(xué)運(yùn)算的時(shí)候,位運(yùn)算操作要比任何布爾運(yùn)算或者算數(shù)運(yùn)算快卑雁。選擇性地用位運(yùn)算替換算數(shù)運(yùn)算可以極大提升復(fù)雜計(jì)算的性能募书。諸如取模绪囱,邏輯與和邏輯或都可
以考慮用位運(yùn)算來替換。
書中的這段話筆者表示不能理解莹捡,由于使用&&
||
做邏輯判斷時(shí)鬼吵,有的時(shí)候只需要求得第一個表達(dá)式的結(jié)果便可以結(jié)束運(yùn)算,而&
|
無論如何都要求得兩個表達(dá)式的結(jié)果才可以結(jié)束運(yùn)算篮赢,因此后者的性能沒有占太大優(yōu)勢齿椅。
這里,補(bǔ)充一下位運(yùn)算符如何發(fā)揮邏輯運(yùn)算符的功能启泣,首先看幾個例子:
7 === 7 & 6 === 6
1
7 === 7 & 5 === 4
0
7 === 7 | 6 ===6
1
7 === 7 | 7 ===6
1
7 === 6 | 6 === 5
0
也許你會恍然大悟涣脚,位運(yùn)算符并沒有產(chǎn)生true
或 false
,它只是利用了Number(true) === 1
Number(false) === 0
Boolean(1) === true
Boolean(0) === false
寥茫。
最小化語句數(shù)
Javascript代碼中的語句數(shù)量會影響執(zhí)行的速度遣蚀,盡量組合語句,可以減少腳本的執(zhí)行時(shí)間纱耻。
多個變量聲明
當(dāng)我們需要聲明多個變量芭梯,比如:
var name = "";
var age = 18;
var hobbies = [];
可以做如下優(yōu)化:
var name = "",
age = 18,
hobbies = [];
合并迭代值
上文中我們提到一個例子,使用while
循環(huán)可以合并自減和判斷終止條件膝迎,我們還可以換一種寫法:
var i = nums.length ;
while(len > -1) {
console.log(nums[len--]);
}
即將自減與使用index取值合并為一個語句粥帚。
使用字面量創(chuàng)建數(shù)組和對象
即將如下代碼:
var array = new Array();
array[0] = 1;
array[1] = 2;
var person = new Object();
person.name = "Sue";
person.age = 18;
替換成:
var array = [1, 2];
var person = { name:"Sue", age:18 };
省了4行代碼。
優(yōu)化DOM操作
DOM操作是最拖累性能的一方面限次,優(yōu)化DOM操作可以顯著提高性能芒涡。
最小化現(xiàn)場更新的次數(shù)
如果我們要修改的DOM已經(jīng)顯示在頁面,那么我們就是在做現(xiàn)場更新卖漫,由于每次更新瀏覽器都要重新計(jì)算费尽,重新渲染,非常消耗性能羊始,因此我們應(yīng)該最小化現(xiàn)場更新的次數(shù)旱幼,比如我們要向頁面添加一個列表:
var body = document.getElementsByTagName("body")[0];
for(let i=0; i<10; i++) {
item = document.createElement("span");
body.appendChild(item);
item.appendChild(document.createTextNode("Item" + i));
}
每次循環(huán)時(shí)都會進(jìn)行兩次現(xiàn)場更新,添加div
突委,為div
添加文字柏卤,總共需要20次現(xiàn)場更新,頁面要重繪20次匀油。
現(xiàn)場更新的性能瓶頸不在于更新的大小缘缚,而在于更新的次數(shù),因此敌蚜,我們可以將所有的更新一次繪制到頁面上桥滨,有以下兩個方法:
文檔片段
可以使用文檔片段先收集好要添加的元素,最后在父節(jié)點(diǎn)上調(diào)用appendChild()
將片段的子節(jié)點(diǎn)添加到父節(jié)點(diǎn)中,注意齐媒,片段本身不會被添加蒲每。
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="container" style="with: 100px; height: 100px; border: 1px solid black;">
<div id="child">this</div>
</div>
<script>
var container = document.getElementById("container"),
fragment = document.createDocumentFragment(),
item,
i;
for (i=0; i < 10; i++) {
item = document.createElement("li");
fragment.appendChild(item);
item.appendChild(document.createTextNode("Item " + i));
}
container.appendChild(fragment);
</script>
</body>
</html>
innerHTML
使用innerHTML與使用諸如createElement()
appendChild()
方法有一個顯著的區(qū)別,前者使用內(nèi)部的DOM來創(chuàng)建DOM結(jié)構(gòu)喻括,后者使用JavaScript的DOM來創(chuàng)建DOM結(jié)構(gòu)邀杏,前者要快得多,之前的例子用innerHTML
改寫為:
var ul = document.getElementById("ul"),
innerHTML = "";
for(let i=0; i<10; i++) {
innerHTML += "<li>Item " + i + "</li>";
}
ul.innerHTML = innerHTML;
整合冒泡事件處理
頁面上的事件處理程序數(shù)量與頁面相應(yīng)用戶交互的速度之間存在負(fù)相關(guān)双妨,具體原因有多方面:
- 創(chuàng)建函數(shù)會占用內(nèi)存
- 綁定事件處理方法時(shí)淮阐,需要訪問DOM
因此對于冒泡事件,盡可能由父元素甚至祖先元素代子元素處理刁品,這樣一個事件處理方法可以負(fù)責(zé)多個目標(biāo)的事件處理泣特,比如:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="container" style="with: 100px; height: 100px; border: 1px solid black;">
<div id="child">this</div>
</div>
<script>
var container = document.getElementById("container");
container.addEventListener("click", function(e) {
switch(e.target.id) {
case "container":
console.log("container clicked");
break;
case "child":
console.log("child clicked");
break;
}
},false);
</script>
</body>
</html>
注意HTMLCollection
訪問HTMLCollection的代價(jià)非常昂貴。
下面的每個項(xiàng)目(以及它們指定的屬性)都返回 HTMLCollection:
- Document (images, applets, links, forms, anchors)
- form (elements)
- map (areas)
- select (options)
- table (rows, tBodies)
- tableSection (rows)
- row (cells)