從字符串拼接看JS優化原則


請把以下用於連接字符串的JavaScript代碼修改為更高效的方式:

var htmlString ='< div class=”container” > ' + '< ul id=”news-list” > ';
for (var i = 0; i < NEWS.length; i++) {
htmlString += '< li > < a href="' +NEWS[i].LINK +'" > +NEWS[i].TITLE + '< /a > < /li >';
}
htmlString += '< /ul > < /div > ';

zhiyelee的回答:

 
JS優化已經討論了很多了,最近又看到 aimingoo的一篇。大體上,aimingoo的說法都是非常正確的。 

除了像aimingoo做個案研究外,這里我想從更一般的角度總結在瀏覽器編程中JS優化的幾個原則。 

首先,與其他語言不同,JS的效率很大程度是取決於JS engine的效率。除了引擎實現的優劣外,引擎自己也會為一些特殊的代碼模式采取一些優化的策略。例如FF、Opera和Safari的JS引擎,都對字符串的拼接運算(+)做了特別優化。顯然,要獲得最大效率,就必須要了解引擎的脾氣,盡量迎合引擎的口味。所以對於不同的引擎,所作的優化極有可能是背道而馳的。 

而如果做跨瀏覽器的web編程,則最大的問題是在於IE6(JScript 5.6)!因為在不打 hotfix的情況下,JScript引擎的垃圾回收的bug,會導致其在真實應用中的performance跟其他瀏覽器根本不在一個數量級上。因此在這種場合做優化,實際上就是為JScript做優化! 

所以第一原則就是 只需要為IE6(未打補丁的JScript 5.6或更早版本)做優化! 

如果你的程序已經優化到在IE6下可以接受的性能,那基本上在其他瀏覽器上性能就完全沒有問題。 

因此,注意我下面講的許多問題在其他引擎上可能完全不同,例如在循環中進行字符串拼接,通常認為需要用Array.join的方式,但是由於SpiderMonkey等引擎對字符串的“+”運算做了優化,結果使用Array.join的效率反而不如直接用“+”!但是如果考慮IE6,則其他瀏覽器上的這種效率的差別根本不值一提。 

JS優化與其他語言的優化也仍然有相同之處。比如說,不要一上來就急吼吼的做優化,那樣毫無意義。優化的關鍵,仍然是要把精力放在最關鍵的地方,也就是瓶頸上。一般來說,瓶頸總是出現在大規模循環的地方。這倒不是說循環本身有性能問題,而是循環會迅速放大可能存在的性能問題。 

所以第二原則就是 以大規模循環體為最主要優化對象。 

以下的優化原則,只在大規模循環中才有意義,在循環體之外做此類優化基本上是沒有意義的。 

目前絕大多數JS引擎都是解釋執行的,而解釋執行的情況下,在所有操作中,函數調用的效率是較低的。此外,過深的prototype繼承鏈或者多級引用也會降低效率。JScript中,10級引用的開銷大體是一次空函數調用開銷的1/2。這兩者的開銷都遠遠大於簡單操作(如四則運算)。 

所以第三原則就是 盡量避免過多的引用層級和不必要的多次方法調用。 

特別要注意的是,有些情況下看似是屬性訪問,實際上是方法調用。例如所有DOM的屬性,實際上都是方法。在遍歷一個NodeList的時候,循環條件對於nodes.length的訪問,看似屬性讀取,實際上是等價於函數調用的。而且IE DOM的實現上,childNodes.length每次是要通過內部遍歷重新計數的。(My god,但是這是真的!因為我測過,childNodes.length的訪問時間與childNodes.length的值成正比!)這非常耗費。所以預先把nodes.length保存到js變量,當然可以提高遍歷的性能。 

同樣是函數調用,用戶自定義函數的效率又遠遠低於語言內建函數,因為后者是對引擎本地方法的包裝,而引擎通常是c,c++,java寫的。進一步,同樣的功能,語言內建構造的開銷通常又比內建函數調用要效率高,因為前者在JS代碼的parse階段就可以確定和優化。 

所以第四原則就是 盡量使用語言本身的構造和內建函數。 

這里有一個例子是 高性能的String.format方法。String.format傳統的實現方式是用String.replace(regex, func),在pattern包含n個占位符(包括重復的)時,自定義函數func就被調用n次。而這個高性能實現中,每次format調用所作的只是一次Array.join然后一次String.replace(regex, string)的操作,兩者都是引擎內建方法,而不會有任何自定義函數調用。兩次內建方法調用和n次的自定義方法調用,這就是性能上的差別。 

同樣是內建特性,性能上也還是有差別的。例如在JScript中對於arguments的訪問性能就很差,幾乎趕上一次函數調用了。因此如果一個可變參數的簡單函數成為性能瓶頸的時候,可以將其內部做一些改變,不要訪問arguments,而是通過對參數的顯式判斷來處理。 

比如: 
function sum() {  
    var r = 0;  
    for (var i = 0; i < arguments.length; i++) {  
        r += arguments[i];  
    }  
    return r;  
}  

 
這個sum通常調用的時候個數是較少的,我們希望改進它在參數較少時的性能。如果改成: 
 
function sum() {  
    switch (arguments.length) {  
        case 1: return arguments[0];  
        case 2: return arguments[0] + arguments[1];  
        case 3: return arguments[0] + arguments[1] + arguments[2];  
        case 4: return arguments[0] + arguments[1] + arguments[2] + arguments[3];  
        default:  
            var r = 0;  
            for (var i = 0; i < arguments.length; i++) {  
                r += arguments[i];  
            }  
            return r;  
    }  
}  
其實並不會有多少提高,但是如果改成: 
function sum(a, b, c, d, e, f, g) {  
    var r = a ? b ? c ? d ? e ? f ? a + b + c + d + e + f : a + b + c + d + e : a + b + c + d : a + b + c : a + b : a : 0;  
    if (g === undefined) return r;  
    for (var i = 6; i < arguments.length; i++) {  
        r += arguments[i];  
    }  
    return r;  
}  
就會提高很多(至少快1倍)。 

最后是第五原則,也往往是真實應用中最重要的性能障礙,那就是 盡量減少不必要的對象創建。 

本身創建對象是有一定的代價的,但是這個代價其實並不大。最根本的問題是由於 JScript愚蠢之極的垃圾回收調度算法,導致隨着對象個數的增加,性能嚴重下降(據微軟的人自己說復雜度是O(n^2))。 

比如我們常見的字符串拼接問題,經過我的測試驗證,單純的多次創建字符串對象其實根本不是性能差的原因。要命的是在對象創建期間的無謂的垃圾回收的開銷。而Array.join的方式,不會創建中間字符串對象,因此就減少了那該死的垃圾回收的開銷。 

因此,如果我們能把大規模對象創建轉化為單一語句,則其性能會得到極大的提高!例如通過構造代碼然后eval——實際上PIES項目中正在根據這個想法來做一個專門的大規模對象產生器…… 

好了上面就是偶總結的JS優化五大原則。 

除了這些原則以外,還有一些特殊情況,如DOM的遍歷,以后有時間再做討論。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM