JavaScript效能優化小知識總結

YouYaInsist的部落格發表於2015-03-04

JavaScript的效能問題不容小覷,這就需要我們開發人員在編寫JavaScript程式時多注意一些細節,本文非常詳細的介紹了一下JavaScript效能優化方面的知識點,絕對是乾貨。

前言

一直在學習javascript,也有看過《犀利開發Jquery核心詳解與實踐》,對這本書的評價只有兩個字犀利,可能是對javascript理解的還不夠透徹異或是自己太笨,更多的是自己不擅於思考懶得思考以至於裡面說的一些精髓都沒有太深入的理解。

鑑於想讓自己有一個提升,進不了一個更加廣闊的天地,總得找一個屬於自己的居所好好生存,所以平時會有意無意的去積累一些使用jQuerry的常用知識,特別是對於效能要求這一塊,總是會想是不是有更好的方式來實現。

下面是我總結的一些小技巧,僅供參考。(我先會說一個總標題,然後用一小段話來說明這個意思 再最後用一個demo來簡單言明)

避免全域性查詢

在一個函式中會用到全域性物件儲存為區域性變數來減少全域性查詢,因為訪問區域性變數的速度要比訪問全域性變數的速度更快些

        function search() {
            //當我要使用當前頁面地址和主機域名
            alert(window.location.href + window.location.host);
        }
        //最好的方式是如下這樣  先用一個簡單變數儲存起來
        function search() {
            var location = window.location;
            alert(location.href + location.host);
        }

定時器

如果針對的是不斷執行的程式碼,不應該使用setTimeout,而應該是用setInterval,因為setTimeout每一次都會初始化一個定時器,而setInterval只會在開始的時候初始化一個定時器

        var timeoutTimes = 0;
        function timeout() {
            timeoutTimes++;
            if (timeoutTimes < 10) {
                setTimeout(timeout, 10);
            }
        }
        timeout();
        //可以替換為:
        var intervalTimes = 0;
        function interval() {
            intervalTimes++;
            if (intervalTimes >= 10) {
                clearInterval(interv);
            }
        }
        var interv = setInterval(interval, 10);

字串連線

如果要連線多個字串,應該少使用+=,如

s+=a;

s+=b;

s+=c;

應該寫成s+=a + b + c;

而如果是收集字串,比如多次對同一個字串進行+=操作的話,最好使用一個快取,使用JavaScript陣列來收集,最後使用join方法連線起來

        var buf = [];
        for (var i = 0; i < 100; i++) {
            buf.push(i.toString());
        }
        var all = buf.join("");

避免with語句

和函式類似 ,with語句會建立自己的作用域,因此會增加其中執行的程式碼的作用域鏈的長度,由於額外的作用域鏈的查詢,在with語句中執行的程式碼肯定會比外面執行的程式碼要慢,在能不使用with語句的時候儘量不要使用with語句

 with (a.b.c.d) {
            property1 = 1;
            property2 = 2;
        }
        //可以替換為:
        var obj = a.b.c.d;
        obj.property1 = 1;
        obj.property2 = 2;

數字轉換成字串

般最好用”" + 1來將數字轉換成字串,雖然看起來比較醜一點,但事實上這個效率是最高的,效能上來說:

(“” +) > String() > .toString() > new String()

浮點數轉換成整型

很多人喜歡使用parseInt(),其實parseInt()是用於將字串轉換成數字,而不是浮點數和整型之間的轉換,我們應該使用Math.floor()或者Math.round()

各種型別轉換

var myVar = "3.14159",
        str = "" + myVar, //  to string  
        i_int = ~ ~myVar,  //  to integer  
        f_float = 1 * myVar,  //  to float  
        b_bool = !!myVar,  /*  to boolean - any string with length 
                                and any number except 0 are true */
        array = [myVar];  //  to array

如果定義了toString()方法來進行型別轉換的話,推薦顯式呼叫toString(),因為內部的操作在嘗試所有可能性之後,會嘗試物件的toString()方法嘗試能否轉化為String,所以直接呼叫這個方法效率會更高

多個型別宣告

在JavaScript中所有變數都可以使用單個var語句來宣告,這樣就是組合在一起的語句,以減少整個指令碼的執行時間,就如上面程式碼一樣,上面程式碼格式也挺規範,讓人一看就明瞭。

