首先,與其他語言不同,js的效率很大程度是取決于js engine的效率。除了引擎實(shí)現(xiàn)的優(yōu)劣外,引擎自己也會為一些特殊的代碼模式采取一些優(yōu)化的策略。例如ff、opera和safari的js引擎,都對字符串的拼接運(yùn)算(+)做了特別優(yōu)化。顯然,要獲得最大效率,就必須要了解引擎的脾氣,盡量迎合引擎的口味。所以對于不同的引擎,所作的優(yōu)化極有可能是背道而馳的。
而如果做跨瀏覽器的web編程,則最大的問題是在于ie6(jscript 5.6)!因?yàn)樵诓淮騢otfix的情況下,jscript引擎的垃圾回收的bug,會導(dǎo)致其在真實(shí)應(yīng)用中的performance跟其他瀏覽器根本不在一個數(shù)量級上。因此在這種場合做優(yōu)化,實(shí)際上就是為jscript做優(yōu)化!
所以第一原則就是只需要為ie6(未打補(bǔ)丁的jscript 5.6或更早版本)做優(yōu)化!
如果你的程序已經(jīng)優(yōu)化到在ie6下可以接受的性能,那基本上在其他瀏覽器上性能就完全沒有問題。
因此,注意我下面講的許多問題在其他引擎上可能完全不同,例如在循環(huán)中進(jìn)行字符串拼接,通常認(rèn)為需要用array.join的方式,但是由于 spidermonkey等引擎對字符串的“+”運(yùn)算做了優(yōu)化,結(jié)果使用array.join的效率反而不如直接用“+”!但是如果考慮ie6,則其他瀏覽器上的這種效率的差別根本不值一提。
js優(yōu)化與其他語言的優(yōu)化也仍然有相同之處。比如說,不要一上來就急吼吼的做優(yōu)化,那樣毫無意義。優(yōu)化的關(guān)鍵,仍然是要把精力放在最關(guān)鍵的地方,也就是瓶頸上。一般來說,瓶頸總是出現(xiàn)在大規(guī)模循環(huán)的地方。這倒不是說循環(huán)本身有性能問題,而是循環(huán)會迅速放大可能存在的性能問題。
所以第二原則就是以大規(guī)模循環(huán)體為最主要優(yōu)化對象。
以下的優(yōu)化原則,只在大規(guī)模循環(huán)中才有意義,在循環(huán)體之外做此類優(yōu)化基本上是沒有意義的。
目前絕大多數(shù)js引擎都是解釋執(zhí)行的,而解釋執(zhí)行的情況下,在所有操作中,函數(shù)調(diào)用的效率是較低的。此外,過深的prototype繼承鏈或者多級引用也會降低效率。jscript中,10級引用的開銷大體是一次空函數(shù)調(diào)用開銷的1/2。這兩者的開銷都遠(yuǎn)遠(yuǎn)大于簡單操作(如四則運(yùn)算)。
所以第三原則就是盡量避免過多的引用層級和不必要的多次方法調(diào)用。
特別要注意的是,有些情況下看似是屬性訪問,實(shí)際上是方法調(diào)用。例如所有dom的屬性,實(shí)際上都是方法。在遍歷一個nodelist的時候,循環(huán)條件對于nodes.length的訪問,看似屬性讀取,實(shí)際上是等價于函數(shù)調(diào)用的。而且ie dom的實(shí)現(xiàn)上,childnodes.length每次是要通過內(nèi)部遍歷重新計(jì)數(shù)的。(my god,但是這是真的!因?yàn)槲覝y過,childnodes.length的訪問時間與childnodes.length的值成正比?。┻@非常耗費(fèi)。所以預(yù)先把nodes.length保存到j(luò)s變量,當(dāng)然可以提高遍歷的性能。
同樣是函數(shù)調(diào)用,用戶自定義函數(shù)的效率又遠(yuǎn)遠(yuǎn)低于語言內(nèi)建函數(shù),因?yàn)楹笳呤菍σ姹镜胤椒ǖ陌b,而引擎通常是c,c++,java寫的。進(jìn)一步,同樣的功能,語言內(nèi)建構(gòu)造的開銷通常又比內(nèi)建函數(shù)調(diào)用要效率高,因?yàn)榍罢咴趈s代碼的parse階段就可以確定和優(yōu)化。
所以第四原則就是盡量使用語言本身的構(gòu)造和內(nèi)建函數(shù)。
這里有一個例子是高性能的string.format方法。string.format 傳統(tǒng)的實(shí)現(xiàn)方式是用string.replace(regex, func),在pattern包含n個占位符(包括重復(fù)的)時,自定義函數(shù)func就被調(diào)用n次。而這個高性能實(shí)現(xiàn)中,每次format調(diào)用所作的只是一次array.join然后一次string.replace(regex, string)的操作,兩者都是引擎內(nèi)建方法,而不會有任何自定義函數(shù)調(diào)用。兩次內(nèi)建方法調(diào)用和n次的自定義方法調(diào)用,這就是性能上的差別。
同樣是內(nèi)建特性,性能上也還是有差別的。例如在jscript中對于arguments的訪問性能就很差,幾乎趕上一次函數(shù)調(diào)用了。因此如果一個可變參數(shù)的簡單函數(shù)成為性能瓶頸的時候,可以將其內(nèi)部做一些改變,不要訪問arguments,而是通過對參數(shù)的顯式判斷來處理。
比如:
代碼如下:
1. function sum() {
2. var r = 0;
3. for (var i = 0; i < arguments.length; i++) {
4. r += arguments[i];
5. }
6. return r;
7. }
這個sum通常調(diào)用的時候個數(shù)是較少的,我們希望改進(jìn)它在參數(shù)較少時的性能。如果改成:
代碼如下:
1. function sum() {
2. switch (arguments.length) {
3. case 1: return arguments[0];
4. case 2: return arguments[0] + arguments[1];
5. case 3: return arguments[0] + arguments[1] + arguments[2];
6. case 4: return arguments[0] + arguments[1] + arguments[2] + arguments[3];
7. default:
8. var r = 0;
9. for (var i = 0; i < arguments.length; i++) {
10. r += arguments[i];
11. }
12. return r;
13. }
14. }
其實(shí)并不會有多少提高,但是如果改成:
代碼如下:
1. function sum(a, b, c, d, e, f, g) {
2. 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;
3. if (g === undefined) return r;
4. for (var i = 6; i < arguments.length; i++) {
5. r += arguments[i];
6. }
7. return r;
8. }
就會提高很多(至少快1倍)。
最后是第五原則,也往往是真實(shí)應(yīng)用中最重要的性能障礙,那就是盡量減少不必要的對象創(chuàng)建。
本身創(chuàng)建對象是有一定的代價的,但是這個代價其實(shí)并不大。最根本的問題是由于jscript愚蠢之極的垃圾回收調(diào)度算法,導(dǎo)致隨著對象個數(shù)的增加,性能嚴(yán)重下降(據(jù)微軟的人自己說復(fù)雜度是o(n^2))。
比如我們常見的字符串拼接問題,經(jīng)過我的測試驗(yàn)證,單純的多次創(chuàng)建字符串對象其實(shí)根本不是性能差的原因。要命的是在對象創(chuàng)建期間的無謂的垃圾回收的開銷。而array.join的方式,不會創(chuàng)建中間字符串對象,因此就減少了那該死的垃圾回收的開銷。
因此,如果我們能把大規(guī)模對象創(chuàng)建轉(zhuǎn)化為單一語句,則其性能會得到極大的提高!例如通過構(gòu)造代碼然后eval——實(shí)際上pies項(xiàng)目中正在根據(jù)這個想法來做一個專門的大規(guī)模對象產(chǎn)生器……
好了上面就是偶總結(jié)的js優(yōu)化五大原則。