這多是每個jser都曾經爲之頭疼的卻又很是經典的問題,關係到內存,關係到閉包,關係到javascript運行機制。關係到功能,關係到性能。javascript
文章內容主要參考自《High Performance JavaScript》,這本書對javascript性能方面確實講的比較深刻,你們有空均可以嘗試着閱讀一下,我這裏有中英電子版,須要的話QQ317665171或者QQ郵箱聯繫。html
複習,筆記,更深刻的理解。java
歡迎拍磚指正。express
下面咱們先搞明白這樣幾個概念:緩存
[[scope]]屬性:網絡
javascript中每一個函數都是一個函數對象(函數實例),既然是對象,就有相關的屬性和方法。[[scope]]就是每一個函數對象都具備的一個僅供javascript引擎內部使用的屬性,該屬性是一個集合(相似於鏈表結構),集合中保存了該函數在被建立時的做用域中的全部對象,而這個做用域集合造成的鏈表則被稱爲ScopeChain(做用域鏈)。閉包
該做用域鏈中保存的做用域對象,就是該函數能夠訪問的全部數據。例如(例子引用自《High Performance JavaScript高性能javascript》):函數
function add(num1, num2){性能
var sum = num1 + num2;學習
return sum;
}
當add函數被建立時,函數所在的全局做用域的全局對象被放置到add函數的做用域鏈([[scope]]屬性)中。咱們能夠從圖1中看到做用域鏈的第一個對象保存的是全局對象,全局對象中保存了諸如this,window,document以及全局對象中的add函數,也就是他本身。這也就是咱們能夠在全局做用域下的函數中訪問window(this),訪問全局變量,訪問函數自身的緣由。固然還有函數做用域不是全局的狀況,等會兒咱們再討論。
Execution Context(運行期上下文)、Activation Object(激活對象):
(前天看了老羅的演講,老羅說過年的時候給全公司的人每人發一臺電冰箱,要給校舍的全部的廁所門上都安上新鎖,保證童鞋們能有個真正隱私的地方。)
var total = add(5, 10);
當開始執行此函數時,就會建立一個Execution Context的內部對象,該對象定義了函數運行時的做用域環境(注意這裏要和函數建立時的做用域鏈對象[[scope]]區分,這是兩個不一樣的做用域鏈對象,這樣分開我猜想一是爲了保護[[scope]],二是爲了方便根據不一樣的運行時環境控制做用域鏈。函數每執行一次,都會建立單獨的Execution Context,也就至關於每次執行函數前,都把函數的做用域鏈複製了一份到當前的Execution Context中)。Execution Context對象有本身的做用域鏈,在Execution Context建立時初始化,會將函數建立時的做用域鏈對象[[scope]]中的所有內容按照在[[scope]]做用域鏈中的順序複製到Execution Context的做用域鏈中。
此時,在Execution Context的做用域鏈的頂部會插入一個新的對象,叫作Activation Object(激活對象),這個激活對象又是幹嗎的呢?這個激活對象保存了函數中的全部形參,實參,局部變量,this指針等函數執行時函數內部的數據狀況,這個Activation Object是一個可變對象,裏面的數據隨着函數執行時的數據的變化而變化,當函數執行結束以後,就會銷燬Execution Context,也就會銷燬Execution Context的做用域鏈,固然也就會銷燬Activation Object(但若是存在閉包,Activation Object就會以另一種方式存在,這也是閉包產生的真正緣由,具體的咱們稍後討論。)。具體狀況如圖所示:
咱們從左往右看,第一部分是函數執行時建立的Execution Context,它有本身的做用域鏈,第二部分是做用域鏈中的對象,索引爲1的對象是從[[scope]]做用域鏈中複製過來的,索引爲0的對象是在函數執行時建立的,第三部分是做用域鏈中的對象的內容Activation Object和Global Object。
函數在運行過程當中,沒遇到一個變量,都會去Execution Context的做用域鏈中從上到下依次搜索,若是在第一個做用域鏈(假如是Activation Object)中找到了,那麼就返回這個變量,若是沒有找到,那麼繼續向下查找,直到找到爲止,這也就是爲何函數能夠訪問全局變量,當局部變量和全局變量同名時,會使用局部變量而不使用全局變量,以及javascript中各類看似怪異的、有趣的做用域問題的答案(你能夠用這種方法來解釋你之前碰到的全部做用域問題,固然,若是仍是有疑問的話,很是但願你能貼出代碼,咱們一塊兒討論。)
通常狀況下,一個函數的做用域鏈是不會在函數運行時被改變的,但有些運算符會臨時改變做用域鏈,with和try catch的catch子句。看下面的例子:
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";
}//eOf with
}
當代碼執行到with時,Execution Context的做用域鏈被臨時改變了,一個新的可變對象被插入到做用域鏈的頂部,這個可變對象包含了with指定的對象的全部屬性。若是此時在with中訪問函數的局部變量,就會先把新插入的可變對象遍歷一遍,而後纔會去Activation Object中查找,直到找到爲止,此時查找效率就會下降(這也是不少人說不要使用with的緣由,我認爲只要設法不影響性能就好了,畢竟訪問with語句指定的對象的屬性仍是很快的,關於性能的問題你們若是想了解的話,能夠關注個人下一篇博文《javascript數據訪問性能》),如圖:
圖3
當try catch語句中try語句塊中的代碼發生錯誤時,會自動跳入catch語句塊,而且會把catch語句指定的異常對象插入到做用域鏈的頂端,但catch有個特色,就是catch子句執行完畢以後,做用域鏈都會返回到原來的狀態。
對於閉包這個經典的話題,網上的前輩高手已經作過不少詳盡的解釋,若是我再過多的說明,顯得有些班門弄斧,不過,對於閉包,理解的角度不一樣,看到的面可能就不同。
這裏咱們從做用域的角度來分析一下閉包產生的方式和特色。
咱們都知道,閉包容許咱們訪問閉包函數做用域以外的做用域內的數據(說簡單點就是能夠閉包容許咱們訪問閉包函數以外的函數的數據。),這是閉包的一個很是強大的功能,不少複雜的網頁應用都和這個特性有關,例如:建立封閉的命名空間、保留外部函數執行環境。
咱們一塊兒來看一個閉包的例子:
function assignEvents(){
var id = "xdi9592";
document.getElementById("save-btn").onclick = function(event){
saveDocument(id);
};
}
上例中,在onclick事件的事件處理器中引用了外部函數assignEvents的局部變量id,造成了閉包,下面咱們看一下它們的做用域圖示:
咱們一塊兒來從做用域的角度分析一下閉包的造成過程:
這也就是閉包爲什麼能「記得」在它周圍到底發生了什麼,爲什麼閉包能訪問外層函數的局部數據,爲什麼閉包能保持這些局部數據而不在外層函數執行完畢銷燬時一塊兒銷燬等等的緣由。
前些天一個前輩(Darrel文叔)告訴我一句話,一針見血:沒有內存,就沒有閉包。
在做用域鏈和閉包中的性能問題主要表如今數據讀寫的速度上。
因爲做用域鏈的緣由,咱們訪問全局做用域的數據(這裏爲何不說變量呢?由於不只包括變量,還有函數,對象等其餘內容)時,效率是最低的,而訪問局部數據時的效率是最高的。
因此一個很是經典的解決數據訪問性能問題的方案出現了:將須要訪問的數據儘可能的以局部數據的方式緩存起來。這樣當標識符解析程序在做用域鏈中尋找數據時,直接就能夠在做用域鏈的最上層找到想要的數據,效率天然就提高了。
這句話能夠解決不少性能問題:設置緩存,將數據保存在局部變量中。
轉載請註明出處:
參考:
4. function fn(){
var i = 0;
(function(){++i;console.log(i)})();
(function(){++i;console.log(i)})();
}
fn();
//1
//2
轉自 紅黑聯盟 http://www.2cto.com/kf/201111/110023.html