插入迭代器

如var name=values[i]; i++;前面兩條語句可以寫成var name=values[i++]

使用直接量

var aTest = new Array(); //替換為
        var aTest = [];
        var aTest = new Object; //替換為
        var aTest = {};
        var reg = new RegExp(); //替換為
        var reg = /../;
        //如果要建立具有一些特性的一般物件,也可以使用字面量,如下:
        var oFruit = new O;
        oFruit.color = "red";
        oFruit.name = "apple";
        //前面的程式碼可用物件字面量來改寫成這樣:
        var oFruit = { color: "red", name: "apple" };

使用DocumentFragment優化多次append

一旦需要更新DOM,請考慮使用文件碎片來構建DOM結構,然後再將其新增到現存的文件中。

for (var i = 0; i < 1000; i++) {
            var el = document.createElement('p');
            el.innerHTML = i;
            document.body.appendChild(el);
        }
        //可以替換為:
        var frag = document.createDocumentFragment();
        for (var i = 0; i < 1000; i++) {
            var el = document.createElement('p');
            el.innerHTML = i;
            frag.appendChild(el);
        }
        document.body.appendChild(frag);

使用一次innerHTML賦值代替構建dom元素

對於大的DOM更改,使用innerHTML要比使用標準的DOM方法建立同樣的DOM結構快得多。

        var frag = document.createDocumentFragment();
        for (var i = 0; i < 1000; i++) {
            var el = document.createElement('p');
            el.innerHTML = i;
            frag.appendChild(el);
        }
        document.body.appendChild(frag);
        //可以替換為:
        var html = [];
        for (var i = 0; i < 1000; i++) {
            html.push('<p>' + i + '</p>');
        }
        document.body.innerHTML = html.join('');

通過模板元素clone,替代createElement

很多人喜歡在JavaScript中使用document.write來給頁面生成內容。事實上這樣的效率較低,如果需要直接插入HTML,可以找一個容器元素,比如指定一個div或者span,並設定他們的innerHTML來將自己的HTML程式碼插入到頁面中。通常我們可能會使用字串直接寫HTML來建立節點,其實這樣做,1無法保證程式碼的有效性2字串操作效率低,所以應該是用document.createElement()方法,而如果文件中存在現成的樣板節點,應該是用cloneNode()方法,因為使用createElement()方法之後,你需要設定多次元素的屬性,使用cloneNode()則可以減少屬性的設定次數——同樣如果需要建立很多元素,應該先準備一個樣板節點

        var frag = document.createDocumentFragment();
        for (var i = 0; i < 1000; i++) {
            var el = document.createElement('p');
            el.innerHTML = i;
            frag.appendChild(el);
        }
        document.body.appendChild(frag);
        //替換為:
        var frag = document.createDocumentFragment();
        var pEl = document.getElementsByTagName('p')[0];
        for (var i = 0; i < 1000; i++) {
            var el = pEl.cloneNode(false);
            el.innerHTML = i;
            frag.appendChild(el);
        }
        document.body.appendChild(frag);

