JavaScript 數據訪問(通譯自High Performance Javascript 第二章) [轉]

JavaScript 數據訪問(通譯自High Performance Javascript 第二章)

提問者:lilei335260(ID:160310) | 懸賞 0.0 希賽幣 | 回答數:12 | 關注度:32 | 提問時間:2014-05-03
 
JavaScript 數據訪問(翻譯自High Performance Javascript 第二章)

  計算機科學中一個經典的問題是決定如何存儲數據,以便進行快速的讀取和寫入操做。 在代碼執行期間,數據如何存儲將會關係到它們的檢索速度。在Javascript中,因爲只存在少數的操做來進行數據存儲, 這個問題彷佛 變得簡單了。可是,與其餘語言同樣,Javascript中數據的存儲方式將決定它們訪問速度。下面是Javascript中能夠進行數據存儲的四種基本方式: 正則表達式

  •   字面量值(Literal values)
      任何僅僅描述自身,且沒有被存儲在一個特定位置上的值。Javascript能夠將字符串,數字,布爾值,對象,數組,函數,正則表達式 以及特殊值null和undefined 做爲字面量。編程

  • 變量
      任何開發者使用var關鍵字定義的數據存儲位置.
  • 數組項
      Javascript數組中使用數字進行索引的位置
  • 對象成員
      Javascript對象中使用字符串進行索引的位置.

  對於上述數據存儲位置而言,它們每一個都有其特定的讀寫花費。雖然實際上的性能差別是強烈依賴於代碼所運行的瀏覽器的。 但在大多數狀況下,從字面量訪問信息與從本地變量訪問信息的性能差別是微不足道的。而數組項和對象成員的訪問則較昂貴。

數組

  670x474

  雖然某些JS引擎對數組項訪問進行了優化,使其能變得更快。但即便如此,一般的建議是儘量的使用字面值和本地變量,並限制數組項和對象成員的使用. 爲了達到這個目的,有以下幾個模式可用來查找和避免問題,並優化你的代碼.

一.管理做用域(Manaing Scope)
  在Javascript中, 做用域(Scope)是一個關鍵的概念。其不只是從性能的角度,並且也從函數的角度解釋了各類問題。做用域在Javascript中產生了諸多影響,從肯定函數能夠訪問那些變量到this上值的分配. 在使用Javascript 做用域的時候,也有一些性能上的考慮。但爲了理解其如何關聯到速度上,首先須要理解做用域是如何工做的。 

