在計算機科學中,數據存儲的位置關係到代碼執行過程當中數據的檢索速度,有一個經典的問題即爲:經過改變數據的存儲位置來得到最佳的讀寫性能。前端
字面量
字面量只表明自身,不存儲在特定的位置。JavaScript中的字面量有:字符串,數字,布爾值,對象,數組,函數,正則表達式,以及null&undefined。
字面量是用於表達源代碼中一個固定值的表示法,例如:string str="hello world";
hello world爲字面量正則表達式
本地變量
開發人員使用關鍵字var定義的數據存儲單元編程
數組元素
存儲在JavaScript數組對象內部,以數字做爲索引,這裏注意和本地變量的區別,
var arr = new Array();
arr爲本地變量,arr[0]爲一個數組元素數組
對象成員
儲存在JavaScript對象內部,以字符串做爲索引瀏覽器
每一種數據存儲的位置都有不一樣的讀寫消耗,通常而言:性能優化
從一個字面量中存取數據的性能約等於局部變量閉包
數組元素和對象成員成本較高,高出多少由瀏覽器決定函數
做用域概念是理解JavaScript的關鍵所在,不只僅從性能,還包括從功能的角度。做用域對JavaScript有不少影響,從肯定哪些變量能夠被函數訪問,到肯定
this
的賦值。性能
做用域鏈function
能夠理解爲一個「製造機器的機器」,那麼咱們能夠這樣理解:每個JavaScript函數都是一個function對象的實例。
那麼function
對象和其餘對象同樣,擁有能夠編程訪問的屬性和一系列不一樣經過代碼訪問而僅供JavaScript引擎存取的內部屬性。優化
內部屬性之Scope
先放一個Scope的有趣解釋;
Scope屬性包含了一個函數被建立時的做用域中的對象的集合,這個集合被稱爲做用域鏈,它決定哪些數據能被函數訪問。
函數做用域中的每一個對象被稱爲一個可變對象,每一個可變對象都以「鍵值對」的形式存在。
當一個函數建立後,它的做用域鏈會被建立此函數的做用域中可訪問的數據對象所填充。
我說下本身的理解:做用域、做用域鏈、內置屬性(Scope)其實能夠類比權限、管理組、全局管理員,做用域中的對象以鍵值對的形式存在,成爲可變對象,做用域鏈用來鏈接做用域和Scope,而Scope就好像一種專門管理全局對象的全局管理員;
舉一個例子:
咱們先建立一個函數:
function add(num1,num2){ var sun = num1 + num2; return sum; }
這裏咱們建立了一個add()函數,當他被建立的時候,在這個函數的內置屬性Scope所包含的做用域鏈中插入一個對象變量,這個全局對象表明着全部在全局範圍內定義的變量。 改全局對象包含像window,navigator,document等;
當咱們來執行上面的函數又會發生什麼呢?
假如執行以下代碼:
var total = add(5,10);
此時函數會建立一個稱爲 執行環境 或者叫 執行上下文 (execution context)的內部對象。
一個execution context定義了一個函數執行時的環境。
函數每次執行時對應的execution context都是獨一無二的,因此屢次調用同一個函數就會建立多個不同的execution context
當函數執行完畢,execution context就會被銷燬
每一個execution context都有本身的做用域鏈,用於解析標識符。
當execution context被建立時,它的做用域鏈初始化爲當前運行函數的Scope屬性中的對象,這些值按照他們出如今函數中的順序,被複制到執行環境的做用域鏈中。
前面這個過程完成以後,一個「活動對象」也爲execution context建立好了,該對象做爲函數運行時的變量對象,包含了全部的局部變量,參數集合以及this。
而後這個對象被推入做用域鏈最前端。
當execution context被銷燬,活動對象也隨之銷燬。
咱們在執行過程當中是怎樣使用做用域鏈的呢?
在函數執行過程當中,沒遇到一個變量,都會經歷一次標識符解析過程以決定從哪裏獲取或存儲數據。標識符解析是性能開銷的,即有代價的!解析標識符實際上就是搜索execution context的做用域鏈,來匹配同名的標識符。
搜索過程從做用域鏈頭部開始,即做用域鏈中的數字越小越優先,這意味着,一個標識符所在的位置越深,它的讀寫速度越慢
函數中讀寫局部變量老是最快的,而讀寫全局變量老是最慢的
在查找過程當中,若是找到,就使用這個標識符對應的變量,若沒找到,則繼續查找下一個對象,若整個搜索過程都沒有找到匹配的對象,那麼這個標識符將被視爲未定義的
正是這個搜索過程影響了性能
在沒有優化JavaScript引擎的瀏覽器中,儘量使用局部變量,一個好的經驗法則就是:若是某個跨做用域的值在函數中被引用一次以上,那麼就把它存儲在局部變量裏。
咱們來看一個例子:
function initUI(){ var bd = document.body, links = document.getElementsByTagName("a"), i = 0, len = links.length; while(i<len){ update(links[i++]); } document.getElementById("btn").onclick = function(){ start(); }; bd.className = "active"; }
咱們看到上面這個函數用了三次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("btn").onclick = function(){ start(); }; bd.className = "active"; }
咱們將訪問document的次數由三次變成了一次,若是這個訪問次數足夠大的話,那麼咱們的性能將獲得極大的改善!
學到這裏,我認爲改善標識符的解析性能能夠從提升解析速度和減小使用次數兩方面入手,前者經過優化JavaScript引擎來進行,後者咱們在編程過程當中能夠進行實踐,二者的前提都是搜索可以正確進行!
通常來講,一個execution context的做用域鏈是不會被改變的,可是在JavaScript中有兩個語句是能夠在執行時臨時改變做用域鏈的,爲動態做用域。
NO.1 With語句
With語句用來在做用域鏈中新建立一個變量對象,這個可變對象包含了參數指定的對象的全部屬性。先看看With在編程中怎麼使用:
function initUI(){ with (document){ var bd = body, links = getElementsByTagName("a"), i = 0, len = links.length; while(i<len){ update(links[i++]); } getElementById("btn").onclick = function(){ start(); }; bd.className = "active"; } }
從代碼中能夠很直觀看到,它也只在全局對象中執行一次搜索,從而避免了屢次書寫document,可是這樣會更加高效嗎?
咱們來看看執行with
語句時,做用域鏈中發生了什麼:
當執行with
語句時,它的execution context被臨時改變了,一個新的對量對象被建立,它包含了參數指定的對象的全部屬性,而且這個對象被推入了做用域鏈的首位;
在上面的例子中,經過把document
對象傳遞給with
語句,一個包含了document對象全部屬性的新的可變變量被置於做用域的頭部,這樣就出現了一個問題:我訪問document對象的屬性很是快,可是當我想訪問活動對象(也就是局部變量)或者全局對象的屬性時,個人解析標識符速度反而下降了,因此,在減小全局對象屬性這方面的性能優化,將document儲存在一個局部變量中比用with語句改變做用域鏈更加可靠!
NO.2 try-catch語句 try-catch
語句中的catch字句也具備臨時改變做用域鏈的效果。
當try代碼塊中發生錯誤,執行過程會自動跳轉到catch字句,而後將異常對象推入一個變量對象並置於做用域的首位,也就是說,在catch代碼塊內部,函數全部的局部變量都會放在第二個做用域鏈對象中,可是,一旦catch代碼塊執行完畢,做用域鏈就會返回到以前的狀態。
try-catch 語句不該該被用來解決JavaScript錯誤,若是某個錯誤重現率很高,最好是儘快修復。其實做用域鏈的改變是發生在catch代碼塊執行的過程當中,那麼咱們若是在catch代碼塊內沒有對局部變量和全局變量的訪問,就可使catch字句對性能的影響最小化!
這種思想的一種實現方法就是將錯誤委託給一個函數來處理!
閉包是JavaScript最強大的特性之一,它容許函數訪問局部做用域以外的數據,可是閉包在使用過程種可能會致使性能問題。
咱們先來看一個閉包的例子:
function assignEvents(){ var id = "xdi9592"; document.getElementById("btn").onclick = function(){ saveDocument(id); }; }
assignEvents()函數給一個DOM元素設置事件處理函數,這個事件處理函數就是一個閉包,它在assignEvents()執行時建立,而且能夠訪問所屬做用域的id變量。
如圖所示,當assignEvents()函數執行時,一個包含了變量id以及其餘數據的活動對象被建立,這個活動對象成爲execution context做用域鏈中的第一個對象,緊接着就是全局對象,而後閉包被建立,而且它的Scope屬性被初始化爲這些對象。
至此,出現了第一個問題:內存問題。
通常來講,一個函數執行完了以後,函數做用域鏈中的活動對象會隨着execution context一塊兒被銷燬,可是引入了閉包以後,因爲引用仍然存在於閉包的Scope屬性中,因此此時活動對象無法被銷燬,這意味着腳本中閉包與非閉包函數相比,須要更多的內存開銷。
而後在閉包代碼執行時,又會建立一個閉包的execution context,它的做用域鏈與自身Scope中所引用的兩個相同的做用域鏈對象一塊兒被初始化,而後建立一個閉包的活動對象,而且放在首位;
能夠看到閉包內代碼所用的id & savaDocument,他們的位置分列2,3,就裏就是咱們在使用閉包過程當中所須要關注的性能點:在頻繁地訪問跨做用域的標識符的時候,每次訪問都會帶來性能損失。
最後劃一下重點:將經常使用的跨做用域變量存儲到局部變量中,而後直接經過局部變量來訪問,是一個可行的方法。
--END--