使用firstChild和nextSibling代替childNodes遍歷dom元素

        var nodes = element.childNodes;
        for (var i = 0, l = nodes.length; i < l; i++) {
            var node = nodes[i];
            //……
        }
        //可以替換為:
        var node = element.firstChild;
        while (node) {
            //……
            node = node.nextSibling;

刪除DOM節點

刪除dom節點之前,一定要刪除註冊在該節點上的事件,不管是用observe方式還是用attachEvent方式註冊的事件,否則將會產生無法回收的記憶體。另外,在removeChild和innerHTML=’’二者之間,儘量選擇後者. 因為在sIEve(記憶體洩露監測工具)中監測的結果是用removeChild無法有效地釋放dom節點

使用事件代理

任何可以冒泡的事件都不僅僅可以在事件目標上進行處理,目標的任何祖先節點上也能處理,使用這個知識就可以將事件處理程式附加到更高的地方負責多個目標的事件處理,同樣,對於內容動態增加並且子節點都需要相同的事件處理函式的情況,可以把事件註冊提到父節點上,這樣就不需要為每個子節點註冊事件監聽了。另外,現有的js庫都採用observe方式來建立事件監聽,其實現上隔離了dom物件和事件處理函式之間的迴圈引用,所以應該儘量採用這種方式來建立事件監聽

重複使用的呼叫結果,事先儲存到區域性變數

        //避免多次取值的呼叫開銷
        var h1 = element1.clientHeight + num1;
        var h2 = element1.clientHeight + num2;
        //可以替換為:
        var eleHeight = element1.clientHeight;
        var h1 = eleHeight + num1;
        var h2 = eleHeight + num2;

注意NodeList

最小化訪問NodeList的次數可以極大的改進指令碼的效能

        var images = document.getElementsByTagName('img');
        for (var i = 0, len = images.length; i < len; i++) {

        }

編寫JavaScript的時候一定要知道何時返回NodeList物件,這樣可以最小化對它們的訪問

  • 進行了對getElementsByTagName()的呼叫
  • 獲取了元素的childNodes屬性
  • 獲取了元素的attributes屬性
  • 訪問了特殊的集合,如document.forms、document.images等等

要了解了當使用NodeList物件時,合理使用會極大的提升程式碼執行速度

優化迴圈

可以使用下面幾種方式來優化迴圈

  • 減值迭代

大多數迴圈使用一個從0開始、增加到某個特定值的迭代器,在很多情況下,從最大值開始,在迴圈中不斷減值的迭代器更加高效

  • 簡化終止條件

由於每次迴圈過程都會計算終止條件,所以必須保證它儘可能快,也就是說避免屬性查詢或者其它的操作,最好是將迴圈控制量儲存到區域性變數中,也就是說對陣列或列表物件的遍歷時,提前將length儲存到區域性變數中,避免在迴圈的每一步重複取值。

        var list = document.getElementsByTagName('p');
        for (var i = 0; i < list.length; i++) {
            //……
        }

        //替換為:
        var list = document.getElementsByTagName('p');
        for (var i = 0, l = list.length; i < l; i++) {
            //……
        }
  • 簡化迴圈體

迴圈體是執行最多的,所以要確保其被最大限度的優化

  • 使用後測試迴圈

在JavaScript中,我們可以使用for(;;),while(),for(in)三種迴圈,事實上,這三種迴圈中for(in)的效率極差,因為他需要查詢雜湊鍵,只要可以,就應該儘量少用。for(;;)和while迴圈,while迴圈的效率要優於for(;;),可能是因為for(;;)結構的問題,需要經常跳轉回去。

        var arr = [1, 2, 3, 4, 5, 6, 7];
        var sum = 0;
        for (var i = 0, l = arr.length; i < l; i++) {
            sum += arr[i];
        }

        //可以考慮替換為:

        var arr = [1, 2, 3, 4, 5, 6, 7];
        var sum = 0, l = arr.length;
        while (l--) {
            sum += arr[l];
        }

最常用的for迴圈和while迴圈都是前測試迴圈,而如do-while這種後測試迴圈,可以避免最初終止條件的計算,因此執行更快。

展開迴圈

當迴圈次數是確定的,消除迴圈並使用多次函式呼叫往往會更快。

避免雙重解釋

如果要提高程式碼效能,儘可能避免出現需要按照JavaScript解釋的字串,也就是

  • 儘量少使用eval函式

使用eval相當於在執行時再次呼叫解釋引擎對內容進行執行,需要消耗大量時間,而且使用Eval帶來的安全性問題也是不容忽視的。

  • 不要使用Function構造器

不要給setTimeout或者setInterval傳遞字串引數

        var num = 0;
        setTimeout('num++', 10);
        //可以替換為:
        var num = 0;
        function addNum() {
            num++;
        }
        setTimeout(addNum, 10);

縮短否定檢測

       if (oTest != '#ff0000') {
            //do something
        }
        if (oTest != null) {
            //do something
        }
        if (oTest != false) {
            //do something
        }
        //雖然這些都正確,但用邏輯非操作符來操作也有同樣的效果:
        if (!oTest) {
            //do something
        }

條件分支

  • 將條件分支,按可能性順序從高到低排列:可以減少直譯器對條件的探測次數
  • 在同一條件子的多(>2)條件分支時,使用switch優於if:switch分支選擇的效率高於if,在IE下尤為明顯。4分支的測試,IE下switch的執行時間約為if的一半。
  • 使用三目運算子替代條件分支
        if (a > b) {
            num = a;
        } else {
            num = b;
        }
        //可以替換為:
        num = a > b ? a : b;

使用常量

  • 重複值:任何在多處用到的值都應該抽取為一個常量
  • 使用者介面字串:任何用於顯示給使用者的字串,都應該抽取出來以方便國際化
  • URLs:在Web應用中,資源位置很容易變更,所以推薦用一個公共地方存放所有的URL
  • 任意可能會更改的值:每當你用到字面量值的時候,你都要問一下自己這個值在未來是不是會變化,如果答案是“是”,那麼這個值就應該被提取出來作為一個常量。

避免與null進行比較

由於JavaScript是弱型別的,所以它不會做任何的自動型別檢查,所以如果看到與null進行比較的程式碼,嘗試使用以下技術替換

  • 如果值應為一個引用型別,使用instanceof操作符檢查其建構函式
  • 如果值應為一個基本型別,作用typeof檢查其型別
  • 如果是希望物件包含某個特定的方法名,則使用typeof操作符確保指定名字的方法存在於物件上

避免全域性量

全域性變數應該全部字母大寫,各單詞之間用_下劃線來連線。儘可能避免全域性變數和函式, 儘量減少全域性變數的使用,因為在一個頁面中包含的所有JavaScript都在同一個域中執行。所以如果你的程式碼中宣告瞭全域性變數或者全域性函式的話,後面的程式碼中載入的指令碼檔案中的同名變數和函式會覆蓋掉(overwrite)你的。

//糟糕的全域性變數和全域性函式
var current = null;
function init(){
//...
}
function change() {
    //...
}
function verify() {
    //...
}
//解決辦法有很多,Christian Heilmann建議的方法是:
//如果變數和函式不需要在“外面”引用,那麼就可以使用一個沒有名字的方法將他們全都包起來。
(function(){
var current = null;
function init() {
    //...
}
function change() {
    //...
}
function verify() {
    //...
}
})();
//如果變數和函式需要在“外面”引用,需要把你的變數和函式放在一個“名稱空間”中
//我們這裡用一個function做名稱空間而不是一個var,因為在前者中宣告function更簡單,而且能保護隱私資料
myNameSpace = function() {
    var current = null;

    function init() {
        //...
    }

    function change() {
        //...
    }

    function verify() {
        //...
    }

//所有需要在名稱空間外呼叫的函式和屬性都要寫在return裡面
    return {
        init: init,
        //甚至你可以為函式和屬性命名一個別名
        set: change
    };
};

尊重物件的所有權

因為JavaScript可以在任何時候修改任意物件,這樣就可以以不可預計的方式覆寫預設的行為,所以如果你不負責維護某個物件,它的物件或者它的方法,那麼你就不要對它進行修改,具體一點就是說:

  • 不要為例項或原型新增屬性
  • 不要為例項或者原型新增方法
  • 不要重定義已經存在的方法
  • 不要重複定義其它團隊成員已經實現的方法,永遠不要修改不是由你所有的物件,你可以通過以下方式為物件建立新的功能:
  • 建立包含所需功能的新物件,並用它與相關物件進行互動
  • 建立自定義型別,繼承需要進行修改的型別,然後可以為自定義型別新增額外功能

迴圈引用

如果迴圈引用中包含DOM物件或者ActiveX物件,那麼就會發生記憶體洩露。記憶體洩露的後果是在瀏覽器關閉前,即使是重新整理頁面,這部分記憶體不會被瀏覽器釋放。

簡單的迴圈引用:

        var el = document.getElementById('MyElement');
        var func = function () {
            //…
        }
        el.func = func;
        func.element = el;

但是通常不會出現這種情況。通常迴圈引用發生在為dom元素新增閉包作為expendo的時候。

        function init() {
            var el = document.getElementById('MyElement');
            el.onclick = function () {
                //……
            }
        }
        init();

init在執行的時候,當前上下文我們叫做context。這個時候,context引用了el,el引用了function,function引用了context。這時候形成了一個迴圈引用。

下面2種方法可以解決迴圈引用:

1)  置空dom物件

       function init() {
            var el = document.getElementById('MyElement');
            el.onclick = function () {
                //……
            }
        }
        init();
        //可以替換為:
        function init() {
            var el = document.getElementById('MyElement');
            el.onclick = function () {
                //……
            }
            el = null;
        }
        init();

