前言
隨著計算機的發展,Web富應用時代的到來,Web 2.0早已不再是用div+css高質量還原設計的時代。自Gmail網頁版郵件服務的問世開始,Web前端開發也開啟了新的紀元。用戶需求不斷提高,各種新的技術層出不窮,前端工程師的地位也越來越重要。然而任何事物都是有兩面性的,隨著前端技術的發展,前端業務越來越繁重,這大大增加了JS代碼量。因此,要提高Web的性能,我們不僅需要關注頁面加載的時間,還要注重在頁面上操作的響應速度。那麼,接下來我們討論幾種能夠提高JavaScript效率的方法。
一、從JavaScript的作用域談起
當JavaScript代碼執行時,JavaScript引擎會創建一個執行環境,又叫執行上下文。執行環境定義了變量或函數有權訪問的其他數據,決定了它們的行為,每個執行環境都有一個與它關聯的變量對象,環境中定義的所有函數、變量都保存在這個對象中。在頁面加載的時候,JavaScript引擎會創建一個全局的執行環境,所有全局變量和函數都是作為window對象(浏覽器中)的屬性和方法創建的。在此之後,每執行一個函數,JavaScript引擎都會創建一個對應的執行環境,並將該環境放入環境棧中,所以當前正在執行的函數的執行環境是在環境棧的最頂部的,當函數執行完畢之後,其執行環境會彈出棧,並被銷毀,保存在其中的變量和函數定義也會被銷毀。
當代碼在一個執行環境中執行時,JavaScript引擎會創建變量對象的一個作用域鏈,它可以保證對執行環境有權訪問的變量和函數的有序訪問。作用域鏈的前端始終是當前執行的代碼所在的環境的變量對象。全局環境的作用域鏈中只有一個變量對象,它定義了所有可用的全局變量和函數。當函數被創建時,JavaScript引擎會把創建時執行環境的作用域鏈賦給函數的內部屬性[[scope]];當函數被執行時,JavaScript引擎會創建一個活動對象,最開始時這個活動對象只有一個變量,即arguments對象。該活動對象會出現在執行環境作用域鏈的頂端,接下來是函數[[scope]]屬性中的對象。
當需要查找某個變量或函數時,JavaScript引擎會通過執行環境的作用域鏈來查找變量和函數,從作用域鏈的頂端開始,如果沒找到,則向下尋找直至找到為止。若一直到全局作用域都沒有找到,則該變量或函數為undefined。
舉個栗子:
function add(a,b) { return a + b; } var result = add(2,3);
代碼執行時,add函數有一個僅包含全局變量對象的[[scope]]屬性,add函數執行時,JavaScript引擎創建新的執行環境以及一個包含this、arguments、a、b的活動對象,並將其添加到作用域鏈中。如下圖所示:
二、使用局部變量
了解了作用域鏈的概念,我們應該知道在查找變量會從作用域鏈的頂端開始一層一層的向下找。顯然,查找的層數越多,花費的時間越多。所以為了提高查找的速度,我們應該盡量使用 局部變量(到目前為止,局部變量是JavaScript中讀寫最快的標識符)。
例如:
function createEle() { document.createElement("div"); } function createEle() { var doc = document; doc.createElement("div"); }
當document使用次數比較少時,可能無所謂,可是如果在一個函數的循環中大量使用document,我們可以提前將document變成局部變量。
來看看jquery怎麼寫的:
(function(window, undefined) { var jQuery = function() {} // ... window.jQuery = window.$ = jQuery; })(window);
這樣寫的優勢:
1、window和undefined都是為了減少變量查找所經過的scope作用域。當window通過傳遞給閉包內部之後,在閉包內部使用它的時候,可以把它當成一個局部變量,顯然比原先在window scope下查找的時候要快一些。(原來的window處於作用域鏈的最頂端,查找速度慢)
2、在jquery壓縮版本jquery.min.js中可以將局部變量window替換成單個字母,減小文件大小,提高加載速度。
3、undefined也是JavaScript中的全局屬性。將undefined作為參數傳遞給閉包,因為沒給它傳遞值,它的值就是undefined,這樣閉包內部在使用它的時候就可以把它當做局部變量使用,從而提高查找速度。undefined並不是JavaScript的保留字或者關鍵字。
4、undefined在某些低版本的浏覽器(例如IE8、IE7)中值是可以被修改的(在ECMAScript3中,undefined是可讀/寫的變量,可以給它賦任意值,這個錯誤在ECMAScript5中做了修正),將undefined作為參數並且不給它傳值可以防止因undefined的值被修改而產生的錯誤。
三、避免增長作用域鏈
在JavaScript中,有兩種語句可以臨時增加作用域鏈:with、try-catch
with可以使對象的屬性可以像全局變量來使用,它實際上是將一個新的變量對象添加到執行環境作用域的頂部,這個變量對象包含了指定對象的所有屬性,因此可以直接訪問。
這樣看似很方便,但是增長了作用域鏈,原來函數中的局部變量不在處於作用域鏈的頂端,因此在訪問這些變量的時候要查找到第二層才能找到它。當with語句塊之行結束後,作用域鏈將回到原來的狀態。鑒於with的這個缺點,所以不推薦使用。
try-catch中的catch從句和with類似,也是在作用域鏈的頂端增加了一個對象,該對象包含了由catch指定命名的異常對象。但是因為catch語句只有在放生錯誤的時候才執行,因此影響比較少。
四、字符串鏈接優化
由於字符串是不可變的,所以在進行字符串連接時,需要創建臨時字符串。頻繁創建、銷毀臨時字符串會導致性能低下。
當然,這個問題在新版本浏覽器包括IE8+中都得到了優化,所以不需要擔心
在低版本浏覽器(IE6、IE7)中,我們可以種數組的join方法來代替。
var temp = []; var i = 0; temp[i++] = "Hello"; temp[i++] = " "; temp[i++] ="everyone"; var outcome = temp.join("");
五、條件判斷
當出現條件判斷時,我們采用什麼樣的結構才能使性能最優?
if(val == 0) { return v0; }else if(val == 1) { return v1; }else if(val == 2) { return v2; }else if(val == 3) { return v3; }else if(val == 4) { return v4; }
當條件分支比較多時,我們可以斟酌哪種條件出現的概率比較大,並將對應的語句放在最上面,這樣可以減少判斷次數。
使用switch語句,新版的浏覽器基本上都對swi做了優化,這樣層數比較深時,性能比if會更好
使用數組:
var v = [v0,v1,v2,v3,v4]; return v[valeue];
要求:對應的結果是單一值,而不是一系列操作
另外,其他方面的優化,譬如
if(condition1) { return v1; } else { return v2 } // 改成 if(condition1) { return v1; } return v2;
六、快速循環
1、循環總次數使用局部變量
for( var i = 0;i < arr.length;i++) { } // 改成 var len = arr.length; for( var i = 0;i < len;i++) { }
這樣就避免了每次循環的屬性查找。這點尤其重要,因為在進行dom操作時,很多人會這樣寫:
var divList = document.getElementsByTagName("div"); for( var i = 0;i < divList.length;i++) { }
查找DOM元素的屬性是相對耗時的,所以應該避免這種寫法。
2、如果可以,遞減代替遞增
for(var i = 0;i < arr.length;i++) { } // 改成 for(var i = arr.length - 1;i--;) { } var i = 0; while(i < arr.length) { i++; } // 改成 var i = arr.length - 1; while(i--) { }
i=0的時候會直接跳出,循環次數比較多時還是很有用的。
七、展開循環
var i = arr.length - 1; while(i--) { dosomething(arr[i]); }
遇到這樣的情況時,執行一次循環的時候我們可以選擇不止執行一次函數。
var interations = Math.floor(arr.length / 8); var left = arr.length % 8; var i = 0; if(left) { do { dosomething(arr[i++]); } while(--left); } do { dosomething(arr[i++]); dosomething(arr[i++]); dosomething(arr[i++]); dosomething(arr[i++]); dosomething(arr[i++]); dosomething(arr[i++]); dosomething(arr[i++]); dosomething(arr[i++]); } while(--interations);
當遇到大數組,減少循環的開銷,性能不就提上去了嘛。(至於為什麼是每次循環,調8次函數,大牛測出來的,這樣達到最佳)
八、高效存取數據
JavaScript中4種地方可以存取數據:
字面量值;變量;數組元素;對象屬性
字面量值和變量中存取數據是最快的,從數組元素和對象屬性中存取數據相對較慢,並且隨著深度增加,存取速度會越來越慢,譬如obj.item.value就比obj.item慢。
某些情況下我們可以將對象、數組屬性存成局部變量來提高速度,譬如:
for( var i = 0;i < arr.length;i++) { } // 改成 var len = arr.length; for( var i = 0;i < len;i++) { }
var divList = document.getElementsByTagName("div"); for( var i = 0;i < divList.length;i++) { } // 改成 // var divList = document.getElementsByTagName("div"); for( var i = 0,len = divList.length;i < len;i++) { }
九、事件委托
事件委托就是利用冒泡的原理,將原本應該添加在某些元素身上的監聽事件,添加到其父元素身上,來達到提高性能的效果。
舉個栗子:
<div> <ul> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> <li>6</li> <li>7</li> <li>8</li> <li>9</li> <li>10</li> </ul> </div> <script> window.onload = function() { var ul = document.getElementsByTagName('ul')[0]; var liList = document.getElementsByTagName('li'); for(var i = 0,len = liList.length;i < len;i++) { liList[i].onclick = function() { alert(this.innerHTML); } } } </script>
這樣我們就為每個li添加了監聽事件了。
顯然,我們通過循環為每個li添加監聽事件是不優化的。這樣不僅浪費了內存,在新的li加入的時候我們還要重新為它添加監聽事件。
我們可以這樣寫:
<div> <ul> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> <li>6</li> <li>7</li> <li>8</li> <li>9</li> <li>10</li> </ul> </div> <script> window.onload = function() { var ul = document.getElementsByTagName('ul')[0]; var liList = document.getElementsByTagName('li'); ul.onclick = function(e) { var e = e || window.event; var target = e.target || e.srcElement; if(target.nodeName.toLowerCase() == "li") { alert(target.innerHTML); } } } </script>
這樣寫的好處:
只添加一個監聽事件,節省了內存;新加入li的時候我們也不用為它單獨添加監聽事件;在頁面中添加事件處理程序所需的時候更少,因為我們只需要為一個DOM元素添加事件處理程序。
最後,提一點不會提高性能的建議
if( 2 == value) { }
類似這種判斷的時候推薦常量放在左邊,這樣就可以預防類似 if( value = 2){} 的錯誤了,因為如果少寫了一個等號, if( value = 2) {} 是合法的語句,而且代碼量變大的時候不容易檢查出來。if( 2 = value) {} 這樣少寫了等號JavaScript引擎會直接報錯,我們就可以愉快地改過來了。(只是建議)