高性能Javascript:高效的数据访问
2019獨角獸企業(yè)重金招聘Python工程師標(biāo)準(zhǔn)>>>
經(jīng)典計算機科學(xué)的一個問題是,數(shù)據(jù)應(yīng)當(dāng)存放在什么地方,以實現(xiàn)最佳的讀寫效率。數(shù)據(jù)存儲是否得當(dāng),關(guān)系到代碼運行期間數(shù)據(jù)被檢索到的速度。在Javascript中,此問題相對簡單,因為數(shù)據(jù)表現(xiàn)方式只有少量方式可供選擇。在Javascript中,有四種基本的數(shù)據(jù)訪問位置:
1.Literal values 直接量
直接量僅僅代表自己,而不存儲于特定的位置。
Javascript的直接量包括:字符串(strings)、數(shù)字(numbers)、布爾值(booleans)、對象(objects)、數(shù)組(arrays)、函數(shù)(functions)、正則表達(dá)式(regular expressions),具有特殊意義的空值(null),以及未定義(undefined)。
2.Variables 變量
開發(fā)人員用var關(guān)鍵字創(chuàng)建用于存儲數(shù)據(jù)值。
3.Array items 數(shù)組項
具有數(shù)字索引,存儲一個Javascript數(shù)組對象。
4.Object members 對象成員
具有字符串索引,存儲一個Javascript對象。
每一種數(shù)據(jù)存儲位置都具有特定的讀寫操作負(fù)擔(dān)。在大多數(shù)情況下,對一個直接量和一個局部變量的數(shù)據(jù)訪問的性能差異是微不足道的。具體而言,訪問數(shù)組項和對象成員的代價要高一些,具體高多少,很大程度上取決于瀏覽器。一般的建議是,如果關(guān)心運行速度,那么盡量使用直接量和局部變量,限制數(shù)組項和對象成員的使用。為此,有如下幾種模式,用于避免并優(yōu)化我們的代碼:
Managing Scope 管理作用域
作用域概念是理解Javascript的關(guān)鍵,無論是從性能還是功能的角度而言,作用域?qū)avascript有著巨大影響。要理解運行速度與作用域的關(guān)系,首先要理解作用域的工作原理。
Scope Chains and Identifier Resolution 作用域鏈和標(biāo)識符解析
每一個Javascript函數(shù)都被表示為對象,它是一個函數(shù)實例。它包含我們編程定義的可訪問屬性,和一系列不能被程序訪問,僅供Javascript引擎使用的內(nèi)部屬性,其中一個內(nèi)部屬性是[[Scope]],由ECMA-262標(biāo)準(zhǔn)第三版定義。
內(nèi)部[[Scope]]屬性包含一個函數(shù)被創(chuàng)建的作用域中對象的集合。此集合被稱為函數(shù)的作用域鏈,它決定哪些數(shù)據(jù)可以由函數(shù)訪問。此函數(shù)中作用域鏈中每個對象被稱為一個可變對象,以“鍵值對”表示。當(dāng)一個函數(shù)創(chuàng)建以后,它的作用域鏈被填充以對象,這些對象代表創(chuàng)建此函數(shù)的環(huán)境中可訪問的數(shù)據(jù):
| function?add(num1,?num2){?var?sum?=?num1?+?num2;?return?sum; } |
當(dāng)add()函數(shù)創(chuàng)建以后,它的作用域鏈中填入了一個單獨可變對象,此全局對象代表了所有全局范圍定義的變量。此全局對象包含諸如窗口、瀏覽器和文檔之類的訪問接口。如下圖所示:(add()函數(shù)的作用域鏈,注意這里只畫出全局變量中很少的一部分)
add函數(shù)的作用域鏈將會在運行時用到,假設(shè)運行了如下代碼:
| var?total?=?add(5,10); |
運行此add函數(shù)時會建立一個內(nèi)部對象,稱作“運行期上下文”(execution context),一個運行期上下文定義了一個函數(shù)運行時的環(huán)境。且對于單獨的每次運行而言,每個運行期上下文都是獨立的,多次調(diào)用就會產(chǎn)生多此創(chuàng)建。而當(dāng)函數(shù)執(zhí)行完畢,運行期上下文被銷毀。
一個運行期上下文有自己的作用域鏈,用于解析標(biāo)識符。當(dāng)運行期上下文被創(chuàng)建的時,它的作用域被初始化,連同運行函數(shù)的作用域鏈[[Scope]]屬性所包含的對象。這些值按照它們出現(xiàn)在函數(shù)中的順序,被復(fù)制到運行期上下文的作用域鏈中。這項工作一旦執(zhí)行完畢,一個被稱作“激活對象”的新對象就位運行期上下文創(chuàng)建好了。此激活對象作為函數(shù)執(zhí)行期一個可變對象,包含了訪問所有局部變量,命名參數(shù),參數(shù)集合和this的接口。然后,此對象被推入到作用域鏈的最前端。當(dāng)作用域鏈被銷毀時,激活對象也一同被銷毀。如下所示:(運行add()時的作用域鏈)
在函數(shù)運行的過程中,每遇到一個變量,就要進(jìn)行標(biāo)識符識別。標(biāo)識符識別這個過程要決定從哪里獲得數(shù)據(jù)或者存取數(shù)據(jù)。此過程搜索運行期上下文的作用域鏈,查找同名的標(biāo)識符。搜索工作從運行函數(shù)的激活目標(biāo)的作用域前端開始。如果找到了,就使用這個具有指定標(biāo)識符的變量;如果沒找到,搜索工作將進(jìn)入作用域鏈的下一個對象,此過程持續(xù)運行,直到標(biāo)識符被找到或者沒有更多可用對象可用于搜索,這種情況視為標(biāo)識符未定義。正是這種搜索過程影響了性能。
Identifier Resolution Performance 標(biāo)識符識別性能
標(biāo)識符識別是耗能的。
在運行期上下文的作用域鏈中,一個標(biāo)識符所處的位置越深,它的讀寫速度就越慢。所以,函數(shù)中局部變量的訪問速度總是最快的,而全局變量通常是最慢的(優(yōu)化Javascript引擎,如Safari在某些情況下可用改變這種情況)。
請記住,全局變量總是處于運行期上下文作用域鏈的最后一個位置,所以總是最遠(yuǎn)才能被訪問的。一個好的經(jīng)驗法則是:使用局部變量存儲本地范圍之外的變量值,如果它們在函數(shù)中的使用多于一次。考慮下面的例子:
| 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"; |
此函數(shù)包含三個對document的引用,而document是一個全局對象。搜索至document,必須遍歷整個作用域鏈,直到最后才能找到它。使用下面的方法減輕重復(fù)的全局變量訪問對性能的影響:
| 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更快,因為它是一個局部變量。當(dāng)然,這個簡單的函數(shù)不會顯示出巨大的性能改進(jìn),因為數(shù)量的原因,不過可以想象一下,如果幾十個全部變量反復(fù)被訪問,那么性能改進(jìn)將顯得多么出色。
Scope Chain Augmentation 改變作用域鏈
一個來說,一個運行期上下文的作用域鏈不會被改變。但是,有兩種表達(dá)式可以在運行時臨時改變運行期上下文。第一個是with表達(dá)式:
| 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表達(dá)式,避免了多次書寫“document”。這看起來似乎更有效率,實際不然,這里產(chǎn)生了一個性能問題。
當(dāng)代碼流執(zhí)行到一個with表達(dá)式,運行期上下文的作用域被臨時改變了。一個新的可變對象將被創(chuàng)建,它包含了指定對象(針對這個例題是document對象)的所有屬性。此對象被插入到作用域鏈的最前端。意味著現(xiàn)在函數(shù)的所有局部變量都被推入到第二個作用域鏈對象中,所以局部變量的訪問代價變的更高了。
正式因為這個原因,最好不要使用with表達(dá)式。這樣會得不償失。正如前面提到的,只要簡單的將document存儲在一個局部變量中,就可以獲得性能上的提升。
另一個能改變運行期上下文的是try-catch語句的字句catch具有同樣的效果。當(dāng)try塊發(fā)生錯誤的時,程序自動轉(zhuǎn)入catch塊,并將所有局部變量推入第二個作用域鏈對象中,只要catch之塊執(zhí)行完畢,作用域鏈就會返回到原來的狀態(tài)。
| try?{?methodThatMightCauseAnError(); }?catch?(ex){alert(ex.message);?//作用域鏈在這里發(fā)生改變 } |
如果使用得當(dāng),try-catch表達(dá)式是非常有用的語句,所以不建議完全避免。但是一個try-catch語句不應(yīng)該作為Javascript錯誤解決的辦法,如果你知道一個錯誤會經(jīng)常發(fā)生,那么說明應(yīng)該修改代碼本身。不是么?
Dynamic Scope 動態(tài)作用域
無論是with表達(dá)式還是try-catch表達(dá)式的子句catch,以及包含()的函數(shù),都被認(rèn)為是動態(tài)作用域。一個動態(tài)作用域因代碼運行而生成存在,因此無法通過靜態(tài)分析(通過查看代碼)來確定是否存在動態(tài)作用域。例如:
| function?execute(code)?{?(code);function?subroutine(){?return?window; }var?w?=?subroutine();?//?w的值是什么? }; |
execute()函數(shù)看上去像一個動態(tài)作用域,因為它使用了()。w變量的值與傳入的code代碼有關(guān)。大多數(shù)情況下,w將等價于全局的window對象。但是如果傳入的是:
| execute("var?window?=?{};"); |
這種情況下,()在execute()函數(shù)中創(chuàng)建了一個局部window變量。所以w將等價于這個局部window變量而不是全局window的那個。所以不運行這段代碼是無法預(yù)知最后的具體情況,標(biāo)識符window的確切含義無法預(yù)先知道。
因此,只有在絕對必要時刻才推薦使用動態(tài)作用域。
Closure,Scope,and Memory 閉包,作用域,和內(nèi)存
閉包是Javascript最強大的一個方面,它允許函數(shù)訪問局部范圍之外的的數(shù)據(jù)。為了解與閉包有關(guān)的性能問題,考慮下面的例子:
| function?assignEvents(){var?id?=?"xdi9592";?document.getElementById("save-btn").onclick?=?function(event){saveDocument(id);?}; } |
assignEvents()函數(shù)為DOM元素指定了一個事件處理句柄。此事件處理是一個閉包,當(dāng)函數(shù)執(zhí)行創(chuàng)建時可以訪問其范圍內(nèi)部的id變量。而這種方法封閉了對id變量的訪問,必須創(chuàng)建一個特定的作用域鏈。
當(dāng)assignEvents()函數(shù)執(zhí)行時,一個激活對象被創(chuàng)建,并且包含了一些應(yīng)有的內(nèi)容,其中包含id變量。它將成為運行期上下文作用域鏈上的第一個對象,全局對象是第二個。當(dāng)閉包創(chuàng)建的時,[[Scope]]屬性與這些對象一起被初始化,如下圖:
由于閉包的[[Scope]]屬性包含與運行期上下文作用域鏈相同的對象引用,會產(chǎn)生副作用,通常,一個函數(shù)的激活對象與運行期上下文一同銷毀。當(dāng)涉及閉包時,激活對象就無法銷毀了,因為仍然存在于閉包的[[Scope]]屬性中。這意味著腳本中的閉包與非閉包函數(shù)相比,需要更多的內(nèi)存開銷。尤其在IE,使用非本地Javascript對象實現(xiàn)DOM對象,閉包可能導(dǎo)致內(nèi)存泄露。
當(dāng)閉包被執(zhí)行,一個運行期上下文將被創(chuàng)建,它的作用域鏈與[[Scope]]中引用的兩個相同的作用域鏈同時被初始化,然后一個新的激活對象為閉包自身創(chuàng)建。如下圖:
可以看到,id和saveDocument兩個標(biāo)識符存在于作用域鏈第一個對象之后的位置。這是閉包最主要的性能關(guān)注點:你經(jīng)常訪問一些范圍之外的標(biāo)識符,每次訪問都將導(dǎo)致一些性能損失。
在腳本中最好小心的使用閉包,內(nèi)存和運行速度都值得被關(guān)注。但是,你可以通過上文談到的,將常用的域外變量存入局部變量中,然后直接訪問局部變量。
Object Members 對象成員
對象成員包括屬性和方法,在Javascript中,二者差別甚微。對象的一個命名成員可以包含任何數(shù)據(jù)類型。既然函數(shù)也是一種對象,那么對象成員除了傳統(tǒng)數(shù)據(jù)類型外,也可以包含函數(shù)。當(dāng)一個命名成員引用了一個函數(shù)時,它被稱作一個“方法”,而一個非函數(shù)類型的數(shù)據(jù)則被稱作“屬性”。
如前所言,對象成員的訪問比直接量和局部變量訪問速度慢,在某些瀏覽器上比訪問數(shù)組還慢,這與Javascript中對象的性質(zhì)有關(guān)。
Prototype 原型
Javascript中的對象是基于原型的,一個對象通過內(nèi)部屬性綁定到它的原型。Firefox,Safari和Chrome向開發(fā)人員開放這一屬性,稱作_proto_。其他瀏覽器不允許腳本訪問這個屬性。任何時候我們創(chuàng)建一個內(nèi)置類型的實現(xiàn),如Object或Array,這些實例自動擁有一個Object作為它們的原型。而對象可以有兩種類型的成員:實例成員和原型成員。實例成員直接存在于實例自身而原型成員則從對象繼承。考慮如下例子:
| var?book?=?{title:?"High?Performance?JavaScript",publisher:?"Yahoo!?Press" }; alert(book.toString());?//"[object?Object]" |
此代碼中book有title和publisher兩個實例成員。注意它并沒有定義toString()接口,但這個接口卻被調(diào)用且沒有拋出錯誤。toString()函數(shù)就是一個book繼承自原型對象的原型成員。下圖表示了它們的關(guān)系:
處理對象成員的過程與處理變量十分相似。當(dāng)book.toString()被調(diào)用時,對成員進(jìn)行名為“toString”的搜索,首先從對象實例開始,若果沒有名為toString的成員,那么就轉(zhuǎn)向搜索原型對象,在那里發(fā)現(xiàn)了toString()方法并執(zhí)行它。通過這種方法,book可以訪問它的原型所擁有的每個屬性和方法。
我們可以使用hasOwnProperty()函數(shù)確定一個對象是否具有特定名稱的實例成員。實例略。
Prototype Chains 原型鏈
對象的原型決定了一個實例的類型。默認(rèn)情況下,所有對象都是Object的實例,并繼承了所有基本方法。如toString()。我們也可以使用構(gòu)造器創(chuàng)建另外一種原型。例如:
| 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);?//truealert(book1?instanceof?Object);?//truebook1.sayTitle();?//"High?Performance?JavaScript"?alert(book1.toString());?//"[object?Object]" |
Book構(gòu)造器用于創(chuàng)建一個新的book實例book1。book1的原型(_proto_)是Book.prototype,Book.prototype的原型是Object。這就創(chuàng)建了一條原型鏈。
注意,book1和book2共享了同一個原型鏈。每個實例擁有自己的title和publisher屬性,其他成員均繼承自原型。而正如你所懷疑的那樣,深入原型鏈越深,搜索的速度就會越慢,特別是IE,每深入原型鏈一層都會增加性能損失。記住,搜索實例成員的過程比訪問直接量和局部變量負(fù)擔(dān)更重,所以增加遍歷原型鏈的開銷正好放大了這種效果。
Nested Members 嵌套成員
由于對象成員可能包含其他成員。譬如window.location.href(獲取當(dāng)前頁面的url)這種模式。每遇到一個點號(.),Javascript引擎就要在對象成員上執(zhí)行一次解析過程,而且成員嵌套越深,訪問速度越慢。location.href總是快于window.location.href,而后者比window.location.href.toString()更快。如果這些屬性不是對象的實例成員,那么成員解析還要在每個點上搜索原型鏈,這將需要更多的時間。
Summary 總結(jié)
1.在Javascript中,數(shù)據(jù)存儲位置可以對代碼整體性能產(chǎn)生重要影響。有四種數(shù)據(jù)訪問類型:直接量,變量,數(shù)組項,對象成員。對它們我們有不同的性能考慮。
2.直接量和局部變量的訪問速度非常快,而數(shù)組項和對象成員需要更長時間。
3.局部變量比外部變量快,是因為它位于作用域鏈的第一個對象中。變量在作用域鏈中的位置越深,訪問所需的時間就越長。而全局變量總是最慢的,因為它處于作用域鏈的最后一環(huán)。
4.避免使用with表達(dá)式,因為它改變了運行期上下文的作用域鏈。而且應(yīng)當(dāng)特別小心對待try-catch語句的catch子句,它具有同樣的效果。
5.嵌套對象成員會造成重大性能影響,盡量少用。
6.一般而言,我們通過將經(jīng)常使用的對象成員,數(shù)組項,和域外變量存入局部變量中。然后,訪問局部變量的速度會快于那些原始變量。
通過上述策略,可以極大提高那些使用Javascript代碼的網(wǎng)頁應(yīng)用的實際性能。
轉(zhuǎn)載于:https://my.oschina.net/netmouse/blog/345893
總結(jié)
以上是生活随笔為你收集整理的高性能Javascript:高效的数据访问的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 大四Java复习笔记之Java基础
- 下一篇: java美元兑换,(Java实现) 美元