主體 分為以下三部分,能力、經驗有限,歡迎拍磚。
1.低效的代碼
2.面向對象的重構重復利用代碼
3.調試的經驗總結
第一部分 日常中低效的代碼
- 加載和運行
<html> <head> <title>Script Example</title> </head> <body> <p> <script type="text/javascript"> document.write("The date is " + (new Date()).toDateString()); </script> </p> </body> </html>
當瀏覽器遇到一個<script>標簽時,正如上面 HTML 頁面中那樣,無法預知 JavaScript 是否在<p>標簽中 添加內容。因此,瀏覽器停下來,運行此 JavaScript 代碼,然后再繼續解析、翻譯頁面。同樣的事情發生 在使用 src 屬性加載 JavaScript 的過程中。瀏覽器必須首先下載外部文件的代碼,這要占用一些時間,然后 解析並運行此代碼。此過程中,頁面解析和用戶交互是被完全阻塞的。
一個<script>標簽可以放在 HTML 文檔的<head>或<body>標簽中,可以在其中多次 出現。傳統上,<script>標簽用於加載外部 JavaScript 文件
第一個 JavaScript 文件開始下載,並阻塞了其他文件的下載過程。進 一步,在 file1.js 下載完之后和 file2.js 開始下載之前有一個延時,這是 file1.js 完全運行所需的時間。每個文件必須等待前一個文件下載完成並運行完之后,才能開始自己的下載過程。當這些文件下載時,用戶面 對一個空白的屏幕。這就是幾年前(現在當網速較慢時,仍可重現這個問題)大多數瀏覽器的行為模式。
因為腳本阻塞其他頁面資源的下載過程,所以推薦的辦法是:將所有<script>標簽放在盡可能接近<body> 標簽底部的位置,盡量減少對整個頁面下載的影響。例如:
<html> <head> <title>Script Example</title> <link rel="stylesheet" type="text/css" href="styles.css"> </head> <body> <p>Hello world!</p> <-- Example of recommended script positioning --> <script type="text/javascript" src="file1.js"></script> <script type="text/javascript" src="file2.js"></script> <script type="text/javascript" src="file3.js"></script> </body> </html>
- 數據訪問
數據存儲在哪里, 關系到代碼運行期間數據被檢索到的速度。在 JavaScript 中,此問題相對簡單,因為數據存儲只有少量方 式可供選擇。正如其他語言那樣,數據存儲位置關系到訪問速度。在 JavaScript 中有四種基本的數據訪問 位置:
直接量(Literal values)
直接量僅僅代表自己,而不存儲於特定位置。
JavaScript 的直接量包括:
字符串(string),數字(Number),布爾值(Boolean),對象(Object), 數組(Array),函數(Function),正則表達式(RegExp),具有特殊意義的空值(null),以及未定義(undefined)。
變量(Variables)
我們使用 var 關鍵字創建用於存儲數據值。
數組項(Array items)
具有數字索引,存儲一個 JavaScript 數組對象。
對象成員(Object members)
具有字符串索引,存儲一個 JavaScript 對象。
每一種數據存儲位置都具有特定的讀寫操作負擔。大多數情況下,對一個直接量和一個局部變量數據訪問的性能差異是微不足道的。
管理作用域(Managing Scope)
作用域概念是理解 JavaScript 的關鍵,不僅從性能的角度,而且從功能的角度。作用域對 JavaScript 有 許多影響,從確定哪些變量可以被函數訪問,到確定 this 的值,首先要理解作用域的工作原理。
作用域鏈和標識符解析(Scope Chains and Identifier Resolution)
每一個 JavaScript 函數都被表示為對象。進一步說,它是一個函數實例。函數對象正如其他對象那樣, 擁有你可以編程訪問的屬性,和一系列不能被程序訪問,僅供 JavaScript 引擎使用的內部屬性。
內部[Scope]屬性包含一個函數被創建的作用域中對象的集合。此集合被稱為函數的作用域鏈,它決定哪些數據可由函數訪問。此函數作用域鏈中的每個對象被稱為一個可變對象,每個可變對象都以“鍵值對”
的形式存在。當一個函數創建后,它的作用域鏈被填充以對象,這些對象代表創建此函數的環境中可訪問的數據。例如下面這個全局函數:
function add(num1, num2) { var sum = num1 + num2; return sum; }
當 add()函數創建后,它的作用域鏈中填入一個單獨的可變對象,此全局對象代表了所有全局范圍定義的變量。此全局對象包含諸如窗口(window)、瀏覽器(browser)和文檔(DOM)之類的訪問接口。(注意: 此圖中只畫出全局變量中很少的一部分,其他部分還很多)。
add()函數的作用域鏈
add 函數的作用域鏈將會在運行時用到。假設運行下面的代碼: var total = add(5, 10);
運行此 add 函數時建立一個內部對象,稱作“運行期上下文”。一個運行期上下文定義了一個函數運行時的環境。對函數的每次運行而言,每個運行期上下文都是獨一的,所以多次調用同一個函數就會導致多次創建運行期上下文。當函數執行完畢,運行期上下文就被銷毀。
一個運行期上下文有它自己的作用域鏈,用於標識符解析。當運行期上下文被創建時,它的作用域鏈被 初始化,連同運行函數的[[Scope]]屬性中所包含的對象。這些值按照它們出現在函數中的順序,被復制到 運行期上下文的作用域鏈中。這項工作一旦完成,一個被稱作“激活對象”的新對象就為運行期上下文創建 好了。此激活對象作為函數執行期的一個可變對象,包含訪問所有局部變量,命名參數,參數集合,和 this 的接口。然后,此對象被推入作用域鏈的前端。當作用域鏈被銷毀時,激活對象也一同銷毀。下圖顯示 了前面實例代碼所對應的運行期上下文和它的作用域鏈。
在函數運行過程中,每遇到一個變量,標識符識別過程要決定從哪里獲得或者存儲數據。此過程搜索運 行期上下文的作用域鏈,查找同名的標識符。搜索工作從運行函數的激活目標之作用域鏈的前端開始。如 果找到了,那么就使用這個具有指定標識符的變量;如果沒找到,搜索工作將進入作用域鏈的下一個對象。 此過程持續運行,直到標識符被找到,或者沒有更多對象可用於搜索,這種情況下標識符將被認為是未定 義的。函數運行時每個標識符都要經過這樣的搜索過程,例如前面的例子中,函數訪問 sum,num1,num2 時都會產生這樣的搜索過程。正是這種搜索過程影響了性能。
在運行期上下文的作用域鏈中, 一個標識符所處的位置越深,它的讀寫速度就越慢。所以,函數中局部變量的訪問速度總是最快的,而全 局變量通常是最慢的(優化的 JavaScript 引擎在某些情況下可以改變這種狀況)。請記住,全局變量總是 處於運行期上下文作用域鏈的最后一個位置,所以總是最遠才能觸及的。
最好盡可能使用局部變量。一個好的經驗法則 是:用局部變量存儲本地范圍之外的變量值,如果它們在函數中的使用多於一次。考慮下面的例子:
function initUI(){ var
bd = document.body, links = document.getElementsByTagName_r("a"),
i = 0, len = links.length;
while(i < len){ update(links[i++]); } document.getElementById("go-btn").onclick = function(){ start(); }; bd.className = "active"; }
此函數包含三個對 document 的引用,document 是一個全局對象。搜索此變量,必須遍歷整個作用域鏈, 直到最后在全局變量對象中找到它。你可以通過這種方法減輕重復的全局變量訪問對性能的影響:首先將 全局變量的引用存儲在一個局部變量中,然后使用這個局部變量代替全局變量。例如,上面的代碼可以重 寫如下:
function initUI(){ var doc = document, bd = doc.body, links = doc.getElementsByTagName_r("a"), i = 0, len = links.length;
while(i < len){ update(links[i++]);
} doc.getElementById("go-btn").onclick = function(){
start();
}; bd.className = "active";
}
DOM 編程(DOM Scripting)
對 DOM 操作代價昂貴,在富網頁應用中通常是一個性能瓶頸。
ECMAScript 需要訪 問 DOM 時,你需要過橋,交一次“過橋費”。你操作 DOM 次數越多,費用就越高。一般的建議是盡量減 少過橋次數,努力停留在 ECMAScript 島上。本章將對此問題給出詳細解答,告訴你應該關注什么地方, 以提高用戶交互速度。
為了給你一個關於 DOM 操作問題的量化印象,考慮下面的例子:
function innerHTMLLoop() { for (var count = 0; count < 15000; count++) { document.getElementById('here').innerHTML += 'a'; }
}
此函數在循環中更新頁面內容。這段代碼的問題是,在每次循環單元中都對 DOM 元素訪問兩次:一次 讀取 innerHTML 屬性能容,另一次寫入它。
一個更有效率的版本將使用局部變量存儲更新后的內容,在循環結束時一次性寫入:
function innerHTMLLoop2() { var content = ''; for (var count = 0; count < 15000; count++) { content += 'a';
} document.getElementById('here').innerHTML += content;
}
你訪問 DOM 越多,代碼的執行速度就越慢。
事件托管(Event Delegation)
當頁面中存在大量元素,而且每個元素有一個或多個事件句柄與之掛接(例如 onclick)時,可能會影 響性能。連接每個句柄都是有代價的,無論其形式是加重了頁面負擔(更多的頁面標記和 JavaScript 代碼) 還是表現在運行期的運行時間上。你需要訪問和修改更多的 DOM 節點,程序就會更慢,特別是因為事件 掛接過程都發生在 onload(或 DOMContentReady)事件中,對任何一個富交互網頁來說那都是一個繁忙的 時間段。掛接事件占用了處理時間,另外,瀏覽器需要保存每個句柄的記錄,占用更多內存。當這些工作 結束時,這些事件句柄中的相當一部分根本不需要(因為並不是 100%的按鈕或者鏈接都會被用戶點到), 所以很多工作都是不必要的。
一個簡單而優雅的處理 DOM 事件的技術是事件托管。它基於這樣一個事實:事件逐層冒泡總能被父元 素捕獲。采用事件托管技術之后,你只需要在一個包裝元素上掛接一個句柄,用於處理子元素發生的所有 事件。
According to the DOM standard, each event has three phases: 根據 DOM 標准,每個事件有三個階段:
- 捕獲
- 到達目標
- 冒泡
當用戶點擊了“menu #1”鏈接,點擊事件首先被<a>元素收到。然后它沿着 DOM 樹冒泡,被<li>元素收 到,然后是<ul>,接着是<div>,等等,一直到達文檔的頂層,甚至 window。這使得你可以只在父元素上 掛接一個事件句柄,來接收所有子元素產生的事件通知。
假設你要為圖中所顯示的文檔提供一個逐步增強的 Ajax 體驗。如果用戶關閉了 JavaScript,菜單中的鏈 接仍然可以正常地重載頁面。但是如果 JavaScript 打開而且用戶代理有足夠能力,你希望截獲所有點擊, 阻止默認行為(轉入鏈接),發送一個 Ajax 請求獲取內容,然后不刷新頁面就能夠更新部分頁面。使用 事件托管實現此功能,你可以在 UL"menu"單元掛接一個點擊監聽器,它封裝所有鏈接並監聽所有 click 事 件,看看他們是否發自一個鏈接。
document.getElementById('menu').onclick = function(e) { e = e || window.event; var target = e.target || e.srcElement; var pageid, hrefparts; if (target.nodeName !== 'A') { return;
} hrefparts = target.href.split('/'); pageid = hrefparts[hrefparts.length - 1]; pageid = pageid.replace('.html', ''); ajaxRequest('xhr.php?page=' + id, updatePageContents); if (typeof e.preventDefault === 'function') {
e.preventDefault(); e.stopPropagation(); } else { e.returnValue = false;
e.cancelBubble = true; }
};
正如你所看到的那樣,事件托管技術並不復雜;你只需要監聽事件,看看他們是不是從你感興趣的元素 中發出的。這里有一些冗余的跨瀏覽器代碼,如果你將它們移入一個可重用的庫中,代碼就變得相當干凈。
- 算法和流 程控制
要熟悉javascript的所有循環方法,不只是單純for
在大多數編程語言中,代碼執行時間多數在循環中度過。在一系列編程模式中,循環是最常用的模式之 一,因此也是提高性能必須關注的地區之一。理解 JavaScript 中循環對性能的影響至關重要,因為死循環 或者長時間運行的循環會嚴重影響用戶體驗。
for 循環,與類 C 語言使用同樣的語法:
for (var i=0; i < 10; i++){ //loop body }
for 循環大概是最常用的 JavaScript 循環結構。它由四部分組成:初始化體,前測條件,后執行體,循環 體。當遇到一個 for 循環時,初始化體首先執行,然后進入前測條件。如果前測條件的計算結果為 true, 則執行循環體。然后運行后執行體。for 循環封裝上的直接性是開發者喜歡的原因。
第二種循環是 while 循環。while 循環是一個簡單的預測試循環,由一個預測試條件和一個循環體構成:
var i = 0;
while(i < 10){
//loop body i++;
}
在循環體執行之前,首先對前測條件進行計算。如果計算結果為 true,那么就執行循環體;否則循環體 將被跳過。任何 for 循環都可以寫成 while 循環,反之亦然。
第三種循環類型是 do-while 循環。do-while 循環是 JavaScript 中唯一一種后測試的循環,它包括兩部分: 循環體和后測試條件體:
var i = 0;
do { //loop body } while (i++ < 10);
在一個 do-while 循環中,循環體至少運行一次,后測試條件決定循環體是否應再次執行。
第四種也是最后一種循環稱為 for-in 循環。此循環有一個非常特殊的用途:它可以枚舉任何對象的命名 屬性。其基本格式如下:
for (var prop in object){ //loop body }
每次循環執行,屬性變量被填充以對象屬性的名字(一個字符串),直到所有的對象屬性遍歷完成才返 回。返回的屬性包括對象的實例屬性和它從原型鏈繼承而來的屬性。
一個典型的數組處理循環,可使用三種循環的任何一種。最常用的代碼寫法如下:
//original loops for (var i=0; i < items.length; i++){
process(items[i]); }
var j=0; while (j < items.length){ process(items[j++]]);
} var k=0;
do {
process(items[k++]);
} while (k < items.length);
在每個循環中,每次運行循環體都要發生如下幾個操作:
1. 在控制條件中讀一次屬性(items.length)
2. 在控制條件中執行一次比較(i < items.length)
3. 比較操作,察看條件控制體的運算結果是不是 true(i < items.length == true)
4. 一次自加操作(i++)
5. 一次數組查找(items[i])
6. 一次函數調用(process(items[i]))
在這些簡單的循環中,即使沒有太多的代碼,每次迭代也要進行許多操作。代碼運行速度很大程度上由 process()對每個項目的操作所決定,即使如此,減少每次迭代中操作的總數可以大幅度提高循環整體性能。
優化循環工作量的第一步是減少對象成員和數組項查找的次數。正如第 2 章討論的,在大多數瀏覽器上, 這些操作比訪問局部變量或直接量需要更長時間。前面的例子中每次循環都查找 items.length。這是一種浪 費,因為該值在循環體執行過程中不會改變,因此產生了不必要的性能損失。你可以簡單地將此值存入一 個局部變量中,在控制條件中使用這個局部變量,從而提高了循環性能:
//minimizing property lookups for (var i=0, len=items.length; i < len; i++){
process(items[i]); }
var j=0, count = items.length;
while (j < count){ process(items[j++]]);
} var k=0, num = items.length;
do { process(items[k++]);
} while (k < num);
以下兩個部分還沒有整理好,爭取在周末發出來。
2.面向對象的重構重復利用代碼
3.調試的經驗總結