高性能的JavaScript--數據訪問(1)

寫在前面

數據存儲在哪裏,關係到代碼運行期間數據被檢索到的速度。在JavaScript中,此問題相對簡單,由於數據存儲只有少許方式可供選擇。正如其餘語言那樣,數據存儲位置關係到訪問速度。在JavaScript中有四種基本的數據訪問位置:前端

1.Literal values 直接量web

直接量僅僅表明本身,而不存儲於特定位置。 JavaScript的直接量包括:字符串,數字,布爾值,對象,數組,函數,正則表達式,具備特殊意義的空值,以及未定義。正則表達式

2.Variables 變量編程

開發人員使用var關鍵字建立用於存儲數據值。數組

3.Array items 數組項瀏覽器

具備數字索引,存儲一個JavaScript數組對象。函數

4.Object members 對象成員性能

具備字符串索引,存儲一個JavaScript對象。優化

每一種數據存儲位置都具備特定的讀寫操做負擔。大多數狀況下,對一個直接量和一個局部變量數據訪問的性能差別是微不足道的。訪問數組項和對象成員的代價要高一些,具體高多少,很大程度上依賴於瀏覽器。總的來講,直接量和局部變量的訪問速度要快於數組項和對象成員的訪問速度。,若是關心運行速度,那麼儘可能使用直接量和局部變量,限制數組項和對象成員的使用。this

管理做用域

1.做用域鏈和標識符解析

每個JavaScript函數都被表示爲對象。進一步說,它是一個函數實例。函數對象正如其餘對象那樣,擁有你能夠編程訪問的屬性,和一系列不能被程序訪問,僅供JavaScript引擎使用的內部屬性。其中一個內部屬性是[[Scope]],由ECMA-262標準第三版定義。內部[[Scope]]屬性包含一個函數被建立的做用域中對象的集合。此集合被稱爲函數的做用域鏈,它決定哪些數據可由函數訪問。此函數做用域鏈中的每一個對象被稱爲一個可變對象,每一個可變對象都以「鍵值對」的形式存在。當一個函數建立後,它的做用域鏈被填充以對象,這些對象表明建立此函數的環境中可訪問的數據。

當代碼在一個環境中執行時,會建立變量對象的一個做用域鏈(scope chain,不簡稱sc)來保證對執行環境有權訪問的變量和函數的有序訪問。做用域第一個對象始終是當前執行代碼所在環境的變量對象(VO)

例如:

function add(x,y){
            var b=x+y;
            return b;
        }

 當add()函數建立後,它的做用域鏈中填入一個單獨的可變對象,此全局對象表明了全部全局範圍定義的變量。此全局對象包含諸如窗口、瀏覽器和文檔之類的訪問接口。

如圖:

 上圖就是函數Add()的做用域鏈。

Add函數的做用域鏈將在運行時用到。若是運行下面的代碼

var total = add(5, 10);

運行此add函數時創建一個內部對象,稱做「運行期上下文」。一個運行期上下文定義了一個函數運行時的環境。對函數的每次運行而言,每一個運行期上下文都是獨一的。因此每次調用同一個函數就會致使多處調用上下文。當函數執行完畢,運行期上下文就被銷燬
一個運行期上下文有它本身的做用域鏈,用於標示符解析。當運行期上下文被建立時,它的做用域被初始化,連同運行函數的[[Scope]]屬性中所包含的對象。這些值按照它們出如今函數中的順序,被複制到運行期上下文的做用域鏈中。這項工做一旦完成,一個被稱做「激活對象」的新對象就爲運行期上下文建立好了。此激活對象做爲函數執行期的一個可變對象,包含訪問全部局部變量,命名參數,參數集合,和this的接口,而後,這個對象被推入做用域的前端。看成用域鏈被銷燬時,激活對象也一同銷燬。

 

 上圖是運行時Add()函數的做用域鏈。

在函數運行過程當中,沒遇到一個變量,標識符識別過程要決定從哪裏得到或者存儲數據,此過程收索運行期上下文的做用域鏈,查找同名的標識符。搜索工做從運行函數的激活目標之做用域鏈的前端開始。若是找到了,那麼就使用這個具備指定標識符的變量,若是沒有找到,搜索工做將進入做用域鏈的下一個對象。此過程持續進行,直到找到標示符。若是整個過程都沒有找到那麼被認爲是undefined。正是這種搜索過程影響了性能。

2.標識符解析的性能

