1、Loading and Execution 加載和運行javascript
從加載和運行角度優化,源於JavaScript運行會阻塞UI更新,JavaScript腳本的下載、解析、運行過程當中,頁面的下載和解析過程都會停下來等待,由於腳本可能在運行過程當中修改頁面內容。前端
Script Positioning 腳本位置java
將<script>標籤放在儘量接近<body>標籤底部的位置,儘可能減小對頁面下載的影響。web
Grouping Scripts 成組腳本編程
旨在減小http請求,將JavaScript腳本文件合併打包,能夠經過打包工具實現(固然能夠手動合併)或者實時工具,好比Yahoo! 的 combo handler,任何網站經過一個「聯合句柄」URL指出包含YUI文件包中的哪些文件,服務器收到URL請求時,將文件合併在一塊兒後返回給客戶端。segmentfault
Nonblocking Scripts 非阻塞腳本數組
頁面加載完成以後,再加載JavaScript源碼,也就是window的load事件發出後開始下載代碼。瀏覽器
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。
2、Data Acess 數據訪問
數據存儲在哪裏,關係到代碼運行期間數據被檢索到的速度。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的值,避免兩次訪問致使的兩次搜索過程。處理嵌套屬性更須要用這種辦法來處理。
針對數據訪問致使的相關性能問題,主要的解決辦法就是對數據進行暫存,好比將全局變量暫存爲局部變量,減小做用域鏈的深刻搜索;將實例的屬性暫存,減小對原型鏈的屢次深刻搜索;另外一個就是減小使用動態做用域和閉包。
l 轉自:https://segmentfault.com/a/1190000010453813
l 做者:前端老李
| 知海匠庫web前端系統課程學習:http://www.zhihaijiangku.com