經典計算機科學的一個問題是,數據應當存放在什麼地方,以實現最佳的讀寫效率。數據存儲是否得當,關係到代碼運行期間數據被檢索到的速度。在Javascript中,此問題相對簡單,由於數據表現方式只有少許方式可供選擇。在Javascript中,有四種基本的數據訪問位置:前端
1.Literal values 直接量web
直接量僅僅表明本身,而不存儲於特定的位置。正則表達式
Javascript的直接量包括:字符串(strings)、數字(numbers)、布爾值(booleans)、對象(objects)、數組(arrays)、函數(functions)、正則表達式(regular expressions),具備特殊意義的空值(null),以及未定義(undefined)。express
2.Variables 變量編程
開發人員用var關鍵字建立用於存儲數據值。數組
3.Array items 數組項瀏覽器
具備數字索引,存儲一個Javascript數組對象。閉包
4.Object members 對象成員函數
具備字符串索引,存儲一個Javascript對象。性能
每一種數據存儲位置都具備特定的讀寫操做負擔。在大多數狀況下,對一個直接量和一個局部變量的數據訪問的性能差別是微不足道的。具體而言,訪問數組項和對象成員的代價要高一些,具體高多少,很大程度上取決於瀏覽器。通常的建議是,若是關心運行速度,那麼儘可能使用直接量和局部變量,限制數組項和對象成員的使用。爲此,有以下幾種模式,用於避免並優化咱們的代碼:
Managing Scope 管理做用域
做用域概念是理解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在某些狀況下可用改變這種狀況)。
請記住,全局變量老是處於運行期上下文做用域鏈的最後一個位置,因此老是最遠才能被訪問的。一個好的經驗法則是:使用局部變量存儲本地範圍以外的變量值,若是它們在函數中的使用多於一次。考慮下面的例子:
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做爲它們的原型。而對象能夠有兩種類型的成員:實例成員和原型成員。實例成員直接存在於實例自身而原型成員則從對象繼承。考慮以下例子:
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 總結
1.在Javascript中,數據存儲位置能夠對代碼總體性能產生重要影響。有四種數據訪問類型:直接量,變量,數組項,對象成員。對它們咱們有不一樣的性能考慮。
2.直接量和局部變量的訪問速度很是快,而數組項和對象成員須要更長時間。
3.局部變量比外部變量快,是由於它位於做用域鏈的第一個對象中。變量在做用域鏈中的位置越深,訪問所需的時間就越長。而全局變量老是最慢的,由於它處於做用域鏈的最後一環。
4.避免使用with表達式,由於它改變了運行期上下文的做用域鏈。並且應當特別當心對待try-catch語句的catch子句,它具備一樣的效果。
5.嵌套對象成員會形成重大性能影響,儘可能少用。
6.通常而言,咱們經過將常用的對象成員,數組項,和域外變量存入局部變量中。而後,訪問局部變量的速度會快於那些原始變量。
經過上述策略,能夠極大提升那些使用Javascript代碼的網頁應用的實際性能。