前言
最近在翻《高效能JavaScript》這本書(2010年版 丁琛譯),感覺可能是因為瀏覽器引擎的改進或是其他原因,書中有些原本能提高效能的程式碼在最新的瀏覽器中已經失效。但是有些章節的有些內容還是相當不錯的,譬如第八章程式設計實踐,為了方便以後的查閱,對此做個總結。失效的程式碼也會在以後做更進一步的探索。
避免雙重求值
這個優化策略很好理解,我們可能都已經不知不覺地運用在了實際的程式設計中:
1 2 3 4 5 6 7 |
// not use this setTimeout('alert("hello world")', 1000); // use this setTimeout(function() { alert('hello world'); }, 1000); |
上面的兩段程式碼都能執行,但是我們一般用第二種,同樣的對於setInterval來說也是如此,類似的還有eval()以及Function()建構函式。JavaScript像其他很多指令碼語言一樣,允許你在程式中提取一個包含程式碼的字串,然後動態執行它。當你在JavaScript程式碼中執行另一段JavaScript程式碼時,都會導致雙重求值的效能消耗,此程式碼首先會以正常的方式求值,然後在執行過程中對包含於字串中的程式碼發起另一個求值運算。所以,大多數時候沒必要使用eval()和Function(),就避免使用它們;至於setTimeout和setInterval,建議傳入函式而不是字串來作為第一個引數。
由此聯想到了php中的”和“”,我們儘量用”去引用字串,也是同樣的原因。
使用Object/Array直接量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// use this var obj = { name: 'hanzichi', age: 10 }; var arr = [0, 1, 2, 3]; // not use this var obj = new Object(); obj.name = 'hanzichi'; obj.age = 10; var arr = new Array(); arr[0] = 0; arr[1] = 1; arr[2] = 2; arr[3] = 3; |
不要重複工作
也許最常見的重複工作就是瀏覽器探測(當然“不要重複工作”只是一種思想,並不一定針對瀏覽器探測)。
考慮一個新增事件處理器的例子,典型的跨瀏覽器程式碼寫法如下:
1 2 3 4 5 6 7 8 9 10 11 |
function addHandler(target, eventType, handler) { if (target.addEventListener) { // DOM2 Events target.addEventListener(eventType, handler, false); } else { // IE target.attachEvent('on' + eventType, handler); } } addHandler(document, 'click', function() { console.log('hello world'); }); |
但是如果一個頁面呼叫了好多次addHandler函式新增事件,每次都會去做瀏覽器的判斷,但是事實是每次的判斷結果都是一樣的,因為瀏覽器並不會變化,這時我們就可以針對“不要重複工作”做一個優化策略。
- 延遲載入
延遲載入,也稱惰性載入,惰性載入等。延遲載入意味著在資訊被使用前不會做任何操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function addHandler(target, eventType, handler) { if (target.addEventListener) { // DOM2 Events addHandler = function(target, eventType, handler) { target.addEventListener(eventType, handler, false); }; } else { // IE addHandler = function(target, eventType, handler) { target.attachEvent('on' + eventType, handler); }; } addHandler(target, eventType, handler); } // 呼叫 addHandler(document, 'click', function() { console.log('hello world'); }); addHandler(window, 'keydown', function() { console.log('key down'); }); |
方法在第一次被呼叫時,會先檢查並決定使用哪種方法去繫結事件處理器,然後原始函式被包含正確操作的新函式覆蓋。最後一步呼叫新的函式(也可以直接return 新的函式),並傳入原始引數。之後的每次呼叫addHandler()都不會再做檢測,因為檢測程式碼已經被新的函式覆蓋。
呼叫延遲載入函式時,第一次總會消耗較長的時間,因為它必須執行檢測接著再呼叫另一個函式完成任務,但隨後呼叫函式會變快,因為不需要再進行檢測。當一個函式在頁面中不會立即呼叫時,延遲載入是最好的選擇。
- 條件預載入
條件預載入會在指令碼載入期間提前檢測,而不會等到函式被呼叫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var addHandler = document.addEventListener ? function(target, eventType, handler) { target.addEventListener(eventType, handler, false); }: function(target, eventType, handler) { target.attachEvent('on' + eventType, handler); }; // 呼叫 addHandler(document, 'click', function() { console.log('hello world'); }); addHandler(window, 'keydown', function() { console.log('key down'); }); |
條件預載入確保所有函式消耗的時間相同,其代價是需要在指令碼載入時就檢測,而不是載入後。預載入適用於一個函式馬上就要被用到,並且在整個頁面的生命週期中頻繁出現的場合。
常見的“不要重複工作”還有做ajax時候的瀏覽器探測,自己可以思考下寫寫程式碼。
使用速度快的部分
- 位運算
用位運算加速大家都應該熟悉並且熟練掌握:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// use &1 instead of %2 var a = 10; if (a & 1) { // use this // ... } if (a % 2) { // not use this // ... } // use << 1 instead of *2 var a = 10; var b = a << 1; // use this var b = a * 2; // not use this var a = 10; var b = a >> 1; // use this var b = a / 2; // not use this |
位運算除了能加速快,還能用在各種演算法和資料結構中,比如狀態壓縮dp,按位dp等等。
- 原生方法
無論你的JavaScript程式碼如何優化,都永遠不會比JavaScript引擎提供的原生方法更快,因為JavaScript原生部分在你寫程式碼前就已經存在在瀏覽器中了,並且都是用底層語言寫的,諸如C++。這意味著這些方法會被編譯成機器碼,成為瀏覽器的一部分。
所以儘量用一些內建的函式或者常量,比如Math物件提供的:
1 2 3 4 5 6 7 8 9 10 11 |
Math.E Math.LN10 Math.LN2 Math.PI Math.SQRT1_2 Math.abs() Math.sin() Math.sqrt() . . . |
另外一個例子是選擇器API,它允許使用CSS選擇器來查詢DOM節點。CSS查詢被JavaScript原生支援並被JQuery發揚光大。JQuery引擎被廣泛認為是最快的CSS查詢引擎,但是它仍然比原生方法慢。原生的querySelector()和querySelectorAll()方法完成任務平均所需時間是基於JavaScript的CSS查詢的10%。所以當原生方法可用時,儘量使用它們。特別是數學運算和DOM操作,用編譯後的程式碼做更多的事情,你的程式碼就會越快。