JavaScript數據訪問性能優化方案

經典計算機科學的一個問題是,數據應當存放在什么地方,以實現最佳的讀寫效率。數據存儲是否得當,關系到代碼運行期間數據被檢索到的速度。在Javascript中,此問題相對簡單,因為數據表現方式只有少量方式可供選擇。在Javascript中,有四種基本的數據訪問位置:

  • Literal values 直接量

    • 直接量僅僅代表自己,而不存儲于特定的位置。

    • Javascript的直接量包括:字符串(strings)、數字(numbers)、布爾值(booleans)、對象(objects)、數組(arrays)、函數(functions)、正則表達式(regular expressions),具有特殊意義的空值(null),以及未定義(undefined)。

  • Variables 變量

    • 開發人員用var關鍵字創建用于存儲數據值。

  • Array items 數組項

    • 具有數字索引,存儲一個Javascript數組對象。

  • Object members 對象成員

    • 具有字符串索引,存儲一個Javascript對象。

每一種數據存儲位置都具有特定的讀寫操作負擔。在大多數情況下,對一個直接量和一個局部變量的數據訪問的性能差異是微不足道的。具體而言,訪問數組項和對象成員的代價要高一些,具體高多少,很大程度上取決于瀏覽器。一般的建議是,如果關心運行速度,那么盡量使用直接量和局部變量,限制數組項和對象成員的使用。為此,有如下幾種模式,用于避免并優化我們的代碼:

Managing Scope 管理作用域

码报开奖结果本期 www.iwqgw.icu 作用域概念是理解Javascript的關鍵,無論是從性能還是功能的角度而言,作用域對Javascript有著巨大影響。要理解運行速度與作用域的關系,首先要理解作用域的工作原理。

Scope Chains and Identifier Resolution 作用域鏈和標識符解析

每一個Javascript函數都被表示為對象,它是一個函數實例。它包含我們編程定義的可訪問屬性,和一系列不能被程序訪問,僅供Javascript引擎使用的內部屬性,其中一個內部屬性是[[Scope]],由ECMA-262標準第三版定義。

內部[[Scope]]屬性包含一個函數被創建的作用域中對象的集合。此集合被稱為函數的作用域鏈,它決定哪些數據可以由函數訪問。此函數中作用域鏈中每個對象被稱為一個可變對象,以“鍵值對”表示。當一個函數創建以后,它的作用域鏈被填充以對象,這些對象代表創建此函數的環境中可訪問的數據:

function add(num1, num2){ 
   var sum = num1 + num2; 
   return sum;
}

當add()函數創建以后,它的作用域鏈中填入了一個單獨可變對象,此全局對象代表了所有全局范圍定義的變量。此全局對象包含諸如窗口、瀏覽器和文檔之類的訪問接口。如下圖所示:(add()函數的作用域鏈,注意這里只畫出全局變量中很少的一部分)

add函數的作用域鏈將會在運行時用到,假設運行了如下代碼:

var total = add(5,10);

運行此add函數時會建立一個內部對象,稱作“運行期上下文”(execution context),一個運行期上下文定義了一個函數運行時的環境。且對于單獨的每次運行而言,每個運行期上下文都是獨立的,多次調用就會產生多此創建。而當函數執行完畢,運行期上下文被銷毀。

一個運行期上下文有自己的作用域鏈,用于解析標識符。當運行期上下文被創建的時,它的作用域被初始化,連同運行函數的作用域鏈[[Scope]]屬性所包含的對象。這些值按照它們出現在函數中的順序,被復制到運行期上下文的作用域鏈中。這項工作一旦執行完畢,一個被稱作“激活對象”的新對象就位運行期上下文創建好了。此激活對象作為函數執行期一個可變對象,包含了訪問所有局部變量,命名參數,參數集合和this的接口。然后,此對象被推入到作用域鏈的最前端。當作用域鏈被銷毀時,激活對象也一同被銷毀。如下所示:(運行add()時的作用域鏈)

