JavaScript 高級程序設計之最佳實踐

1、可維護性:可理解性、直觀性、可適應性、可擴展性、可調試性
  1. 代碼約定:
    1. 可讀性
      1. 格式化:建議縮進大小爲4個空格
      2. 註釋:函數和方法、大段代碼、複雜的算法、hack
    2. 變量和函數命名
      1. 變量名爲名詞
      2. 函數名爲動詞開始
      3. 變量和函數使用合符邏輯的名字,不要擔憂長度。
    3. 變量類型透明:表示變量類型的三種方式
      1. 初始化: var found = false; //布爾型
      2. 使用匈牙利標記法來指定變量類型:o表明對象,s表明字符串,i表明整數,f表明浮點數,b表明布爾型
      3. 使用類型註釋: var found /*:Boolen*/ = false;
  2. 鬆散耦合:
    1. 解耦HTML/JavaScript
      1. HTML中包含JavaScript,示例:<script type="text/javascript">document.write("hello world!")</script>; // <script>標籤緊密耦合
        <input type="button" value="Click me " onclick="doSomething();"/> //事件屬性值緊密耦合
      2. 理想狀況:HTML和JavaScript應該徹底分離,並經過外部文件和使用DOM附加行爲來包含JavaScript。
        問題:出現JavaScript錯誤就要判斷是在HTML中仍是在JavaScript中,且在doSomething()可用以前就按下button,也會引起JavaScript錯誤。
      3. JavaScript中包含HTML,JavaScript生成HTML,這個應該避免,保持層次的分離有助於很容易的肯定錯誤來源
      4. 理想狀況:JavaScript用於插入數據時,儘可能不直接插入標記,能夠控制標記的顯示和隱藏,而非生成它。另外一種方法是進行Ajax請求並獲取更多要顯示的HTML,這個方法可讓一樣的渲染層(PHP、JSP、Ruby)來輸出標記。
    2. 解耦CSS/JavaScript
      1. 利用JavaScript修改樣式時,應該經過動態修改樣式類而非特定樣式來實現。
      2. 顯示問題的惟一來源應該是CSS,行爲問題的惟一來源應該是JavaScript。
    3. 解耦應用邏輯/事件處理程序
      1. 應用邏輯和事件處理程序相分離,一個事件處理程序應該從事件對象中獲取相關信息,並將這些信息傳送處處理應用程序的某個方法中。
      2. 好處:能夠更容易更改觸發特定過程的事件;其次能夠在不附加到事件的狀況下測試代碼,使其更易建立單元測試或者是自動化應用流程。
      3. 應用和業務邏輯之間鬆散耦合的幾條原則:
        1. 勿將event對象傳給其餘方法;只傳來自event對象中所需的數據;
        2. 任何在應用層面的動做都應該能夠在不執行任何事件處理程序的狀況下進行。
        3. 任何事件處理程序都應該處理事件,而後將處理轉交給應用邏輯。
  3. 編程實踐:
    1. 尊重對象全部權:若是你不負責建立和維護某個對象、它的對象或者它的方法,那麼你就不能對它們進行修改。
      1. 不要爲實例或者原型添加屬性;
      2. 不要爲實例或者原型添加方法;
      3. 不要重定義已存在的方法。
    2. 避免全局變量:最多建立一個全局量,讓其餘對象和函數存在其中。
    3. 避免與null進行比較
      1. 若是值應爲一個引用類型,使用instanceof操做符檢查其構造函數。
      2. 若是值應爲一個基本類型,使用typeof檢查其類型。
      3. 若是是但願對象包含某個特定的方法名,則使用typeof操做符確保指定名稱的方法存在於對象上。
    4. 使用常量
      1. 關鍵在於將數據和使用它的邏輯進行分離
      2. 重複值:任何在多處用到的值都應抽取爲一個常量,也包含css類名,這就限制了當一個值變了而另外一個沒變的時候會形成的錯誤。
      3. 用戶界面字符串:方便國際化
      4. URLs:在web應用中,資源位置很容易變動,因此推薦用一個公共地方存放全部的URL
      5. 任意可能會更改的值

