編寫高效JavaScript代碼

jopen 9年前發布 | 20K 次閱讀 JavaScript開發 JavaScript

本文是閱讀Writing Fast, Memory-Efficient JavaScript后的總結和筆記,不是嚴格意義上的翻譯,如果有時間,推薦閱讀原文。

原則

不要做任何優化除非的確需要優化

任何的性能優化都必須以測量數據為基礎,如果你懷疑代碼存在性能問題,首先通過測試來驗證你的想法。

性能優化三問

  1. 我還能做哪些工作從而讓代碼變得更有效率?
  2. 流行的JavaScript引擎通常會做哪些優化工作?
  3. 哪些優化是JavaScript引擎不能做的,垃圾回收器是否能清理我們期望清理的?

對JavaScript引擎的深入了解有助于我們編寫高效的JavaScript代碼,但不要只針對某一特定引擎做性能優化。

V8的幾個關鍵概念

  • 基礎編譯器,解析你的JavaScript代碼并生成Native Machine Code執行,而不是執行字節碼或是直接對JavaScript解釋執行。
  • 在V8中,對象以object model的形式存在。對象在JavaScript中是以關聯數組的形式存在,但V8采用的是Hidden Classes——一種對查找操作進行了優化的內部類型系統。
  • 運行時探查器監視運行中的系統,并識別出Hot functions,即是耗用了較長時間的代碼
  • 優化編譯器重新編譯并優化由運行時探查器識別出來的Hot代碼
  • V8支持反優化,優化編譯器能夠發現過度優化的代碼并對其進行處理
  • V8有自己的垃圾回收器

垃圾回收

垃圾回收是一種內存管理機制,垃圾回收器會嘗試清理掉不再被使用的對象,并回收內存。

  • 在絕大多數情況下都不需要手動解除引用
  • 你不可能強制垃圾回收器工作

刪除引用的誤區

盡可能不要使用delete,在下面的列子中,delete 帶來的弊遠遠大于利

var o = { x: 1};
delete o.x;

主要的原因是為了避免在運行時修改Hot對象的結構,因為固定的對象結構有助于JavaScript引擎對其進行優化,而delete會導致對象結構改變

另外一個誤區是將對象設置為null,將對象設置為null不會刪除對象,只是將對象指向null,這要好過采用delete,但通常也是不必要的。

全局變量在整個頁面生命周期中都是不會被清理的,無論頁面打開多長時間,除非是刷新頁面或者轉到其他頁面。局部變量(Function-scoped)在方法執行完后,且沒有被引用的情況下將會被回收。

所以,請盡量避免使用全局變量

經驗法則

為了使垃圾回收器盡早回收對象,不要保持不必要的對象引用

  • 比手動解除引用更好的方法是將對象放在合適的變量域中,能用局部變量就不要采用全局變量
  • 當事件監聽不再需要時,請解除事件綁定,尤其是當事件綁定的DOM對象被刪除時
  • 如果有使用本地緩存,請確保有合適的清理機制(比如時效機制),從而避免大量無用的數據存儲。

方法 (Function)

如前面所說,垃圾回收器只有在對象不可觸及的時候才會對其做回收處理。考慮如下兩個列子

function foo(){
  var bar = new LargeObject()
  bar.someCall();
}
function foo(){
  var bar = new LargeObject()
  bar.someCall();
  return bar;
}
var b = foo();

在第一個例子中,bar指向的對象會在方法執行完畢后處于可回收狀態;在第二個列子中,由于在局部變量外維護了一個全局變量bbar指向的對象無法被回收。

閉包 (Closures)

當一個方法返回一個內部方法時,被返回的內部方法能訪問外部方法的局部變量域即使外部方法已經執行完畢。

function sum(x){
  function sumIt(y){
      return x + y;
  }
}
var sumA = sum(4);
var sumB = sumA(3);

在上面的例子中,sumIt方法即使處于sum的局部變量域中,但由于存在一個sumA全局變量,在sum執行完畢后也無法被回收。

再看兩個例子

var a = function(){
  var largeObj = new LargeObject();
  return function(){
      return largeObj;
  }
}();
var a = function(){
  var smallObj = new SmallObj();
  var largeObj = new LargeObj();
  return function(n){
      return smallObj;
  }
}();

第一個例子中,largeObj可以通過變量a訪問,因此不可被回收;在第二個例子中,方法一旦執行完畢,largeObj就無法被訪問了,因此處于可回收狀態。

定時器 (Timer)

setTimeout / setInterval 方法中的引用,只有當定時器執行完成后才能被回收

