計算機科學中一個經典的問題是決定如何存儲數據,以便進行快速的讀取和寫入操做。 在代碼執行期間,數據如何存儲將會關係到它們的檢索速度。在Javascript中,因爲只存在少數的操做來進行數據存儲, 這個問題彷佛 變得簡單了。可是,與其餘語言同樣,Javascript中數據的存儲方式將決定它們訪問速度。下面是Javascript中能夠進行數據存儲的四種基本方式: 正則表達式
字面量值(Literal values)
任何僅僅描述自身,且沒有被存儲在一個特定位置上的值。Javascript能夠將字符串,數字,布爾值,對象,數組,函數,正則表達式 以及特殊值null和undefined 做爲字面量。編程
對於上述數據存儲位置而言,它們每一個都有其特定的讀寫花費。雖然實際上的性能差別是強烈依賴於代碼所運行的瀏覽器的。 但在大多數狀況下,從字面量訪問信息與從本地變量訪問信息的性能差別是微不足道的。而數組項和對象成員的訪問則較昂貴。
數組
雖然某些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等。下圖顯示了該關係(注意,圖中的全局對象只顯示了部分屬性值,但實際上它還包含了許多其餘屬性):var total = add(5, 10);函數
執行add函數的時候,將會建立一個稱爲執行上下文(execution context) 的內部對象。執行上下文定義了函數執行的環境. 每一個執行上下文都是惟一的,因此對相同函數的屢次調用將會產生多個執行上下文。當函數執行完成後,執行上下文將會被銷燬。
標識符解析的性能
標識符解析並非不消耗資源的,由於事實上有沒哪項計算操做能夠不產生性能開銷。當在執行上下文的做用域鏈中進行深度查找時,讀寫操做將會變得緩慢。所以,本地變量是函數內部訪問數據最快的方式。而通常狀況下全局變量的訪問則是最慢的(優化過的Javascript引擎會在一些條件下優化該過程)。請記住,全局變量老是處於執行上下文的做用域鏈中最後一個,因此老是產生最多的解析花費。下面2張圖顯示了標識符在做用域鏈上不一樣深度的解析速度.深度爲1則表示本地變量.
讀操做:
寫操做:
對全部瀏覽器而言,總的趨勢是標識符在做用域鏈中的位置越深,它的讀寫操做將會變得更慢。雖然一些優化過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"; } }
try { methodThatMightCauseAnError(); } catch (ex){ alert(ex.message); // 此處做用域鏈已被擴大 }
須要注意的是,只要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最強大的方面之一,它容許一個函數訪問其本地做用域以外的數據。閉包的使用已由Douglas Crockford 所寫的文章普及,而且在大多數複雜的Web程序中無處不在. 不過,閉包的使用也關聯了一些性能影響. 爲了理解閉包的性能問題,請考慮下面的代碼:
function assignEvents(){ var id = "xdi9592"; document.getElementById("save-btn").onclick = function(event){ saveDocument(id); }; }
assignEvents 函數爲DOM元素分配了事件處理器, 這個事件處理器既是一個閉包,由於它是在assignEvents執行時建立的,但可以在其包含範圍內訪問到外部的id變量.爲了使這個閉包訪問到id變量, Javascript引擎必須建立一個特殊的做用域鏈。
二.對象成員(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對象繼承的原型成員。下圖顯示了此關係: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繼承了該原型鏈上全部的方法。下圖顯示了這種關係:
嵌套成員
由於對象的成員能夠包含其餘成員,因此常常能夠見到諸如 window.location.href 這類的Javascript代碼。這些嵌套成員致使Javascript引擎在每遇到一個點號(.)後都會進行成員解析處理。下圖顯示了對象成員深度和訪問時間之間的關係:
結果並不令人吃驚,成員的嵌套數越多,其數據訪問速度將越慢。所以 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次。由於兩次讀取的都是相同的屬性值,因此值讀取一次並將其保存爲本地變量是有意義的。在後面對本地變量的訪問操做將會快不少。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個地方對數據進行訪問: 字面量, 變量,數組項 以及對象成員。這些位置均有不一樣的性能考慮。
經過使用這些策略,你能夠極大地加強Web應用程序的實際性能。對於那些須要大量JavaScript代碼的應用而言,性能提高將更加可觀。