瀏覽器

  •   做用域鏈(Scope Chain)與標識符解析
      Javascript中的每一個函數都被表示成一個對象--更具體的說,是做爲函數的實例. 就像其餘對象同樣,函數對象也能夠包含屬性(properties),這些屬性包括能夠編程訪問的常規屬性以及一系列Javascript引擎所使用到的內部屬性。內部屬性沒法經過代碼來訪問。其中一個內部屬性是在ECMA-262,第三版規範中定義的 [ [Scope] ] 屬性. 
    [ [Scope] ]內部屬性包含了函數被建立時表示其所在做用域的對象集合(The internal [[Scope]] property contains a collection of objects representing the scope in which the function was created)。該集合被稱爲函數的做用域鏈,它決定了一個函數所能訪問到的數據。函數做用域鏈中的每一個對象都稱爲可變對象. 每一個可變對象包含一些鍵值對(Key-Value Pairs). 當一個函數被建立時,它的做用域鏈會填充一些在其建立環境內能夠訪問到的數據對象。例如,請考慮下面的全局函數:緩存

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

      當 add() 函數被建立時,他的做用域鏈將會填充一個單獨的可變對象: 即全局範圍內包含全部值的全局對象(global object).該全局對象包含了諸如window, navigator 和document等。下圖顯示了該關係(注意,圖中的全局對象只顯示了部分屬性值,但實際上它還包含了許多其餘屬性):

    563x169
    add 函數的做用域鏈將會在其執行時用到。例如假設運行如下代碼:

      var total = add(5, 10);函數

      執行add函數的時候,將會建立一個稱爲執行上下文(execution context) 的內部對象。執行上下文定義了函數執行的環境. 每一個執行上下文都是惟一的,因此對相同函數的屢次調用將會產生多個執行上下文。當函數執行完成後,執行上下文將會被銷燬。
      一個執行上下文自身也包含了做用域鏈,該做用域鏈將用來進行標識符解析。當執行上下文建立時,首先會把其執行函數的[ [Scope] ] 屬性中的對象複製到自身的做用域鏈中。該複製過程將會以對象在 [ [Scope] ]屬性中出現的位置依次進行。當該過程完畢後,將會爲執行上下文建立一個稱爲激活對象(activation object)的新對象. 該激活對象包含了全部的本地變量,命名參數, 參數集合(arguments)以及this。接着,激活對象將會被推入做用域鏈的最頂端,做爲該次執行中的可變對象。當執行上下文被銷燬時,該激活對象也同時銷燬。下圖顯示了前面代碼中的執行上下文和做用域鏈.

    645x407
      在函數執行時,每遇到一個變量,將會產生一個標識符解析的過程,該過程將決定數據檢索和存儲的位置。在這個過程當中,將會在執行上下文的做用域鏈中查找一個與變量名稱相同的標識符. 查找將會從做用域鏈的頂端開始(即激活對象),依次遍歷做用域鏈。當找到相同名稱的標識符時,將使用該標識符。而當遍歷完整個做用域鏈後均沒有找到標識符時,標識符將會被就看作是未定義的(undefined). 函數執行時,每一個標識符的查找都將經歷上面的過程.之前面的例子來講,add函數中的sum, num1 和 num2 將會產生這一查找過程。而正是這個搜索過程影響了性能。
      注意在做用域鏈中不一樣的部分可能會存在兩個名稱相同的變量。此時,標識符解析將會以首先找到的對象爲準。然後面部分中的對象將會被遮蔽(shadow).

     

  •   標識符解析的性能
      標識符解析並非不消耗資源的,由於事實上有沒哪項計算操做能夠不產生性能開銷。當在執行上下文的做用域鏈中進行深度查找時,讀寫操做將會變得緩慢。所以,本地變量是函數內部訪問數據最快的方式。而通常狀況下全局變量的訪問則是最慢的(優化過的Javascript引擎會在一些條件下優化該過程)。請記住,全局變量老是處於執行上下文的做用域鏈中最後一個,因此老是產生最多的解析花費。下面2張圖顯示了標識符在做用域鏈上不一樣深度的解析速度.深度爲1則表示本地變量.

    讀操做:
      596x536

    寫操做:

      595x533
      
      對全部瀏覽器而言,總的趨勢是標識符在做用域鏈中的位置越深,它的讀寫操做將會變得更慢。雖然一些優化過Javascript引擎的瀏覽器,例如Chrome 和 Safari 4 在訪問外部做用域(out-of-scope)中的標識符時並無這種性能損耗,然而 IE, Safari 3.2 以及其餘瀏覽器則產生了較大的影響。值得一提的時,一些早期的瀏覽器,例如IE 6 以及 Firefox 2 將會產生很是大的性能差距.
      有了這些信息,咱們最好儘量的使用本地變量來在未優化JS引擎的瀏覽器中加強性能。一個好的經驗是當外部做用域的值在函數中使用了不止一次時,老是將其保存爲本地變量。請考慮下面的例子:性能

      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"; }優化

      該函數包含了3個對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"; }this

      修改事後的initUI() 函數會先使用本地對象來保存document 的引用。而不是原來那樣進行3次全局對象的訪問。固然,在這個簡單的函數中這麼作可能並不會顯示出巨大的性能加強,但能夠想象,在一個大量編碼的函數中許多全局變量被重複訪問的狀況下,該方式將會帶來可觀的性能加強。

     

  •   做用域鏈擴大(Scope Chain Augmentation)
      通常來講,一個執行上下文的做用域鏈並不會改變。可是,有2個語句能夠在函數執行時臨時地擴大執行上下文的做用域鏈。第一個語句是 with.
      With 語句能夠用來對指定對象的全部屬性建立一個默認操做變量。該特性是模仿其餘語言中類似的特性。其本意是避免重複編寫相同的代碼。前面的initUI函數能夠被改寫爲下面這樣:

      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"; } }


      這個修改後的initUI函數使用了with語句來避免處處使用document引用。雖然這看起來彷佛更有效率,但它實際上卻產生了一個性能問題.
      當代碼執行進入with語句時,執行上下文的做用域鏈將會臨時地擴大。這將產生一個新對象,該新對象包含了with語句所指定對象的全部屬性值。接着該新對象將會被推入做用域鏈的頂端.這也意味着此時函數中全部的本地變量變成了做用域鏈中的次級節點. 所以帶來了額外的訪問開銷.

    702x603

      當將document對象傳遞進with語句時,一個新的包含了document全部屬性的可變對象將會被推入做用域鏈的頂端。這會使對document的訪問變得更快可是卻下降了對本地變量的訪問。處於這個理由,最好避免使用with語句,而是使用前面所述的只是簡單地將document存儲在一個本地變量中,並以此得到性能上的提高.

      With語句並非Javascript中惟一一個能夠擴大執行上下文的做用域鏈的方法。Try-catch語句中的catch 子句也會產生相同的效果。當try塊中出現一個錯誤時,執行將會自動地轉入catch塊且異常對象將會被推入一個可變對象,並放在做用域鏈的頂端。所以在catch塊中,函數中全部的本地變量將會變成次級做用域對象。

      try { methodThatMightCauseAnError(); } catch (ex){ alert(ex.message); // 此處做用域鏈已被擴大 }

      須要注意的是,只要catch子句結束執行,做用域鏈將會回到前面的狀態。
    在使用得當的狀況下,try-catch 語句是很是有用的。因此徹底避免使用try-catch 是沒有意義的。若是你正準備使用try-catch語句,請確保你理解了出錯的可能性。Try-catch 語句不該該做爲Javascript錯誤的解決方案。若是你已經知道了一個錯誤常常出現,那麼說明代碼自己出現了問題,而且應該被修正。
      你能夠經過只在catch 子句中執行少許必要的代碼來最小化性能衝擊。一個好的模式是在catch子句中使用一個委託方法來處理錯誤. 以下面的例子:

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

      此處的catch子句中只使用一個handleError()方法來處理. 而handleError能夠自由地選擇適宜的處理方式。由於此時只包含了單條語句執行而且沒有本地變量地訪問,臨時的做用域鏈擴大並無影響代碼的性能。

     

  •   動態做用域
      with語句和try-catch 中的catch 子句,以及一個包含evel()調用的函數, 均被認爲是動態做用域。動態做用域只存在於代碼執行期間,所以並不能簡單地經過靜態分析(查看代碼結構)來決定. 例如:

      function execute(code) { eval(code); function subroutine(){ return window; } var w = subroutine(); //w 如何取值 };

    這裏execute()函數使用到了evel()函數, 所以它是一個動態做用域. 此處w的值是否改變是基於參數code的值。在大多數狀況下,w將等於全局對象window, 但請考慮下面的代碼:

      execute("var window = {};")

      在這種狀況下,evel() 在execute() 內部建立了一個名爲window的本地變量.因此 w 也最終等於該本地變量而非全局的window. 這種狀況在代碼執行以前是沒法知曉,也意味着標識符window的值沒法預先決定.
      某些優化後的Javascript引擎,例如Safari’s Nitro 試圖經過分析代碼中給定時間內那些變量能夠被訪問來加速標識符解析。這些引擎嘗試使用索引標識符的方式來加速解析過程,並避免採用傳統的做用域鏈查找. 可是,當出現一個動態做用域時,這些優化技術將變得無效,Javascript引擎不得不切換回較慢的哈希查找方式來進行標識符解析。這更相似於傳統的做用域鏈查找。
    處於這個理由,只應該在絕對必要的狀況下使用動態做用域。

     

  •   閉包,做用域與內存
      閉包是Javascript最強大的方面之一,它容許一個函數訪問其本地做用域以外的數據。閉包的使用已由Douglas Crockford 所寫的文章普及,而且在大多數複雜的Web程序中無處不在. 不過,閉包的使用也關聯了一些性能影響. 爲了理解閉包的性能問題,請考慮下面的代碼:

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

    assignEvents 函數爲DOM元素分配了事件處理器, 這個事件處理器既是一個閉包,由於它是在assignEvents執行時建立的,但可以在其包含範圍內訪問到外部的id變量.爲了使這個閉包訪問到id變量, Javascript引擎必須建立一個特殊的做用域鏈。
    當assignEvents執行時,將會建立一個只包含ID變量的激活對象。該激活對象將會成爲執行上下文的做用域鏈中的第一個對象,全局對象後處在第二位。當閉包被建立時,它的 [ [Scope] ]屬性將被初始化賦值爲與外部執行上下文相同的對象.以下圖所示:

    693x408
      由於閉包的 [ [Scope]]屬性包含了其外部環境中對象引用,這也產生了一個負面影響。典型狀況下,一個函數的激活對象在其執行上下文銷燬時隨之銷燬,但當有閉包存在時,激活對象並沒及時銷燬,由於此時閉包的[ [Scope] ]屬性仍然保存這一個對該對象的引用.這也意味着,使用閉包的函數相對於非閉包函數將會帶來更多的內存開銷。對於大型Web應用而言,這將會是一個問題。 對於IE而言更是如此。 IE將DOM對象做爲非本地Javascript對象來實現,於是使用閉包時將會可能產生內存泄露。
      當閉包被執行時,將會建立一個執行上下文. 該執行上下文的做用域鏈將會被初始化爲其[ [Scope] ]屬性中所引用的對象(此處爲2個)。且一個針對閉包自身的激活對象將會被建立. 以下圖所示:

    693x517
      請注意,在閉包中所使用到的id和saveDocument等標識符,此時均處於做用域鏈的尾部,這也是閉包性能問題的主要關注點: 你常常訪問一個最遠距離的標識符,並所以帶來了性能損失。
      在編寫你本身的腳本時,最好時刻關注閉包的使用,由於他們可能帶來內存泄露和執行速度的問題。可是,你能夠經過本文前面所討論的,將外部做用域中經常使用到的變量存儲爲本地變量來下降對執行速度的影響。

     

  二.對象成員(Object Members)
  大多數Javascript腳本都使用了面向對象的風格。不管是自定義的對象仍是像DOM或BOW(Browser Object Model)中的嵌入對象。在這些狀況下,都會產生許多針對對象成員的訪問操做。
  在此處,對象成員既指屬性,也能夠指方法. 在Javascript中,對象的屬性和方法之間並無多大的區別。一個對象的命名成員能夠包含任何類型的數據。由於函數是被表示爲對象的關係,對象成員也能夠包含一個函數,就像包含傳統數據類型那樣。當一個命名成員引用了一個函數時,該成員被稱爲方法. 而當引用的是非函數的數據類型時,該成員被稱爲屬性。
  本文前面曾經討論過,對象成員的訪問是慢於字面量訪問和變量訪問的。而且在某些瀏覽器中,它也慢於數組項的訪問。爲了理解爲何會發生這種狀況,首先須要理解Javascript中對象的本質。

  •   原型(Prototypes)
      Javascript中的對象是基於原型的。原型是一個做爲其餘對象基礎的對象, 它定義並實現了新對象必須擁有的成員。這與傳統面向對象編程中」類」的概念是徹底不一樣的。OOP中的」類」定義的是建立新對象的處理過程。
      對於一個給定的類型,其原型對象被全部的實例所共享,所以全部的實例都可以對原型對象中的成員進行訪問。
      對象是使用一個內部屬性來關聯到其原型的。在Firefox, Safari 以及 Chrome中,這個屬性被開放爲 [_proto_] 屬性,並容許開發者訪問。但其餘瀏覽器則不容許腳本訪問該屬性。
      因而可知,一個對象自身所包含的成員能夠分爲兩類: 實例成員(也稱爲」全部(own)」成員) 和原型成員。實例成員直接存在於對象自身,但原型成員則是繼承自原型對象.請考慮下面的例子:

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

    在上面的代碼中,book對象擁有兩個實例成員: title 與 publisher. 請注意這裏並無定義toString()方法,但在toString()方法調用時並無出現錯誤。由於此處的toString()方法是book對象繼承的原型成員。下圖顯示了此關係: 

      527x260
    對象成員的解析過程很是類似於變量的解析。當book.toString()被調用時,將會首先從對象自身搜索一個名爲」toString」的成員,當沒有找到時,將會繼續搜索對象的原型對象.而在原型對象中,將會找到並執行這個toString方法。經過這種方式,book對象能夠訪問其原型對象上的每一個屬性或方法。
     你可使用hasOwnProperty 方法來肯定一個對象是否擁有所給名稱的實例成員。當須要肯定對象是否能夠訪問所給名稱的成員時(不管是實例成員仍是原型成員),可使用 in 操做。 以下所示:

      var book = { title: "High Performance JavaScript", publisher: "Yahoo! Press" }; alert(book.hasOwnProperty("title")); //true alert(book.hasOwnProperty("toString")); //false alert("title" in book); //true alert("toString" in book); //true

    在上面的代碼中,由於title 是對象的實例成員,因此當傳入」title」給hasOwnProperty方法時,該方法返回true. 而由於toString是一個原造成員,因此傳入 「toString」 時返回false. 但當對兩者進行in操做時,均返回true. 由於in 操做並不區分實例成員和原型成員。

     

  •   原型鏈
      對象的原型決定了對象實例的類型。默認狀況下,全部的對象均是Object的實例,並所以繼承了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", "Yahoo! Press"); var book2 = new Book("JavaScript: The Good Parts", "Yahoo! Press"); alert(book1 instanceof Book); //true alert(book1 instanceof Object); //true book1.sayTitle(); //"High Performance JavaScript" alert(book1.toString()); //"[object Object]"

    Book構造式用來建立一個新的Book實例。此時book1 實例的原型(_proto_)爲Book.prototype.而Book.prototype的原型則是Object. 該過程建立了一個原型鏈.使得book1和book2繼承了該原型鏈上全部的方法。下圖顯示了這種關係:

    721x344
      注意此時Book的2個實例均是共享相同的原型鏈。每一個實例擁有本身的title和publisher屬性,但其餘全部的屬性都是繼承而來的。
      如今當book1.toString()被調用時,搜索過程必須深刻到原型鏈的最底層(Object)處來解析toString. 如你所料,成員屬性在原型鏈中的位置越深,它的查找速度將會越慢。下圖顯示了原型鏈中成員深度與訪問時間的關係:

    588x458
      雖然優化過Javascript引擎的瀏覽器在執行時表現很好,但較老的瀏覽器,例如IE和Firefox 3.5 將會隨着原型鏈查找深度的增長而帶來性能損耗。請注意,原型鏈中成員的查找過程將會比從字面量或變量中訪問數據更昂貴。而對原型鏈進行遍歷將會擴大這種差距.

     

  •   嵌套成員
      由於對象的成員能夠包含其餘成員,因此常常能夠見到諸如 window.location.href 這類的Javascript代碼。這些嵌套成員致使Javascript引擎在每遇到一個點號(.)後都會進行成員解析處理。下圖顯示了對象成員深度和訪問時間之間的關係:

    582x505
      結果並不令人吃驚,成員的嵌套數越多,其數據訪問速度將越慢。所以 location.href 將會快於window.location.href, 類似地,window.location.href 將快於 window.location.href.toString(). 若是這些屬性不存在於對象的實例中,成員的解析還將會持續到對象的原型鏈上。

  •   緩存對象成員的值
      因爲對象成員關聯了以上性能問題,你應該在可能的狀況下避免使用它們。更精確地說,你應該只在必要的狀況下使用對象成員。例如,在單個函數中是沒有理由從成員變量中進行多於一次的訪問操做的。

      function hasEitherClass(element, className1, className2){ return element.className == className1 || element.className == className2; }

     在上面的代碼中,對element.className 進行了2次訪問。明顯地,在這段代碼的執行過程當中,className屬性的值將不會改變,但此處卻產生了2次成員查找的性能開銷。你能夠經過將屬性值保存爲本地變量來減小一次查找過程。

      function hasEitherClass(element, className1, className2){ var currentClassName = element.className; return currentClassName == className1 || currentClassName == className2; }

    上面修改後的函數將對成員的查找減小到了1次。由於兩次讀取的都是相同的屬性值,因此值讀取一次並將其保存爲本地變量是有意義的。在後面對本地變量的訪問操做將會快不少。
      通常來講,若是你在一個函數中屢次訪問了對象的屬性,最好將該屬性保存爲本地變量。在隨後的處理中使用這個本地變量來代替對屬性的訪問。以此來避免查找過程所帶來的性能開銷。這在處理嵌套對象成員時尤爲重要,它將會對執行速度產生可觀的影響。
    Javascript命名空間,例如YUI中所使用的技術,是常常進行嵌套屬性訪問的來源,以下所示:

      function toggle(element){ if (YAHOO.util.Dom.hasClass(element, "selected")){ YAHOO.util.Dom.removeClass(element, "selected"); return false; } else { YAHOO.util.Dom.addClass(element, "selected"); return true; } }

    上面的代碼重複了三次 YAHOO.util.Dom 的使用,其以此來獲取對不一樣方法的訪問。對於每一個方法,該操做都產生了3此成員查找。那麼總共產生了9次成員查找處理。這使得上述代碼效率很低。一個更好的方式是將YAHOO.util.Dom 保存爲本地變量,並在以後的操做中訪問該本地變量。

      function toggle(element){ var Dom = YAHOO.util.Dom; if (Dom.hasClass(element, "selected")){ Dom.removeClass(element, "selected"); return false; } else { Dom.addClass(element, "selected"); return true; } }

    上面修改後的代碼將對成員的查找處理從9次下降到了5次。除了在所需值肯可能變化的狀況下,你不該該在單個函數中進行多於一次的對象成員查找。

     

  三.總結
  在Javascript中如何存儲和訪問數據將會對代碼的整體性能產生重要的影響。能夠從如下4個地方對數據進行訪問: 字面量, 變量,數組項 以及對象成員。這些位置均有不一樣的性能考慮。

  • 訪問字面量以及本地變量的速度是很是快的,數組項和對象成員的訪問則較慢。
  • 本地變量的訪問將快於外部範圍內的變量。由於本地變量存在於函數做用域鏈中的第一個可變對象內(激活對象). 變量在做用域鏈中的位置越深。其訪問的時間也就越長。由於全局變量處在做用域鏈中的最後位置,因此對它的訪問老是最慢的。
  • 避免使用with語句,由於他們擴大了執行上下文的做用域鏈。同時,也須要注意try-catch語句中的catch子句,它也會產生相同的效果。
  • 嵌套對象成員遭受着重大的性能影響,應當最小化它的使用.
  • 屬性或方法在原型鏈中的位置越深,訪問它們的速度亦越慢。
  • 通常來講,你能夠經過將常用的對象成員,數組項以及外部變量保存爲本地變量來加強代碼的性能。針對本地變量的訪問將快於原始的訪問方式。

  經過使用這些策略,你能夠極大地加強Web應用程序的實際性能。對於那些須要大量JavaScript代碼的應用而言,性能提高將更加可觀。

相關文章
相關標籤/搜索