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)化過的代碼仍然有不足的地方,后面的部分會(huì)提到。
避免使用with
為什么避免使用with?
-
with并不是必須的,使用局部變量可以達(dá)到同樣的目的 -
with創(chuàng)建了自己的作用域,相當(dāng)于增加了作用域內(nèi)部查找變量的深度
舉一個(gè)例子:
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作用域減小了對(duì)全局變量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í)間恒定,比如使用簡(jiǎn)單值或訪問存貯在變量中的值 |
| O(lg n) | 對(duì)數(shù) | 總執(zhí)行時(shí)間與值的數(shù)量相關(guān),但不一定需要遍歷每一個(gè)值 |
| O(n) | 線性 | 總執(zhí)行時(shí)間與值的數(shù)量線性相關(guān) |
| O(n2) | 平方 | 總執(zhí)行時(shí)間與值的數(shù)量相關(guān),每個(gè)值要獲取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];
避免不必要的屬性查找
在對(duì)象上訪問屬性是一個(gè)O(n)的操作,Javascript 面向?qū)ο蟮某绦蛟O(shè)計(jì)(原型鏈與繼承)文中提到過,訪問對(duì)象中的屬性時(shí),需要沿著原型鏈追溯查找,屬性查找越多,執(zhí)行時(shí)間越長(zhǎng),比如:
var persons = ["Sue", "Jane", "Ben"];
for(let i=0; i<persons.length; i++) {
console.log(persons[i]);
}
上述代碼中,每次循環(huán)都會(huì)比較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)長(zhǎng)度在循環(huán)開始時(shí)即可確定,就將要循環(huán)的長(zhǎng)度在初始化的時(shí)候聲明為一個(gè)局部變量。
優(yōu)化循環(huán)
由于循環(huán)時(shí)反復(fù)執(zhí)行的代碼,動(dòng)輒上百次,因此優(yōu)化循環(huán)時(shí)性能優(yōu)化中很重要的部分。
減值迭代
為什么要進(jìn)行減值迭代,我們比較如下兩個(gè)循環(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ū)別:
- 迭代順序不同
- 前者支持動(dòng)態(tài)增減數(shù)組元素,后者不支持
- 后者性能優(yōu)于前者,前者每次循環(huán)都會(huì)計(jì)算
nums.length,頻繁的屬性查找降低性能
因此,出于性能的考慮,如果不在乎順序,迭代長(zhǎng)度初始即可確定,使用減值迭代更優(yōu)。
簡(jiǎn)化終止條件
上述情況,我們也可以不使用減值迭代,即像上文提到過的,在初始化時(shí)即將迭代長(zhǎng)度賦值給一個(gè)局部變量。
簡(jiǎn)化循環(huán)體
循環(huán)體應(yīng)最大程度地被優(yōu)化,避免進(jìn)行不必要的密集的計(jì)算
使用while循環(huán)
為什么使用while循環(huán),我們可以比較如下兩個(gè)循環(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]);
}
以上兩個(gè)循環(huán)有一個(gè)很明顯的不同點(diǎn):while循環(huán)將每次循環(huán)終止條件的判斷和index的自增合并為一個(gè)語句,在后續(xù)部分會(huì)講解語句數(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ù)”分成兩個(gè)單獨(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引擎會(huì)將其解析成可以執(zhí)行的代碼,意味著,Javascript執(zhí)行到這里需要額外開一個(gè)解釋器來解析字符串,會(huì)明顯降低性能,因此:
- 盡量避免使用
eval() - 避免使用
Function構(gòu)造函數(shù),用一般function來代替 -
setTimeout()傳入函數(shù)作為參數(shù)
其他
使用原生方法
原生方法都是用C/C++之類的編譯語言寫出來的,比Javascript快得多。
使用switch語句
多個(gè)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í)候只需要求得第一個(gè)表達(dá)式的結(jié)果便可以結(jié)束運(yùn)算,而& |無論如何都要求得兩個(gè)表達(dá)式的結(jié)果才可以結(jié)束運(yùn)算,因此后者的性能沒有占太大優(yōu)勢(shì)。
這里,補(bǔ)充一下位運(yùn)算符如何發(fā)揮邏輯運(yùn)算符的功能,首先看幾個(gè)例子:
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
也許你會(huì)恍然大悟,位運(yùn)算符并沒有產(chǎn)生true 或 false,它只是利用了Number(true) === 1 Number(false) === 0 Boolean(1) === true Boolean(0) === false。
最小化語句數(shù)
Javascript代碼中的語句數(shù)量會(huì)影響執(zhí)行的速度,盡量組合語句,可以減少腳本的執(zhí)行時(shí)間。
多個(gè)變量聲明
當(dāng)我們需要聲明多個(gè)變量,比如:
var name = "";
var age = 18;
var hobbies = [];
可以做如下優(yōu)化:
var name = "",
age = 18,
hobbies = [];
合并迭代值
上文中我們提到一個(gè)例子,使用while循環(huán)可以合并自減和判斷終止條件,我們還可以換一種寫法:
var i = nums.length ;
while(len > -1) {
console.log(nums[len--]);
}
即將自減與使用index取值合并為一個(gè)語句。
使用字面量創(chuàng)建數(shù)組和對(duì)象
即將如下代碼:
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)場(chǎng)更新的次數(shù)
如果我們要修改的DOM已經(jīng)顯示在頁(yè)面,那么我們就是在做現(xiàn)場(chǎng)更新,由于每次更新瀏覽器都要重新計(jì)算,重新渲染,非常消耗性能,因此我們應(yīng)該最小化現(xiàn)場(chǎng)更新的次數(shù),比如我們要向頁(yè)面添加一個(gè)列表:
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í)都會(huì)進(jìn)行兩次現(xiàn)場(chǎng)更新,添加div,為div添加文字,總共需要20次現(xiàn)場(chǎng)更新,頁(yè)面要重繪20次。
現(xiàn)場(chǎng)更新的性能瓶頸不在于更新的大小,而在于更新的次數(shù),因此,我們可以將所有的更新一次繪制到頁(yè)面上,有以下兩個(gè)方法:
文檔片段
可以使用文檔片段先收集好要添加的元素,最后在父節(jié)點(diǎn)上調(diào)用appendChild()將片段的子節(jié)點(diǎn)添加到父節(jié)點(diǎn)中,注意,片段本身不會(huì)被添加。
<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()方法有一個(gè)顯著的區(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;
整合冒泡事件處理
頁(yè)面上的事件處理程序數(shù)量與頁(yè)面相應(yīng)用戶交互的速度之間存在負(fù)相關(guān),具體原因有多方面:
- 創(chuàng)建函數(shù)會(huì)占用內(nèi)存
- 綁定事件處理方法時(shí),需要訪問DOM
因此對(duì)于冒泡事件,盡可能由父元素甚至祖先元素代子元素處理,這樣一個(gè)事件處理方法可以負(fù)責(zé)多個(gè)目標(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à)非常昂貴。
下面的每個(gè)項(xiàng)目(以及它們指定的屬性)都返回 HTMLCollection:
- Document (images, applets, links, forms, anchors)
- form (elements)
- map (areas)
- select (options)
- table (rows, tBodies)
- tableSection (rows)
- row (cells)