在函數運行的過程中,每遇到一個變量,就要進行標識符識別。標識符識別這個過程要決定從哪里獲得數據或者存取數據。此過程搜索運行期上下文的作用域鏈,查找同名的標識符。搜索工作從運行函數的激活目標的作用域前端開始。如果找到了,就使用這個具有指定標識符的變量;如果沒找到,搜索工作將進入作用域鏈的下一個對象,此過程持續運行,直到標識符被找到或者沒有更多可用對象可用于搜索,這種情況視為標識符未定義。正是這種搜索過程影響了性能。

Identifier Resolution Performance 標識符識別性能

標識符識別是耗能的。

在運行期上下文的作用域鏈中,一個標識符所處的位置越深,它的讀寫速度就越慢。所以,函數中局部變量的訪問速度總是最快的,而全局變量通常是最慢的(優化Javascript引擎,如Safari在某些情況下可用改變這種情況)。

請記住,全局變量總是處于運行期上下文作用域鏈的最后一個位置,所以總是最遠才能被訪問的。一個好的經驗法則是:使用局部變量存儲本地范圍之外的變量值,如果它們在函數中的使用多于一次??悸竅旅嫻睦櫻?/p>

function initUI(){
  var bd = document.body,
  links = document.getElementsByTagName("a"), 
  i = 0,
  len = links.length;

  while(i < len){
    update(links[i++]); 
  }    document.getElementById("go-btn").onclick = function(){ 
    start();
  };

  bd.className = "active"; 
}

此函數包含三個對document的引用,而document是一個全局對象。搜索至document,必須遍歷整個作用域鏈,直到最后才能找到它。使用下面的方法減輕重復的全局變量訪問對性能的影響:

function initUI(){    var doc=document,
   bd = doc.body,
  links = doc.getElementsByTagName("a"), 
  i = 0,
  len = links.length;

  while(i < len){
    update(links[i++]); 
  }

     doc.getElementById("go-btn").onclick = function(){ 
    start();
  };

  bd.className = "active"; 
}

用doc代替document更快,因為它是一個局部變量。當然,這個簡單的函數不會顯示出巨大的性能改進,因為數量的原因,不過可以想象一下,如果幾十個全部變量反復被訪問,那么性能改進將顯得多么出色。

Scope Chain Augmentation 改變作用域鏈

一個來說,一個運行期上下文的作用域鏈不會被改變。但是,有兩種表達式可以在運行時臨時改變運行期上下文。第一個是with表達式:

function initUI(){    with (document){ //avoid!
      var bd = body,
      links = getElementsByTagName("a"), 
      i = 0,
      len = links.length;      while(i < len){
        update(links[i++]); 
      }

      getElementById("go-btn").onclick = function(){ 
        start();
      };

      bd.className = "active"; 
  }
}

此重寫版本使用了一個with表達式,避免了多次書寫“document”。這看起來似乎更有效率,實際不然,這里產生了一個性能問題。

當代碼流執行到一個with表達式,運行期上下文的作用域被臨時改變了。一個新的可變對象將被創建,它包含了指定對象(針對這個例題是document對象)的所有屬性。此對象被插入到作用域鏈的最前端。意味著現在函數的所有局部變量都被推入到第二個作用域鏈對象中,所以局部變量的訪問代價變的更高了。

正式因為這個原因,最好不要使用with表達式。這樣會得不償失。正如前面提到的,只要簡單的將document存儲在一個局部變量中,就可以獲得性能上的提升。

另一個能改變運行期上下文的是try-catch語句的字句catch具有同樣的效果。當try塊發生錯誤的時,程序自動轉入catch塊,并將所有局部變量推入第二個作用域鏈對象中,只要catch之塊執行完畢,作用域鏈就會返回到原來的狀態。