將el置空,context中不包含對dom物件的引用,從而打斷迴圈應用。

如果我們需要將dom物件返回,可以用如下方法:

        function init() {
            var el = document.getElementById('MyElement');
            el.onclick = function () {
                //……
            }
            return el;
        }
        init();
        //可以替換為:
        function init() {
            var el = document.getElementById('MyElement');
            el.onclick = function () {
                //……
            }
            try {
                return el;
            } finally {
                el = null;
            }
        }
        init();

2)  構造新的context

        function init() {
            var el = document.getElementById('MyElement');
            el.onclick = function () {
                //……
            }
        }
        init();
        //可以替換為:
        function elClickHandler() {
            //……
        }
        function init() {
            var el = document.getElementById('MyElement');
            el.onclick = elClickHandler;
        }
        init();

把function抽到新的context中,這樣,function的context就不包含對el的引用,從而打斷迴圈引用。

通過javascript建立的dom物件,必須append到頁面中

IE下,指令碼建立的dom物件,如果沒有append到頁面中,重新整理頁面,這部分記憶體是不會回收的!

        function create() {
            var gc = document.getElementById('GC');
            for (var i = 0; i < 5000; i++) {
                var el = document.createElement('div');
                el.innerHTML = "test";
                //下面這句可以註釋掉,看看瀏覽器在工作管理員中,點選按鈕然後重新整理後的記憶體變化
                gc.appendChild(el);
            }
        }

