JS性能優化——數據存取
首先,了解幾個概念:
字面量:它只代表自身,不存儲在特定的位置。JavaScript中的字面量有:字符串、數字、布爾值、對象、數組、函數、正則,以及特殊的null和undefined值
本地變量:使用var 定義的數據單元
數組元素:存儲在JavaScript數組對象內部,以數字作為索引
對象成員:存儲在JavaScript對象內部,以字符串作為索引。
每一種數據存儲的位置都有不同的讀寫消耗。大多數情況下,從一個字面量和局部變量中存取數據的性能差異是微不足道的。訪問數組元素和對象成員的代價則要高一些,高多少主要取決於瀏覽器。
字面量和局部變量的訪問速度快於數組項和對象成員的訪問速度。
通常的建議是盡量使用字面量和局部變量,減少數字向和對象成員的使用。為此有下邊幾種模式可優化代碼:
管理作用域:
每一個JavaScript函數都表示為一個對象,確切的說是Function對象的一個實例。Function對象擁有可編程訪問的屬性,和一系列不能通過代碼訪問僅供JavaScript引擎存取的內部屬性。其中一個就是[[scope]]即作用域鏈。它決定了哪些屬性能被函數訪問。函數作用域中的每個對象都被稱為一個可變對象,每個可變對象都以鍵值對的形式存在。每一個變量都會經歷一次標識符解析過程,該過程搜索執行期的作用域鏈,這個搜索過程會影響性能。
標識符的解析是有代價的,在執行環境的作用域鏈中,一個標識符所在的位置越深,它的讀寫速度也就越慢。
因此函數中讀寫局部變量總是最快的。讀寫全局變量是最慢的。全局變量總是存在於執行環境作用域鏈的做末端,是最遠的。
建議盡可能的使用局部變量。一個經驗法則:如果某個跨作用域的值在函數中被引用了一次以上,那麽就把它存儲到局部變量裏。
function initUI(){ var bd = document.body, links = document.getElementsByTagName("a"), i = 0, len = links.length; ... ... } //在方法裏多次用到document這個全局對象,可以把它的引用存放在一個局部變量中,讓局部變量代替全局變量。訪問全局變量的次數由2次變成了1次function initUI(){ var doc = document, bd = doc.body, links = doc.getElementsByTagName("a"), i = 0, len = links.length; ... ... }
改變作用域鏈:
有兩個語句在執行時會改變作用域鏈。
一個是with語句:with語句避免了多次書寫同一個全局變量,但是會產生性能問題。
function initUI(){ with(document){ var bd = body, links = getElementsByTagName("a"), i = 0, len = links.length; ... ... } }
當程序執行到with語句時,執行環境的作用域鏈臨時被改變了。一個新的變量對象被創建,它包含了參數指定的所有屬性。這個對象被推入作用域鏈的首位,這意味著函數的所有局部變量現在都位於第二個作用域鏈對象中,因此訪問的代價更高了:訪問document對象的屬性非常快,而訪問局部變量則變慢了,比如bd
另一個是try-catch語句中的catch子句:也具有with同樣的效果
try{ methodThatMightCauseAnError(); }catch(ex){ alert(ex.message);//作用域鏈在此處改變 }
try代碼塊中發生錯誤,執行過程會自動跳轉到catch子句,然後把異常對象推入一個變量對象並置於作用域的首位。在catch代碼塊內部,函數所有局部變量將會放在第二個作用域鏈對象中。一旦catch子句執行完畢,作用域鏈就會返回之前的狀態。
try-catch語句是一個非常有用的語句 不建議完全棄用。可以盡量簡化代碼使catch對子句的影響最小化,可以將錯誤處理委托給一個函數來處理
try{ methodThatMightCauseAnError(); }catch(ex){ handleError(ex); }
由於只執行一條語句,沒有局部變量的訪問,作用域鏈的臨時改變就不會影響代碼性能。
動態作用域:
無論是with語句還是try-catch還是包含eval()的函數,都被認為是動態作用域。動態作用域只存在於代碼執行過程中,因此無法通過靜態分析(查看代碼結構)檢測出來。
經過優化的JavaScript引擎。比如safari的Nitro引擎,嘗試通過分析代碼來確定哪些變量可以在特定的時候被訪問。這些引擎試圖避開傳統作用域鏈的查找,用標識符索引的方式進行快速查找來代替。但是當遇到動態作用域時就失效了,腳本引擎必須切換回較慢的基於哈希表的標識符識別方式,這更像是傳統的作用域鏈查找。
推薦:只有在確實有必要時才使用動態作用域。
閉包、作用域和內存:
由於閉包的[[scope]]屬性包含了與執行環境作用域鏈相同的對象的引用,因此函數的激活對象不會隨著執行環境一同銷毀。這意味著腳本中的閉包和非閉包函數相比,需要更多的內存開銷。在大型WEB應用中,這可能是個問題。尤其是IE瀏覽器中需要關註,由於IE使用非原生JavaScript對象來實現DOM對象,因此閉包會導致內存泄漏。
腳本編程中,最好小心的使用閉包,它同時關系到內存和執行速度。
對象成員:
對象在原型鏈中存在的位置越深,找到它也就越慢。每深入一層原型鏈都會增加性能損失,搜索實例成員比從字面量或局部變量中讀取數據代價更高,並且還有遍歷原型鏈帶來的開銷,這些讓性能問題更為嚴重。
嵌套成員,即點操作符:window.location.href。每次遇到點操作符,嵌套成員會導致JavaScript引擎搜索所有對象成員。
對象成員嵌套的越深,讀取速度就會越慢。執行location.href總是比window.location.href要快,後者也比window.location.href.toString()要快。如果這些屬性不是對象的實例屬性,那麽成員解析還需要搜索原型鏈,這會花更多的時間。
大部分瀏覽器中,通過點表示法(object.name)操作和通過括號表示法(object["name"])操作並沒有明顯的區別。只有在Safari中,點符號始終會更快,但這並不意味著不要用括號符號。
緩存對象成員值:
所有類似的性能問題都與對象成員有關,因此應該盡可能避免使用他們,或者應該說,只在必要時使用對象成員。(在同一個函數中沒有必要多次讀取同一個對象成員)
通常來說,如果函數中要多次讀取同一個對象的屬性,最佳做法是講=將屬性值保存到局部變量中。局部變量能用來替代屬性以避免多次查找帶來的性能開銷。特別是在處理嵌套對象成員時,這樣做會明顯提升執行速度。
不要再同一個函數中多次查找同一個對象成員,除非它的值改變了。
總結:在JavaScript中,數據存儲的位置會對代碼整體性能產生重大的影響。數據存儲共有4種方式:字面量、變量、數組項、對象成員。
1、訪問字面量和局部變量的速度最快,相反訪問數組元素和對象成員相對較慢。
2、由於局部變量存在於作用鏈的起始位置,因此訪問局部變量比訪問跨作用域變量更快。變量在作用域鏈中的位置越深,訪問所需要時間就越長。由於全局變量總處在作用域鏈的最末端,因此訪問速度也是最慢的
3、避免使用with語句,因為它會改變執行環境作用域鏈。同樣的try-catch語句中的catch子句也有同樣的影響,因此要小心使用。
4、嵌套的對象成員會明顯影響性能,盡量少用。
5、屬性或方法在原型鏈中的位置越深,訪問它的速度也越慢。
6、通常來說,你可以把常用的對象成員、數組元素、跨域變量保存在局部變量中來改善JavaScript性能,因為局部變量訪問速度更快。
JS性能優化——數據存取