早前閱讀高性能JavaScript一書所作筆記。javascript
從加載和運行角度優化,源於JavaScript
運行會阻塞UI更新,JavaScript
腳本的下載、解析、運行過程當中,頁面的下載和解析過程都會停下來等待,由於腳本可能在運行過程當中修改頁面內容。前端
Script Positioning
腳本位置將<script>標籤放在儘量接近<body>標籤底部的位置,儘可能減小對頁面下載的影響。java
Grouping Scripts
成組腳本旨在減小http請求,將JavaScript
腳本文件合併打包,能夠經過打包工具實現(固然能夠手動合併)或者實時工具,好比Yahoo! 的 combo handler
,任何網站經過一個「聯合句柄」URL指出包含YUI文件包中的哪些文件,服務器收到URL請求時,將文件合併在一塊兒後返回給客戶端。編程
Nonblocking Scripts
非阻塞腳本頁面加載完成以後,再加載JavaScript
源碼,也就是window
的load
事件發出後開始下載代碼。segmentfault
Deferred Scripts
延期腳本數組
HTML4
爲<script>
標籤訂義的擴展屬性:defer
。若是你爲<script>
指定defer
屬性,代表此腳本不打算修改DOM,代碼能夠稍後執行。IE4+/FF3.5+
支持。具備defer
屬性的腳本,能夠放在頁面的任何位置,能夠和頁面的其餘資源一期並行下載,但會在DOM加載完成,onload
事件句柄被調用以前執行。瀏覽器
Dynamic Script Elements
動態腳本元素
建立一個script
元素,指定src
屬性,而後在頁面加載完成以後添加到頁面的任何地方。一個簡單的通用demo:緩存
function loadScript(url, callback) { var script = document.createElement("script") script.type = "text/javascript"; if (script.readyState) { //IE script.onreadystatechange = function() { if (script.readyState == "loaded" || script.readyState == "complete") { script.onreadystatechange = null; callback(); } }; } else { //Others script.onload = function() { callback(); }; } script.src = url; document.getElementsByTagName("head")[0].appendChild(script); }
XMLHttpRequest Script Injection XHR
腳本注入
使用XMLHttpRequest
對象,將腳本注入到頁面,和動態腳本元素有相似之處,先建立XHR
對象,而後經過get
方法下載JavaScript
文件,接着用動態<script>
元素將JavaScript
代碼注入頁面。服務器
var xhr = new XMLHttpRequest(); xhr.open("get","file.js",true); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){ var script = document.createElement ("script"); script.text = xhr.responseText; document.body.appendChild(script); } } } xhr.send(null);
因爲JavaScript
的同源策略,腳本文件必須和頁面放置在同一個域內,不能經過CDN下載,所以不常見於大型網頁。閉包
Recommended Nonblocking Pattern
推薦的非阻塞模式先加載一個加載器,而後加載JavaScript
。
好比上文中提到的loadScript
方法就能夠封裝爲一個初級的加載器,而後經過loadScript
方法來加載其餘腳本。只不過這個微型加載器要保證依賴關係會比較醜:
loadScript("./file1.js", function () { loadScript("./file2.js", function () { loadScript("./file3.js", function () { //do something }) }) })
再好比YUI3
,好比lazyload.js
,好比lab.js
。
數據存儲在哪裏,關係到代碼運行期間數據被檢索到的速度。JavaScript
中有四種基本的數據存儲位置:Literal
values
(直接量)、Variables
(變量)、Array items
(數組項)、Object members
(對象成員)。對於直接量和局部變量的訪問性能差別微不足道,性能消耗代價高一些的是全局變量、數組項、對象成員。
Managing Scope
管理做用先了解一下做用域的原理
每個JavaScript
函數都是一個對象,也能夠叫函數實例,函數對象和其餘對象同樣,擁有編程能夠訪問的屬性和不能被程序訪問,僅供JavaScript
引擎使用的內部屬性,一種有一個叫[[Scope]]
的屬性。[[Scope]]
中包含函數做用域中對象的集合(做用域鏈),它表示當前函數環境中可訪問的數據,以鏈式的形式存在。當一個函數被建立後,做用域鏈中被放入可訪問的對象。例如:
function add (a,b) { var result = a + b; return result; }
此時做用域鏈中被推入一個可變的全局對象(隨便取個名叫「房間A」),表明了全部全局範圍中的變量,包含window、document、navigator
等的訪問接口。
在函數運行期間,函數內部會創建一個內部對象,稱爲運行期上下文。這個對象定義了函數運行時的環境,每次函數運行,這個上下文都是獨一的,屢次調用函數就會屢次建立運行期上下文對象,函數執行完畢,這個上下文對象會被銷燬。這個上下文環境也有本身的做用域鏈,用來解析標識符(理解爲尋找變量),當一個運行期上下文被建立時,它的做用域鏈被初始化,函數自己的[[Scope]]
屬性中的對象,按照原來的順序被複制到運行期上下文的做用域鏈中。此時運行期上下文會建立一個新的對象,名叫「激活對象
(取名叫「房間B」)」,「房間B」中存儲了全部的局部變量、命名參數、參數集合和this的接口。而後「房間B」被推入到做用域鏈的前端。在剛剛所說的可變全局對象(「房間A」)的前面。
函數過程當中,每遇到一個變量,標識符識別過程都要決定從哪裏得到或者存儲數據。它會搜索運行期上下文的做用域鏈,查找同名的標識符,搜索工做從做用域鏈的前端開始查找,也就是剛纔的「房間B」那裏查找,若是找到了,就是用對應的變量值,若是沒找到就進入「房間A」進行查找,若是找到,就用對應的值,沒有找到就認爲這個標識符是未定義的("undefined");
在以前的add
函數運行過程當中,result/a/b
三個變量的查找實際上都進行了上述的搜索過程,所以產生性能問題。當一個標識符所處位置越深,讀寫速度就越慢,因此函數中局部變量的訪問速度是最快的,全局變量一般很慢,由於全局變量老是處於做用域鏈最後一個位置,前面的房間都找過了,沒找到,纔會過來他這裏找。所以,就有了優化性能的辦法:
用局部變量存儲本地範圍以外的變量值(若是這個變量值被屢次使用)
好比:
function foo() { var a = document.getElementById("a"), b = document.getElementsByTagName("div"); }
這時候document
被查找了兩次,並且每次都要先找「房間B」,再找「房間A」才能找到,這時候就能夠用一個局部變量暫存document
:
function foo() { var doc = document, a = doc.getElementById("a"), b = doc.getElementsByTagName("div"); }
減小使用動態做用域(Dynamic Scopes)
with()
with
能夠臨時改變函數的做用域鏈,在某些特殊場景下,能夠加快一些變量的訪問。好比一個函數內屢次使用document
:
function foo() { var a = document.getElementById("a"), b = document.getElementsByTagName("div"); console.log(a.className); }
能夠改寫爲:
function foo() { with(document){ var a = getElementById("a"), b = getElementsByTagName("div"); console.log(a.className); } }
在這裏,document
對象以及document
對象全部的屬性,都被插入到做用域的最前端,頁面在尋找"getElementById"方法是會首先從document
對象屬性中尋找,而不須要從foo()
的做用域中查找,而後再到全局做用域中進行查找,下降了二次查找的消耗。可是在document
對象的屬性被推入做用域鏈的最前端的同時,其餘局部變量都被推入做用域鏈第二的位置。上例中,在查找a的時候,會先從document
對象屬性中查找,沒有才會從foo()
的做用域中進行查找。這樣帶來的性能消耗每每得不償失。所以with
必須慎用,只有在極個別的場景中才划算。try-cahch
當try
中程序塊發生錯誤而轉入catch
塊中時,程序會自動將異常對象推入做用域鏈的最前端。一樣會改變做用域鏈,帶來性能問題。所以在不得不用try-catch
語句的時候,能夠採用下面的操做方式:
try{ //do something }catch(e){ handleError(e); }
在catch
塊中運行錯誤處理函數,將錯誤對象做爲參數傳給錯誤處理函數,catch
塊中做用域鏈的改變就沒什麼影響了。others
還有一些其餘的狀況,好比:
function foo(f){ (f); function returnWindow(){ return window; } var s = returnWindow(); }
正常狀況下,上述函數window
就是window
,可是若是咱們執行:
foo("var window = 'I am not window';");
這時候的window
就再也不是那個window
了。性能上的問題不說,只是變量做用域變得不可控了,帶來其餘的問題。同時,在一些現代瀏覽器中,好比Safari
的Nitro
引擎中,會銅鼓分析代碼來肯定哪些變量應該在任意時刻被訪問,繞過傳統做用域鏈查找,用標識符索引的方式快速查找,以此來加快標識符識別過程。可是遇到動態做用域的時候,引擎須要切回慢速的基於哈希表的標識符識別方法,這裏的瀏覽器引擎作的努力就沒辦法了。
closures
慎用閉包
慎用閉包有兩個方面的緣由。一是閉包必然存在函數嵌套,閉包內訪問外部變量都會通過最少兩次的查找。更重要的問題在於,閉包須要訪問外部變量,所以致使函數運行期的激活對象被保存,沒法銷燬。引用始終存在於閉包的[[Scope]]
屬性中,不只消耗更多的內存開銷,在IE中還會致使內存泄露。
Object Members
對象成員JavaScript
中一切皆對象,對象的命名成員能夠包含任意數據類型,固然就能夠包含函數。這裏所說的對象成員,指的就是函數對象,函數對象的訪問速度,比直接亮和局部變量要慢,某些瀏覽器的實現中,甚至比數組還要慢。找到優化辦法以前,須要先了解緣由。
Prototypes
原型JavaScript
中的對象是基於原型的,原型是對象的基礎,定義並實現了一個新對象所必須具備的成員。原型對象爲全部給定類型的對象實例共享,全部的實例共享原型對象的成員。一個對象經過一個內部屬性綁定到本身的原型,在FF/Safari/Chrome
中,這一對象被稱爲_proto_
,任什麼時候候建立一個內置類型的實例,這些實例將自動擁有一個Object
做爲他們的原型。
所以一個對象擁有成員能夠分爲兩類:實例成員(own成員)和原型成員。實例成員直接存在於實例自身,而原型成員則從對象成員繼承。例:
var cat = { name:"xiaohua", age:1 }
在這裏,cat
的實例成員就是name
和age
,原型成員就是cat._proto_
中的成員屬性,而cat._proto_
屬性是Object.prototype
,在這裏就是Object
,以下調用時:
console.log(cat.name);
在調用cat.name
屬性時,如今cat
實例成員中查找,若是調用cat.toString()
方法時,一樣先在cat
的實例成員中查找,找不到的時候再到其原型成員中查找,和處理變量的過程相似,一樣也就致使了性能問題。
Prototype Chains
原型鏈
對象的原型決定了一個實例的類型,默認狀況下,全部對象都是Object
的實例,並繼承了全部基本方法,當咱們使用構造器建立實例時,就建立了另一種類型的原型。
function Animal(name,age){ this.name = name, this.age = age } Animal.prototype.sayHello = function(){ console.log("Hello,I am a " + this.name); } var cat = new Animal("cat",1); var dog = new Animal("dog",1);
cat
是Animal
的實例,cat._proto_
是Animal.prototype
,Animal.prototype._proto_
是Object
;dog
和cat
共享一個原型鏈,但各自擁有本身的實例成員name
和age
。若是咱們調用了cat.toString()
方法時,搜索路徑以下:
cat---cat._proto_(Animal.prototype)---cat._proto_.constructor(Animal)---cat._proto_.Constructor._proto_(Object);
原型鏈每深刻一個層級,就會帶來更大的性能消耗,速度也就會更慢。而對於實例成員的搜索開銷自己就大於訪問直接量或者是局部變量,所以這種性能消耗仍是很值得去優化的。
Nested Members
嵌套成員
例如:window.local.href
;每遇到一個 .
;JavaScript
引擎就會在該對象成員上執行一次解析過程。好比若是href
並非local
的實例屬性,解析引擎就會去local
的原型鏈去進行搜索,由此帶來嚴重的性能消耗。
Caching Object Member Values
緩存對象成員的值
上述的性能問題都是和對象成員有關,所以要儘可能避免對對象成員的搜索,好比:
function foo(ele,className1,className2) { return ele.className == className1 || ele.className == className2; }
在這裏,咱們訪問了兩次ele
的className
屬性,可是這兩次訪問時,ele
的className
屬性值是同樣的,所以能夠在這裏用一個變量暫存ele.className
的值,避免兩次訪問致使的兩次搜索過程。處理嵌套屬性更須要用這種辦法來處理。
針對數據訪問致使的相關性能問題,主要的解決辦法就是對數據進行暫存,好比將全局變量暫存爲局部變量,減小做用域鏈的深刻搜索;將實例的屬性暫存,減小對原型鏈的屢次深刻搜索;另外一個就是減小使用動態做用域和閉包。
高性能JavaScript閱讀簡記(一)
高性能JavaScript閱讀簡記(二)
高性能JavaScript閱讀簡記(三)