釋放dom元素佔用的記憶體

將dom元素的innerHTML設定為空字串,可以釋放其子元素佔用的記憶體。

在rich應用中,使用者也許會在一個頁面上停留很長時間,可以使用該方法釋放積累得越來越多的dom元素使用的記憶體。

釋放javascript物件

在rich應用中,隨著例項化物件數量的增加,記憶體消耗會越來越大。所以應當及時釋放對物件的引用,讓GC能夠回收這些記憶體控制元件。

物件:obj = null

物件屬性:delete obj.myproperty

陣列item:使用陣列的splice方法釋放陣列中不用的item

避免string的隱式裝箱

對string的方法呼叫,比如’xxx’.length,瀏覽器會進行一個隱式的裝箱操作,將字串先轉換成一個String物件。推薦對宣告有可能使用String例項方法的字串時,採用如下寫法:

var myString = new String(‘Hello World’);

鬆散耦合

1、解耦HTML/JavaScript

JavaScript和HTML的緊密耦合:直接寫在HTML中的JavaScript、使用包含內聯程式碼的<script>元素、使用HTML屬性來分配事件處理程式等

HTML和JavaScript的緊密耦合:JavaScript中包含HTML,然後使用innerHTML來插入一段html文字到頁面

其實應該是保持層次的分離,這樣可以很容易的確定錯誤的來源,所以我們應確保HTML呈現應該儘可能與JavaScript保持分離

2、解耦CSS/JavaScript

顯示問題的唯一來源應該是CSS,行為問題的唯一來源應該是JavaScript,層次之間保持鬆散耦合才可以讓你的應用程式更加易於維護,所以像以下的程式碼element.style.color=”red”儘量改為element.className=”edit”,而且不要在css中通過表示式嵌入JavaScript

3、解耦應用程式/事件處理程式

將應用邏輯和事件處理程式相分離:一個事件處理程式應該從事件物件中提取,並將這些資訊傳送給處理應用邏輯的某個方法中。這樣做的好處首先可以讓你更容易更改觸發特定過程的事件,其次可以在不附加事件的情況下測試程式碼,使其更易建立單元測試

效能方面的注意事項

1、儘量使用原生方法

2、switch語句相對if較快

通過將case語句按照最可能到最不可能的順序進行組織

3、位運算較快

當進行數字運算時,位運算操作要比任何布林運算或者算數運算快