2、保證代碼性能
  1. 注意做用域:
    1. 避免全局查找:使用全局變量和函數確定要比局部的開銷更大,由於涉及做用域鏈上的查找。
      1. 示例代碼:
        function updateUI(){
            var imgs = document.getElementsByTagName("img");
            for(var i=0,len=imgs.length;i<len;i++)
            {
                imgs[i].title = document.title + " image " + i;
            }
            var msg = document.getElementById("msg");
            msg.innerHTML = "Update complete.";
        }

      2. 優化後的代碼

        function updateUI(){
            var doc = document;
            var imgs = doc .getElementsByTagName("img");
            
            for(var i=0,len=imgs.length;i<len;i++)
            {
                imgs[i].title = doc .title + " image " + i;
            }
            var msg = doc .getElementById("msg");
            msg.innerHTML = "Update complete.";
        }

    2. 避免with語句:在性能很是重要的地方必須避免使用with語句。
      1. 和函數相似,with語句會建立本身的做用域,確定會增長其中執行的代碼的做用域鏈的長度。
      2. 必須使用with語句的狀況不多,它主要用於消除額外的字符。在大多數狀況下,能夠用局部變量完成相同的事情而不用引入新的做用域。
      3. 實例代碼:
        function updateBody(){
            with(document.body){
                alert(tagName);
                innerHTML = "hello world!";
            }
        }
        改進後的代碼:

        function updateBody(){
            var body = document.body;
            alert(body.tagName);
            body.innerHTML = "hello world!";
        }

  2. 選擇正確方法
    1. 避免沒必要要的屬性查找
      1. 常數值O(1):指代字面值和存儲在變量中的值,訪問數組元素
      2. 對數值O(log n):
      3. 線性O(n):訪問對象,對象上的任何屬性查找都要比訪問變量或者數組花費更長時間,由於必須在原型鏈中對擁有該名稱的屬性進行一次搜索,屬性查找越多,執行時間久越長。
        一旦屢次用到對象屬性,應該將其存儲在局部變量中。
      4. 平方O(n²):
    2. 優化循環:基本步驟以下:
      1. 減值迭代:在不少狀況下,從最大值開始,在循環中不斷減值的迭代器更加高效。
      2. 簡化終止條件:因爲每次循環過程都會計算終止條件,因此必須保證它儘量快。也就是說避免屬性查找或者其餘O(n)的操做。
      3. 簡化循環體:循環體是執行最多的,因此要確保其被最大限度的優化。確保沒有某些能夠被很容易移除循環的密集計算。
      4. 使用後測試循環:最經常使用的for循環和while循環都是前測試循環,而如do-while這種後測試循環,能夠避免最初終止條件的計算,所以運行更快。
      5. 示例代碼:
        for(var i=0; i < values.length; i++){
            process(value[i]);
        }
        減值迭代優化:
        for(var i=values.length; i >= 0 ; i--){
            process(value[i]);
        }

        後測試循環優化:記住使用後測試循環時必須確保要處理的值至少有一個,空數組會致使多餘的一次循環而前測試循環則能夠避免。
        var i = values.length - 1;
        if(i > -1){
            do{

                process(values[i]);
            }while(--i > 0);
        }


    3. 展開循環
      1. 當循環的次數是肯定的,消除循環並使用屢次函數調用每每更快。
      2. 若是循環中的迭代次數不能事先肯定,可使用duff裝置技術,它以建立者Tom Duff命名,並最先在C語言中使用這項技術。Jeff Greenberg 用JavaScript實現了Duff裝置,基本概念是經過計算迭代的次數是否爲8的倍數將一個循環展開爲一系列語句。
      3. Jeff Greenberg的Duff裝置技術代碼:經過將values數組中元素個數除以8來計算出循環須要進行屢次迭代的。
        //credit: Jeff Greenberg for JS implementation of Duff's Device
        //假設values.length > 0
        var iterations = Math.ceil(values.length / 8);
        var startAt = values.length % 8;
        var i = 0;

        do{
            switch(startAt){
                case 0: process(values[i++]);
                case 1: process(values[i++]);
                case 2: process(values[i++]);
                case 3: process(values[i++]);
                case 4: process(values[i++]);
                case 5: process(values[i++]);
         
                case 6: process(values[i++]);
                case 7: process(values[i++]);
            }
         
            startAt = 0;
        } while (--iterations > 0);


      4. 由Andrew B.King 所著的Speed Up your Site(New Riders,2003),提出了一個更快的Duff裝置技術,將do-while循環分紅2個單獨的循環。一下是例子:
        //credit: Speed Up your Site(New Riders,2003)
        var iterations = Math.floor(values.length / 8);
        var leftover = values.length % 8;

        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);

      5. 在這個實現中,剩餘的計算部分不會再實際循環中處理,而是在一個初始化循環中進行除以8的操做。當處理掉額外元素,繼續執行每次調用8次process()的主循環,這個方法幾乎比原始的Duff裝置實現快上40%。
      6. 針對大數據集使用展開循環能夠節省不少時間,不過對於小數據集,額外的開銷則可能得不償失。

    4. 避免雙重解釋:當JavaScript代碼想解析JavaScript的時候就會存在雙重解釋懲罰。當使用eval函數或者是Function構造函數以及使用setTimeout()傳一個字符串參數時都會發生這種狀況。
      實例代碼:
      //某些代碼求值---避免
      eval("alert('hello world!')");

      //建立新函數---避免
      var sayHi = new Function("alert('hello world!')");

      //設置超時---避免
      setTimeout("alert('hello world!')",500);

      分析:以上代碼中都要解析包含了JavaScript代碼的字符串,這個操做是不能再初始的解析過程當中完成的,由於代碼是包含在字符串中的,也就是說在JavaScript代碼運行的同時必須新啓動一個解析器來解析新的代碼。

      修正後的例子:
      //已修正
      alert('hello world!');

      //建立新函數---已修正
      var sayHi = function(){
          alert('hello world!');
      };

      //設置一個超時---已修正
      setTimeout(function(){
          alert('hello world!');
      },500);
    5. 性能的其餘注意事項:
      1. 原生方法較快:原生方法是用諸如C/C++之類的編譯型語言寫出來的,因此要比JavaScript快的不少不少。JavaScript最容易被忘記的就是能夠在Math對象中找到的複雜的數學運算,這些方法要比任何用JavaScript的一樣方法如正弦、餘弦快的多。
      2. Switch語句較快
      3. 位運算符較快:取模、邏輯與和邏輯或

  3. 最小化語句數:JavaScript代碼中的語句數量也影響所執行的操做的速度。完成多個操做的單個語句要比完成單個操做的多個語句快。
    1. 多個變量聲明
    2. 插入迭代值
    3. 使用素組和對象字面量
  4. 優化DOM交互
    1. 最小化現場更新
      1. 現場更新:須要當即(現場)對頁面對用戶的顯示進行更新。每個更改,不論是插入單個字符,仍是移除整個片斷,都有一個性能懲罰,由於瀏覽器要從新計算無數尺寸以進行更新。
      2. 實例代碼:
        var list = document.getElementById("myList"),
                        item,
                        i;
        for(i = 0; i < 10;i ++){
             item = document.createElement("li");
             list = appendChild(item);
             item.append(document.createTextNode("Item " + i));
        }
        分析:該代碼添加每一個項目時都有2個現場更新:一個添加<li>元素,另外一個給它添加文本節點。總共須要20個現場更新。兩種優化方法:第一種將列表從頁面上移除,最後進行更新,最後再將列表插回到一樣的位置,這個方法不理想,由於每次頁面更新的時候會沒必要要的閃爍。第二個方法是使用文檔片斷來構建DOM結構,接着將其添加到List元素中,這種方法避免了現場更新和頁面閃爍問題。
        優化後的代碼:
        var list = document.getElementById("myList"),
                        fragment.document.createDocumentFragment(),
                        item,
                        i;
        for(i = 0; i < 10;i ++){
             item = document.createElement("li");
             fragment.appendChild(item);
             item.appendChild(document.createTextNode("Item " + i));
        }

        list.appendChild(fragment);

    2. 使用innerHTML:
      1. 頁面中建立DOM節點的方法有:使用諸如createElement()和appendChild()之類的DOM方法,以及使用innerHTML。對於小的DOM更改而言,兩種方法效率都差很少。然而對於大的DOM更改,使用innerHTML要比使用標準DOM方法建立一樣的DOM結構快得多。
      2. 由於當把innerHTML設置爲某個值時,後臺會建立一個HTML解析器,而後使用內部的DOM調用來建立DOM結構,而非基於JavaScript的DOM調用。因爲內部方法是編譯好的而非解釋執行的,因此執行快的多。
      3. 調用innerHTML(和其餘DOM操做同樣)關鍵在於最小化調用它的次數。
    3. 使用事件代理
      1. 頁面上的事件處理程序的數量和頁面響應用戶交互的速度之間有個負相關,爲了減輕這種懲罰,最好使用事件代理。
      2. 事件代理用到了事件冒泡,任何能夠冒泡的事件都不只僅能夠在事件目標上進行處理,目標的任何祖先節點上也能處理,所以能夠將事件處理程序附加到更高層的地方負責多個目標的事件處理,若是在文檔級別附加事件處理程序,就能夠處理整個頁面的事件。
    4. 注意HTMLCollection
      1. 訪問HTMLCollection,無論它是一個屬性仍是一個方法,都是在文檔上進行一個查詢,並且這個查詢開銷很昂貴,最小化訪問HTMLColletcion的次數能夠極大地改進腳本性能。
      2. 優化HTMLCollection訪問最重要的地方在於循環
        實例代碼:
        var images = document.getElementsByTagName("img"),image,i,len;

        for(i=0,len=images.length;i < len;i++){
             image = images[i];
            //處理
        }
      3. 什麼時候會返回HTMLCollection對象:
        1. 進行了對getElementsByTagName()的調用
        2. 獲取了元素的childNodes屬性
        3. 獲取了元素的attributes屬性
        4. 訪問了特殊的集合,如document.forms、document.images等。
相關文章
相關標籤/搜索