try { 
    methodThatMightCauseAnError();
} catch (ex){
    alert(ex.message); //作用域鏈在這里發生改變}

如果使用得當,try-catch表達式是非常有用的語句,所以不建議完全避免。但是一個try-catch語句不應該作為Javascript錯誤解決的辦法,如果你知道一個錯誤會經常發生,那么說明應該修改代碼本身。不是么?

Dynamic Scope 動態作用域

無論是with表達式還是try-catch表達式的子句catch,以及包含()的函數,都被認為是動態作用域。一個動態作用域因代碼運行而生成存在,因此無法通過靜態分析(通過查看代碼)來確定是否存在動態作用域。例如:

function execute(code) { 
  (code);
  function subroutine(){ 
    return window;
}
  var w = subroutine(); // w的值是什么?};

execute()函數看上去像一個動態作用域,因為它使用了()。w變量的值與傳入的code代碼有關。大多數情況下,w將等價于全局的window對象。但是如果傳入的是:

execute("var window = {};");

這種情況下,()在execute()函數中創建了一個局部window變量。所以w將等價于這個局部window變量而不是全局window的那個。所以不運行這段代碼是無法預知最后的具體情況,標識符window的確切含義無法預先知道。

因此,只有在絕對必要時刻才推薦使用動態作用域。

Closure,Scope,and Memory 閉包,作用域,和內存

閉包是Javascript最強大的一個方面,它允許函數訪問局部范圍之外的的數據。為了解與閉包有關的性能問題,考慮下面的例子:

function assignEvents(){
  var id = "xdi9592"; 
  document.getElementById("save-btn").onclick = function(event){
    saveDocument(id); 
  };
}

assignEvents()函數為DOM元素指定了一個事件處理句柄。此事件處理是一個閉包,當函數執行創建時可以訪問其范圍內部的id變量。而這種方法封閉了對id變量的訪問,必須創建一個特定的作用域鏈。

當assignEvents()函數執行時,一個激活對象被創建,并且包含了一些應有的內容,其中包含id變量。它將成為運行期上下文作用域鏈上的第一個對象,全局對象是第二個。當閉包創建的時,[[Scope]]屬性與這些對象一起被初始化,如下圖:

由于閉包的[[Scope]]屬性包含與運行期上下文作用域鏈相同的對象引用,會產生副作用,通常,一個函數的激活對象與運行期上下文一同銷毀。當涉及閉包時,激活對象就無法銷毀了,因為仍然存在于閉包的[[Scope]]屬性中。這意味著腳本中的閉包與非閉包函數相比,需要更多的內存開銷。尤其在IE,使用非本地Javascript對象實現DOM對象,閉包可能導致內存泄露。

當閉包被執行,一個運行期上下文將被創建,它的作用域鏈與[[Scope]]中引用的兩個相同的作用域鏈同時被初始化,然后一個新的激活對象為閉包自身創建。如下圖:

可以看到,id和saveDocument兩個標識符存在于作用域鏈第一個對象之后的位置。這是閉包最主要的性能關注點:你經常訪問一些范圍之外的標識符,每次訪問都將導致一些性能損失。

在腳本中最好小心的使用閉包,內存和運行速度都值得被關注。但是,你可以通過上文談到的,將常用的域外變量存入局部變量中,然后直接訪問局部變量。

Object Members 對象成員

對象成員包括屬性和方法,在Javascript中,二者差別甚微。對象的一個命名成員可以包含任何數據類型。既然函數也是一種對象,那么對象成員除了傳統數據類型外,也可以包含函數。當一個命名成員引用了一個函數時,它被稱作一個“方法”,而一個非函數類型的數據則被稱作“屬性”。

如前所言,對象成員的訪問比直接量和局部變量訪問速度慢,在某些瀏覽器上比訪問數組還慢,這與Javascript中對象的性質有關。

Prototype 原型  

Javascript中的對象是基于原型的,一個對象通過內部屬性綁定到它的原型。Firefox,Safari和Chrome向開發人員開放這一屬性,稱作_proto_。其他瀏覽器不允許腳本訪問這個屬性。任何時候我們創建一個內置類型的實現,如Object或Array,這些實例自動擁有一個Object作為它們的原型。而對象可以有兩種類型的成員:實例成員和原型成員。實例成員直接存在于實例自身而原型成員則從對象繼承??悸僑縵呂櫻?/p>

var book = {
  title: "High Performance JavaScript",
  publisher: "Yahoo! Press" };
alert(book.toString()); //"[object Object]"

此代碼中book有title和publisher兩個實例成員。注意它并沒有定義toString()接口,但這個接口卻被調用且沒有拋出錯誤。toString()函數就是一個book繼承自原型對象的原型成員。下圖表示了它們的關系:

處理對象成員的過程與處理變量十分相似。當book.toString()被調用時,對成員進行名為“toString”的搜索,首先從對象實例開始,若果沒有名為toString的成員,那么就轉向搜索原型對象,在那里發現了toString()方法并執行它。通過這種方法,book可以訪問它的原型所擁有的每個屬性和方法。

我們可以使用hasOwnProperty()函數確定一個對象是否具有特定名稱的實例成員。實例略。

Prototype Chains 原型鏈

對象的原型決定了一個實例的類型。默認情況下,所有對象都是Object的實例,并繼承了所有基本方法。如toString()。我們也可以使用構造器創建另外一種原型。例如:

function Book(title, publisher){ 
  this.title = title;
  this.publisher = publisher;
}

Book.prototype.sayTitle = function(){
  alert(this.title); 
};
  var book1 = new Book("High Performance JavaScript", "Prototype Chains"); 
  var book2 = new Book("JavaScript: The Good Parts", "Prototype Chains"); 
  alert(book1 instanceof Book); //true  alert(book1 instanceof Object); //true  book1.sayTitle(); //"High Performance JavaScript"   alert(book1.toString()); //"[object Object]"

Book構造器用于創建一個新的book實例book1。book1的原型(_proto_)是Book.prototype,Book.prototype的原型是Object。這就創建了一條原型鏈。

注意,book1和book2共享了同一個原型鏈。每個實例擁有自己的title和publisher屬性,其他成員均繼承自原型。而正如你所懷疑的那樣,深入原型鏈越深,搜索的速度就會越慢,特別是IE,每深入原型鏈一層都會增加性能損失。記住,搜索實例成員的過程比訪問直接量和局部變量負擔更重,所以增加遍歷原型鏈的開銷正好放大了這種效果。

Nested Members 嵌套成員

由于對象成員可能包含其他成員。譬如window.location.href(獲取當前頁面的url)這種模式。每遇到一個點號(.),Javascript引擎就要在對象成員上執行一次解析過程,而且成員嵌套越深,訪問速度越慢。location.href總是快于window.location.href,而后者比window.location.href.toString()更快。如果這些屬性不是對象的實例成員,那么成員解析還要在每個點上搜索原型鏈,這將需要更多的時間。

Summary 總結

  • 在Javascript中,數據存儲位置可以對代碼整體性能產生重要影響。有四種數據訪問類型:直接量,變量,數組項,對象成員。對它們我們有不同的性能考慮。

  • 直接量和局部變量的訪問速度非???,而數組項和對象成員需要更長時間。

  • 局部變量比外部變量快,是因為它位于作用域鏈的第一個對象中。變量在作用域鏈中的位置越深,訪問所需的時間就越長。而全局變量總是最慢的,因為它處于作用域鏈的最后一環。

  • 避免使用with表達式,因為它改變了運行期上下文的作用域鏈。而且應當特別小心對待try-catch語句的catch子句,它具有同樣的效果。

  • 嵌套對象成員會造成重大性能影響,盡量少用。

  • 一般而言,我們通過將經常使用的對象成員,數組項,和域外變量存入局部變量中。然后,訪問局部變量的速度會快于那些原始變量。

通過上述策略,可以極大提高那些使用Javascript代碼的網頁應用的實際性能。

來源:碼農網

上一篇: 你可能不知道的 5 個強大的 HTML5 API

下一篇: JavaScript無阻塞加載性能優化方案

分享到: 更多
中国竞彩网首页 双色球开奖号码数据 时时彩新闻 mg电子网址 21点棋牌游戏 21点游戏官方下载 3d精准6码 复式6码二中二多少组 十分快三计划预测 飞艇7码1期技巧 彩票单双大小规律贴吧 分分pk拾实时计划 福彩3d计划必中软件 11选5一胆全托 11选5前一稳赚方法 310v大赢家即时比分