標示符識別不是免費的,事實上沒有哪一種電腦操做能夠不產生性能開銷。在運行期山下文的做用域鏈中,一個標示符所處的位置越深,它的讀寫速度就越慢。因此,函數中局部變量的訪問速度老是最快的,而全局變量一般是最慢的(優化的JavaScript引擎在某些狀況下能夠改變這種狀況,如谷歌瀏覽器)。全局變量老是處於運行前上下文做用域鏈的最後一個位置。因此老是最遠才能觸及。
用局部變量存儲本地範圍以外的變量值,若是它們在函數中的使用多於一次。考慮下面的例子:

 

複製代碼
function initUI(){
  var 
    bd = document.body,     links = document.getElementsByTagName_r("a"),
     i = 0,     len = links.length;   
  
   while(i < len){     update(links[i++]); }     document.getElementById("go-btn").onclick = function(){ start();    };    bd.className = "active"; }
複製代碼

此函數包括三個對document的引用,document是一個全局對象。搜索此變量,必須遍歷整個做用域鏈,指導最後在全局變量對象中找到它。你能夠經過這種方法減輕重複的全局變量訪問對性能的影響;首先將全局變量的引用放在一個局部變量中,而後使用整個局部變量代替全局變量。 代碼重寫以下:

 

複製代碼
function initUI(){

    var doc = document,
    bd = doc.body,
    links = doc.getElementsByTagName_r("a"),
    i = 0,
   len = links.length;
  
   while(i < len){      update(links[i++]);
   }    doc.getElementById(
"go-btn").onclick = function(){
    start();
   };   bd.className
= "active";
}
複製代碼

 initUI()的新版本首先將document的引用存入局部變量doc中,如今訪問全局變量的次數是1次,而不是3次。用doc替代document更快,由於它是一個局部變量。固然,這個簡單的函數不回顯示出巨大的性能改進,由於數量的緣由。不過若是幾十個全局變量被反覆訪問,那麼性能改進將明顯的多麼出色。

3.改變做用域鏈

 通常來講,一個運行期上下文的做用域鏈不會忽然被改變。可是,有兩種表達式能夠在運行時臨時改變運行期上下文做用域鏈。第一個是with表達式。

with表達式爲全部的對象屬性建立了一個默認操做變量。在其餘語言中,相似的功能一般用來避免書寫一些重複的代碼。initUI()函數能夠重寫成以下樣式:

function  initUI(){
with (document){ //avoid!
var bd = body,
links = getElementsByTagName_r("a"),
i = 0,
len = links.length;
while(i < len){
update(links[i++]);
}
getElementById("go-btn").onclick = function(){
start();
};
bd.className = "active";
}
}

此重寫的initUI()版本使用了一個with表達式,避免屢次書寫document,這看起來彷佛更有效率,而實際上卻產生了一個性能問題。
當代碼流執行到一個with表達式時,運行期上下文的做用域鏈被臨時改變了。一個新的可變對象將被建立,她包含指定對象的全部屬性。此對象被插入到做用域鏈的前端,意味着如今函數的全部局部變量都被推入第二個做用域鏈對象中,因此訪問代價更高了。

經過將document對象傳遞給with表達式,一個新的可變對象容納了document對象的全部屬性,被插入到做用域鏈的前端。這使得訪問document的屬性很是快,可是訪問局部變量的速度卻變慢了,例如bd變量。正由於這個緣由,最好不要使用with表達式。正如前面提到的,只要簡單地將document存儲在一個局部變量中,就能夠得到性能上的提高。

在JavaScript中不僅是with表達式人爲地改變運行期上下文的做用域鏈,try-catch表達式的catch子句具備相同效果。當try塊發生錯誤時,程序流程自動轉入catch塊,並將異常對象推入做用域鏈前端的一個可變對象中。在catch塊中,函數的全部局部變量如今被放在第二個做用域鏈對象中。例如: 

try {
  methodThatMightCauseAnError();
} catch (ex){
  alert(ex.message); //scope chain is augmented here
}

可是,只要catch子句執行完畢,做用域鏈就會返回到原來的狀態。

若是使用得當,try-catch表達式是很是有用的語句,因此不建議徹底避免。若是你計劃使用一個try-catch語句,請確保你瞭解可能發生的錯誤。一個try-catch語句不該該做爲JavaScript錯誤的解決辦法。若是你知道一個錯誤會常常發生,那說明應當修正代碼自己的問題

你能夠經過精縮代碼的辦法最小化catch子句對性能的影響。一個很好的模式是將錯誤交給一個專用函數來處理。如:

try {
  methodThatMightCauseAnError();
} catch (ex){
  handleError(ex); //delegate to handler method
}

handleError()函數是catch子句中運行的惟一代碼。此函數以適當方法自由地處理錯誤,並接收由錯誤產生的異常對象。因爲只有一條語句,沒有局部變量訪問,做用域鏈臨時改變就不會影響代碼的性能。

相關文章
相關標籤/搜索