V8優化小貼士

  • 某些行為會導致V8停止優化工作,比如try-catch, 為了能弄清哪些代碼可以被優化,哪些不能,你可以在V8命令行工具中使用—trace-opt file.js獲得有用的信息。

  • 如果你在意速度,那就盡可能保證你的方法是”單形的(monomophic)"

    不要做類似如下的嘗試

    function add(x, y){
      return x+y;
    }
    add(1,2);
    add('a','b');
    add(my_custom_object, undefined);
  • 不要加載沒有被初始化或者已被刪除的元素,盡管在輸出上沒有不同,但卻會讓代碼變得更慢

  • 不要寫大方法,因為他們很難被優化。

對象還是數組, 如何選擇?

  • 如果存儲的是大量數字,或者是相同類型的對象列表,采用數組;

  • 如果根據語義你需要一個有很多屬性的對象,那就采用對象,在內存利用方面這會很高效,同時也很快;

  • 無論是數組還是對象,采用整數索引都最快的。

    var sum = 0;
    for (var x=0; x<arr.length; ++x){
      sum + = arr[x].payload;
    }
    var sum = 0;
    for(var x in obj){
      sum += obj[x].payload;
    }
    var sum = 0;
    for(var x=0; x<1000,++x){
      sum += obj[x].payload;
    }
    var sum = 0;
    var keys = Objects.keys(obj);
    for(var x=0; x<keys.length;++x){
      sum += obj[keys[x]].payload;
    }

    在上面的四段代碼中,第一段和第三段速度比第二段和第四段要快很多。其中,第一段代碼執行最快,最后一段代碼執行最慢。

  • 相比數組中的元素,對象的屬性在結構上相對復雜。在引擎層面,內存中越是簡單的結構越容易被優化,尤其是包含數字的數組。因此,如果你需要向量,采用數組而不是一個包含x, y, z屬性的對象會有更優的性能表現。

在JavaScript中,數組和對象最重要的不同是數組的length屬性,如果你能自己維護這個值,對象在V8中也能跑出數組的速度。

使用對象的性能小貼士

  • 使用構造函數創建對象,因為所有采用同一構造函數創建的對象都具有相同的hidden class,另外,采用構造函數創建對象也比Object.create()這種方法略塊。
  • 盡管JavaScript沒有限制類型數量和對象的復雜度,但長原型鏈和大量的對象屬性會對性能造成損害。因此盡可能保持較短的原型鏈和較少的對象屬性。

對象的拷貝

for..in循環是性能殺手,通過該方法遍歷對象屬性進行拷貝非常低效。拷貝大對象始終會降低性能,盡可能不要干這樣的事情,當然大對象的存在本身就是一個錯誤。如果你確實需要在性能攸關的代碼中拷貝對象,可以采用如下的方式。

function clone(original){
  this.foo = original.foo;
  this.bar = original.bar;
}
var copy = new clone(original);

緩存采用模塊化編程(Module Pattern)的方法

// prototypal

Klass1 = function(){}
Klass1.prototype.foo = function(){
  log('foo');
}
Klass1.prototype.bar = function(){
  log('bar');
}
// Module pattern
Klass2 = function(){
  var foo = function(){
    log('foo');
  }
  var bar = function(){
    log('bar');
  }
  return {foo:foo,bar:bar}
}
// Module pattern with cached functions
var fooFn = function(){
  log('foo');
}
var barFn = function(){
  log('bar')
}
Klass3 = function(){
  return{
    foo: fooFn,
    bar: barFn
  }
}

執行速度從快到慢依次是

Module Pattern with Cached functions → prototypal → Module pattern

使用數組的性能小貼士

  • 不要刪除數組元素,當數組的Key set分布分散后,V8會將存儲方式轉為字典,導致速度變慢。
  • 數組常量更高效,尤其是小數組和中等大小的數組。
var a = [1, 2, 3, 4]
var a = [];
for(var i=1, i<=4; i++){
  a.push(i);
}

不要采用第二段代碼中的方法初始化數組。

  • 不要在數組中存儲不同類型的元素
  • V8中,稀疏數組( Sparse Arrays)是被當成字典對待的,因此相比密集數組(Full Arrays),執行速度更慢
  • 與緊湊的數組相比,滿身是洞的數組執行更慢,即使是從密集數組中刪除一個元素,也會帶來性能上的損失。
  • 不要預先給大數組(大于64k)分配一個最大值
var arr = [];
for(var i = 0; i< 1000000; i++){
  arr[i] = 1;
}
var arr = new Array(1000000);
for(var i=0; i<1000000; i++){
  arr[i]=i;
}

需要注意的是,不同引擎在這一點上有不同,在Nitro(Safari)中,第二段代碼跑得更快,但在V8(Chrome), SpiderMonkey(Firefox)中,第一段更快。

來自:http://www.jianshu.com/p/60ae173a4192

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!