優(yōu)化循環(huán)
如果現(xiàn)在有個一個data[]數(shù)組蔗崎,需要對其進行遍歷琼富,應當怎么做雌贱?最簡單的代碼是:
for (var i = 0; i < data.length; i++) {
//do someting
}
這里每次循環(huán)開始前都需要判斷i是否小于data.length凛虽,JavaScript并不會對data.length進行緩存死遭,而是每次比較都會進行一次取值。如我們所知凯旋,JavaScript數(shù)組其實是一個對象呀潭,里面有個length屬性,所以這里實際上就是取得對象的屬性至非。如果直接使用變量的話就會少一次索引對象钠署,如果數(shù)組的元素很多,效率提升還是很可觀的荒椭。所以我們通常將代碼改成如下所示:
for(var i = 0, m = data.length; i < m; i++) {
//do someting
}
這里多加了一個變量m用于存放data.length屬性谐鼎,這樣就可以在每次循環(huán)時,減少一次索引對象趣惠,但是代價是增加了一個變量的空間狸棍,如果遍歷不要求順序,我們甚至可以不用m這個變量存儲長度味悄,在不要求順序的時候可以使用如下代碼:
for(var i = data.length; i--; ) {
//do someting
}
當然我們可以使用while來替代:
var i = data.length;while(i--) {
//do someting
}
這樣就可只使用一個變量了
運算結(jié)果緩存
由于JavaScript中的函數(shù)也是對象(JavaScript中一切都是對象)草戈,所以我們可以給函數(shù)添加任意的屬性。這也就為我們提供符合備忘錄模式的緩存運算結(jié)果的功能侍瑟,比如我們有一個需要大量運算才能得出結(jié)果的函數(shù)如下:
function calculator(params) {
//大量的耗時的計算 return result;
}
如果其中不涉及隨機唐片,參數(shù)一樣時所返回的結(jié)果一致,我們就可以將運算結(jié)果進行緩存從而避免重復的計算:
function calculator(params) {
var cacheKey = JSON.stringify(params);
var cache = calculator.cache = calculator.cache || {};
if(typeof cache[cacheKey] !== 'undefined') {
return cache[cacheKey];
}
//大量耗時的計算
cache[cacheKey] = result; return result;
}
這里將參數(shù)轉(zhuǎn)化為JSON字符串作為key涨颜,如果這個參數(shù)已經(jīng)被計算過费韭,那么就直接返回,否則進行計算庭瑰。計算完畢后再添加入cache中星持,如果需要,可以直接查看cache的內(nèi)容:calculator.cache
這是一種典型的空間換時間的方式见擦,由于瀏覽器的頁面存活時間一般不會很長钉汗,占用的內(nèi)存會很快被釋放(當然也有例外,比如一些WEB應用)鲤屡,所以可以通過這種空間換時間的方式來減少響應時間损痰,提升用戶體驗。這種方式并不適用于如下場合:
- 相同參數(shù)可能產(chǎn)生不同結(jié)果的情況(包含隨機數(shù)之類的)
- 運算結(jié)果占用特別多內(nèi)存的情況
不要在循環(huán)中創(chuàng)建函數(shù)
這個很好理解酒来,每創(chuàng)建一個函數(shù)對象是需要大批量空間的卢未。所以在一個循環(huán)中創(chuàng)建函數(shù)是很不明智的,盡量將函數(shù)移動到循環(huán)之前創(chuàng)建,比如如下代碼:
for(var i = 0, m = data.length; i < m; i++) {
handlerData(data[i], function(data){ //do something });
}
就可以修改為:
var handler = function(data){
//do something
};
for(var i = 0, m = data.length; i < m; i++) {
handlerData(data[i], handler);
}
讓垃圾回收器回收那些不再需要的對象
之前我曾在 淺談V8引擎中的垃圾回收機制 中講到了V8引擎如何進行垃圾回收辽社∥扒剑可以從中看到,如果長時間保存對象滴铅,老生代中占用的空間將增大戳葵,每次在老生代中的垃圾回收過程將會相當漫長。而垃圾回收器判斷一個對象為活對象還是死對象汉匙,是按照是否有活對象或根對象含有對它的引用來判定的拱烁。如果有根對象或者活對象引用了這個對象,它將被判定為活對象噩翠。所以我們需要通過手動消除這些引用來讓垃圾回收器對回收這些對象戏自。
delete
一種方式是通過delete方式來消除對象中的鍵值對,從而消除引用伤锚。但這種方式并不提倡擅笔,它會改變對象的結(jié)構(gòu),可能導致引擎中對對象的存儲方式變更屯援,降級為字典方式進行存儲(詳細請見V8 之旅:對象表示)猛们,不利于JavaScript引擎的優(yōu)化,所以盡量減少使用
null
另一種方式是通過將值設為null來消除引用狞洋。通過將變量或?qū)ο蟮膶傩栽O為null阅懦,可以消除引用,使原本引用的對象成為一個“孤島”徘铝,然后在垃圾回收的時候?qū)ζ溥M行回收。這種方式不會改變對象的結(jié)構(gòu)惯吕,比使用delete要好
全局對象
另外需要注意的是惕它,垃圾回收器認為根對象永遠是活對象,永遠不會對其進行垃圾回收废登。而全局對象就是根對象淹魄,所以全局作用域中的變量將會一直存在
事件處理器的回收
在平常寫代碼的時候,我們經(jīng)常會給一個DOM節(jié)點綁定事件處理器堡距,但有時候我們不需要這些事件處理器后甲锡,就不管它們了,它們默默的在內(nèi)存中保存著羽戒。所以在某些DOM節(jié)點綁定的事件處理器不需要后缤沦,我們應當銷毀它們。同時綁定的時候也盡量使用事件代理的方式進行綁定易稠,以免造成多次重復的綁定導致內(nèi)存空間的浪費缸废,事件代理可見前端性能優(yōu)化(DOM操作篇)
閉包導致的內(nèi)存泄露
JavaScript的閉包可以說即是“天使”又是“魔鬼”,它“天使”的一面是我們可以通過它突破作用域的限制,而其魔鬼的一面就是和容易導致內(nèi)存泄露企量,比如如下情況:
var result = (function() {
var small = {};
var big = new Array(10000000);
//do something
return function(){
if(big.indexOf("someValue") !== -1) {
return null;
} else {
return small;
}
}
})();
這里测萎,創(chuàng)建了一個閉包。使得返回的函數(shù)存儲在result中届巩,而result函數(shù)能夠訪問其作用域內(nèi)的small對象和big對象硅瞧。由于big對象和small對象都可能被訪問,所以垃圾回收器不會去碰這兩個對象恕汇,它們不會被回收腕唧。我們將上述代碼改成如下形式:
var result = (function() {
var small = {};
var big = new Array(10000000);
var hasSomeValue;
//do something
hasSomeValue = big.indexOf("someValue") !== -1;
return function(){
if(hasSomeValue) {
return null;
} else {
return small;
}
}
})();
這樣,函數(shù)內(nèi)部只能夠訪問到hasSomeValue變量和small變量了拇勃,big沒有辦法通過任何形式被訪問到四苇,垃圾回收器將會對其進行回收,節(jié)省了大量的內(nèi)存方咆。
慎用eval和with
Douglas Crockford將eval比作魔鬼月腋,確實在很多方面我們可以找到更好地替代方式。使用它時需要在運行時調(diào)用解釋引擎對eval()函數(shù)內(nèi)部的字符串進行解釋運行瓣赂,這需要消耗大量的時間榆骚。像Function、setInterval煌集、setTimeout也是類似的
Douglas Crockford也不建議使用with妓肢,with會降低性能,通過with包裹的代碼塊苫纤,作用域鏈將會額外增加一層碉钠,降低索引效率
對象的優(yōu)化
緩存需要被使用的對象
JavaScript獲取數(shù)據(jù)的性能有如下順序(從快到慢):變量獲取 > 數(shù)組下標獲取(對象的整數(shù)索引獲染砭小) > 對象屬性獲群胺稀(對象非整數(shù)索引獲取)栗弟。我們可以通過最快的方式代替最慢的方式:
var body = document.body;
var maxLength = someArray.length;//...
需要考慮污筷,作用域鏈和原型鏈中的對象索引。如果作用域鏈和原型鏈較長乍赫,也需要對所需要的變量繼續(xù)緩存瓣蛀,否則沿著作用域鏈和原型鏈向上查找時也會額外消耗時間
緩存正則表達式對象
需要注意,正則表達式對象的創(chuàng)建非常消耗時間雷厂,盡量不要在循環(huán)中創(chuàng)建正則表達式惋增,盡可能多的對正則表達式對象進行復用
考慮對象和數(shù)組
在JavaScript中我們可以使用兩種存放數(shù)據(jù):對象和數(shù)組。由于JavaScript數(shù)組可以存放任意類型數(shù)據(jù)這樣的靈活性罗侯,導致我們經(jīng)常需要考慮何時使用數(shù)組器腋,何時使用對象。我們應當在如下情況下做出考慮:
- 存儲一串相同類型的對象,應當使用數(shù)組
- 存儲一堆鍵值對纫塌,值的類型多樣诊县,應當使用對象
- 所有值都是通過整數(shù)索引,應當使用數(shù)組
數(shù)組使用時的優(yōu)化
- 往數(shù)組中插入混合類型很容易降低數(shù)組使用的效率措左,盡量保持數(shù)組中元素的類型一致
- 如果使用稀疏數(shù)組依痊,它的元素訪問將遠慢于滿數(shù)組的元素訪問。因為V8為了節(jié)省空間怎披,會將稀疏數(shù)組通過字典方式保存在內(nèi)存中胸嘁,節(jié)約了空間,但增加了訪問時間
對象的拷貝
需要注意的是凉逛,JavaScript遍歷對象和數(shù)組時性宏,使用for...in的效率相當?shù)停栽诳截悓ο髸r状飞,如果已知需要被拷貝的對象的屬性毫胜,通過直接賦值的方式比使用for...in方式要來得快,我們可以通過定一個拷貝構(gòu)造函數(shù)來實現(xiàn)诬辈,比如如下代碼:
function copy(source){
var result = {};
var item;
for(item in source) {
result[item] = source[item];
} return result;
}
var backup = copy(source);
可修改為:
function copy(source){
this.property1 = source.property1;
this.property2 = source.property2;
this.property3 = source.property3;
//...
}
var backup = new copy(source);
字面量代替構(gòu)造函數(shù)
JavaScript可以通過字面量來構(gòu)造對象酵使,比如通過[]構(gòu)造一個數(shù)組,{}構(gòu)造一個對象焙糟,/regexp/
構(gòu)造一個正則表達式口渔,我們應當盡力使用字面量來構(gòu)造對象,因為字面量是引擎直接解釋執(zhí)行的穿撮,而如果使用構(gòu)造函數(shù)的話缺脉,需要調(diào)用一個內(nèi)部構(gòu)造器,所以字面量略微要快一點點悦穿。
緩存AJAX
曾經(jīng)聽過一個訪問時間比較(當然不精確):
- cpu cache ≈ 100 * 寄存器
- 內(nèi)存 ≈ 100 * cpu cache
- 外存 ≈ 100 * 內(nèi)存
- 網(wǎng)絡 ≈ 100 * 外存
可看到訪問網(wǎng)絡資源是相當慢的枪向,而AJAX就是JavaScript訪問網(wǎng)絡資源的方式,所以對一些AJAX結(jié)果進行緩存咧党,可以大大減少響應時間。那么如何緩存AJAX結(jié)果呢
函數(shù)緩存
我們可以使用前面緩存復雜計算函數(shù)結(jié)果的方式進行緩存陨亡,通過在函數(shù)對象上構(gòu)造cache對象傍衡,原理一樣,這里略過负蠕。這種方式是精確到函數(shù)蛙埂,而不精確到請求
本地緩存
HTML5提供了本地緩存sessionStorage和localStorage,區(qū)別就是前者在瀏覽器關(guān)閉后會自動釋放遮糖,而后者則是永久的绣的,不會被釋放。它提供的緩存大小以MB為單位,比cookie(4KB)要大得多屡江,所以我們可以根據(jù)AJAX數(shù)據(jù)的存活時間來判斷是存放在sessionStorage還是localStorage當中芭概,在這里以存儲到sessionStorage中為例(localStorage只需把第一行的window.sessionStorage
修改為window.localStorage):
function(data, url, type, callback){
var storage = window.sessionStorage;
var key = JSON.stringify({ url : url, type : type, data : data });
var result = storage.getItem(key);
var xhr;
if (result) {
callback.call(null, result);
} else {
xhr.onreadystatechange = function(){
if(xhr.readyState === 4){
if(xhr.status === 200){
storage.setItem(key, xhr.responseText);
callback.call(null, xhr.responseText);
} else { }
}
};
xhr.open(type, url, async);
xhr.send(data);
}
};
使用布爾表達式的短路
在很多語言中,如果bool表達式的值已經(jīng)能通過前面的條件確定惩嘉,那么后面的判斷條件將不再會執(zhí)行罢洲,比如如下代碼
function calCondition(params) {
var result;
//do lots of work
return !!result;
}
if(otherCondition && calCondition(someParams)) {
console.log(true);
} else {
console.log(false);
}
這里首先會計算otherCondition的值,如果它為false文黎,那么整個正則表達式就為false了惹苗,后續(xù)的需要消耗大量時間的calCondition()函數(shù)就不會被調(diào)用和計算了,節(jié)省了時間
使用原生方法
在JavaScript中耸峭,大多數(shù)原生方法是使用C++編寫的桩蓉,比js寫的方法要快得多,所以盡量使用諸如Math之類的原生對象和方法
字符串拼接
在IE和FF下劳闹,使用直接+=的方式或是+的方式進行字符串拼接院究,將會很慢。我們可以通過Array的join()方法進行字符串拼接玷或。不過并不是所有瀏覽器都是這樣儡首,現(xiàn)在很多瀏覽器使用+=比join()方法還要快
使用web worker
web worker是HTML5提出的一項新技術(shù),通過多線程的方式為JavaScript提供并行計算的能力偏友,通過message的方式進行相互之間的信息傳遞蔬胯,我還沒有仔細研究過
JavaScript文件的優(yōu)化
使用CDN
在編寫JavaScript代碼中,我們經(jīng)常會使用庫(jQuery等等)位他,這些JS庫通常不會對其進行更改氛濒,我們可以將這些庫文件放在CDN(內(nèi)容分發(fā)網(wǎng)絡上),這樣能大大減少響應時間
壓縮與合并JavaScript文件
在網(wǎng)絡中傳輸JS文件鹅髓,文件越長舞竿,需要的時間越多。所以在上線前窿冯,通常都會對JS文件進行壓縮骗奖,去掉其中的注釋、回車醒串、不必要的空格等多余內(nèi)容执桌,如果通過uglify的算法,還可以縮減變量名和函數(shù)名芜赌,從而將JS代碼壓縮仰挣,節(jié)約傳輸時的帶寬。另外經(jīng)常也會將JavaScript代碼合并缠沈,使所有代碼在一個文件之中膘壶,這樣就能夠減少HTTP的請求次數(shù)错蝴。合并的原理和sprite技術(shù)相同
使用Application Cache緩存
出處:天鑲