4、巧用||&&布林運算子

        function eventHandler(e) {
            if (!e) e = window.event;
        }
        //可以替換為:
        function eventHandler(e) {
            e = e || window.event;
        }
        if (myobj) {
            doSomething(myobj);
        }
        //可以替換為:
        myobj && doSomething(myobj);

避免錯誤應注意的地方

1、每條語句末尾須加分號

在if語句中,即使條件表示式只有一條語句也要用{}把它括起來,以免後續如果新增了語句之後造成邏輯錯誤

2、使用+號時需謹慎

JavaScript 和其他程式語言不同的是,在 JavaScript 中,’+'除了表示數字值相加,字串相連線以外,還可以作一元運算子用,把字串轉換為數字。因而如果使用不當,則可能與自增符’++’混淆而引起計算錯誤

        var valueA = 20;
        var valueB = "10";
        alert(valueA + valueB);     //ouput: 2010 
        alert(valueA + (+valueB));  //output: 30 
        alert(valueA + +valueB);    //output:30 
        alert(valueA ++ valueB);     //Compile error

3、使用return語句需要注意

一條有返回值的return語句不要用()括號來括住返回值,如果返回表示式,則表示式應與return關鍵字在同一行,以避免壓縮時,壓縮工具自動加分號而造成返回與開發人員不一致的結果

        function F1() {
            var valueA = 1;
            var valueB = 2;
            return valueA + valueB;
        }
        function F2() {
            var valueA = 1;
            var valueB = 2;
            return
            valueA + valueB;
        }
        alert(F1());  //output: 3 
        alert(F2());  //ouput: undefined

==和===的區別

避免在if和while語句的條件部分進行賦值,如if (a = b),應該寫成if (a == b),但是在比較是否相等的情況下,最好使用全等執行符,也就是使用===和!==操作符會相對於==和!=會好點。==和!=操作符會進行型別強制轉換

        var valueA = "1";
        var valueB = 1;
        if (valueA == valueB) {
            alert("Equal");
        }
        else {
            alert("Not equal");
        }
        //output: "Equal"
        if (valueA === valueB) {
            alert("Equal");
        }
        else {
            alert("Not equal");
        }
        //output: "Not equal"

不要使用生偏語法

不要使用生偏語法,寫讓人迷惑的程式碼,雖然計算機能夠正確識別並執行,但是晦澀難懂的程式碼不方便以後維護

函式返回統一型別

雖然JavaScript是弱型別的,對於函式來說,前面返回整數型資料,後面返回布林值在編譯和執行都可以正常通過,但為了規範和以後維護時容易理解,應保證函式應返回統一的資料型別

總是檢查資料型別

要檢查你的方法輸入的所有資料,一方面是為了安全性,另一方面也是為了可用性。使用者隨時隨地都會輸入錯誤的資料。這不是因為他們蠢,而是因為他們很忙,並且思考的方式跟你不同。用typeof方法來檢測你的function接受的輸入是否合法

何時用單引號,何時用雙引號

雖然在JavaScript當中,雙引號和單引號都可以表示字串, 為了避免混亂,我們建議在HTML中使用雙引號,在JavaScript中使用單引號,但為了相容各個瀏覽器,也為了解析時不會出錯,定義JSON物件時,最好使用雙引號

部署

  • 用JSLint執行JavaScript驗證器來確保沒有語法錯誤或者是程式碼沒有潛在的問
  • 部署之前推薦使用壓縮工具將JS檔案壓縮
  • 檔案編碼統一用UTF-8
  • JavaScript 程式應該儘量放在 .js 的檔案中,需要呼叫的時候在 HTML 中以 <script src=”filename.js”> 的形式包含進來。JavaScript 程式碼若不是該 HTML 檔案所專用的,則應儘量避免在 HTML 檔案中直接編寫 JavaScript 程式碼。因為這樣會大大增加 HTML 檔案的大小,無益於程式碼的壓縮和快取的使用。另外,<script src=”filename.js”> 標籤應儘量放在檔案的後面,最好是放在</body>標籤前。這樣會降低因載入 JavaScript 程式碼而影響頁面中其它元件的載入時間。

永遠不要忽略程式碼優化工作,重構是一項從專案開始到結束需要持續的工作,只有不斷的優化程式碼才能讓程式碼的執行效率越來越好

相關文章