舉例詳細說明javascript做用域、閉包原理以及性能問題(轉)

這多是每個jser都曾經爲之頭疼的卻又很是經典的問題,關係到內存,關係到閉包,關係到javascript運行機制。關係到功能,關係到性能。javascript

文章內容主要參考自《High Performance JavaScript》,這本書對javascript性能方面確實講的比較深刻,你們有空均可以嘗試着閱讀一下,我這裏有中英電子版,須要的話QQ317665171或者QQ郵箱聯繫。html

複習,筆記,更深刻的理解。java

歡迎拍磚指正。express

做用域:

下面咱們先搞明白這樣幾個概念:緩存

  • 函數對象的[[scope]]屬性、ScopeChain(做用域鏈)
  • Execution Context(運行期上下文)、Activation Object(激活對象)

[[scope]]屬性:網絡

javascript中每一個函數都是一個函數對象(函數實例),既然是對象,就有相關的屬性和方法。[[scope]]就是每一個函數對象都具備的一個僅供javascript引擎內部使用的屬性,該屬性是一個集合(相似於鏈表結構),集合中保存了該函數在被建立時的做用域中的全部對象,而這個做用域集合造成的鏈表則被稱爲ScopeChain(做用域鏈)。閉包

該做用域鏈中保存的做用域對象,就是該函數能夠訪問的全部數據。例如(例子引用自《High Performance JavaScript高性能javascript》):函數

 

       function add(num1, num2){性能

                     var sum = num1 + num2;學習

                     return sum;

                     }

      

 

	image
		圖 1

當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就會以另一種方式存在,這也是閉包產生的真正緣由,具體的咱們稍後討論。)。具體狀況如圖所示:

image

圖 2

咱們從左往右看,第一部分是函數執行時建立的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數據訪問性能》),如圖:

image

圖3

當try catch語句中try語句塊中的代碼發生錯誤時,會自動跳入catch語句塊,而且會把catch語句指定的異常對象插入到做用域鏈的頂端,但catch有個特色,就是catch子句執行完畢以後,做用域鏈都會返回到原來的狀態。

閉包:

  • A " closure" is an expression (typically a function) that can have free varuables together with an environment that binds those variables (that "closes" the expression). —— ECMA262
  • 「閉包」是一個表達式(通常是函數),它具備自由變量以及綁定這些變量的環境(該環境「封閉了」這個表達式)。—— 李鬆峯
  • 閉包就是可以讀取其餘函數內部變量的函數。——阮一峯

對於閉包這個經典的話題,網上的前輩高手已經作過不少詳盡的解釋,若是我再過多的說明,顯得有些班門弄斧,不過,對於閉包,理解的角度不一樣,看到的面可能就不同。

這裏咱們從做用域的角度來分析一下閉包產生的方式和特色。

咱們都知道,閉包容許咱們訪問閉包函數做用域以外的做用域內的數據(說簡單點就是能夠閉包容許咱們訪問閉包函數以外的函數的數據。),這是閉包的一個很是強大的功能,不少複雜的網頁應用都和這個特性有關,例如:建立封閉的命名空間、保留外部函數執行環境。

咱們一塊兒來看一個閉包的例子:

	function assignEvents(){
	var id = "xdi9592";
	document.getElementById("save-btn").onclick = function(event){
	saveDocument(id);
	};
	}

上例中,在onclick事件的事件處理器中引用了外部函數assignEvents的局部變量id,造成了閉包,下面咱們看一下它們的做用域圖示:

image圖 4

咱們一塊兒來從做用域的角度分析一下閉包的造成過程:

image圖 5

這也就是閉包爲什麼能「記得」在它周圍到底發生了什麼,爲什麼閉包能訪問外層函數的局部數據,爲什麼閉包能保持這些局部數據而不在外層函數執行完畢銷燬時一塊兒銷燬等等的緣由。

前些天一個前輩(Darrel文叔)告訴我一句話,一針見血:沒有內存,就沒有閉包。

性能問題:

在做用域鏈和閉包中的性能問題主要表如今數據讀寫的速度上。

因爲做用域鏈的緣由,咱們訪問全局做用域的數據(這裏爲何不說變量呢?由於不只包括變量,還有函數,對象等其餘內容)時,效率是最低的,而訪問局部數據時的效率是最高的。

因此一個很是經典的解決數據訪問性能問題的方案出現了:將須要訪問的數據儘可能的以局部數據的方式緩存起來。這樣當標識符解析程序在做用域鏈中尋找數據時,直接就能夠在做用域鏈的最上層找到想要的數據,效率天然就提高了。

這句話能夠解決不少性能問題:設置緩存,將數據保存在局部變量中。

轉載請註明出處:

參考:

  1. assignEvents函數建立,詞法解析後,函數對象assignEvents的[[scope]]屬性被初始化,做用域鏈造成,做用域鏈中包含了全局對象的全部屬性和方法(注意,此時由於assignEvents函數並未執行,因此閉包函數並無被解析)。
  2. assignEvents函數執行,在開始執行時,建立Execution Context(咱們將圖4按照從左到右,從上到下的順序劃分爲6部分,第一部分就是運行期上下文),在運行期上下文的做用域鏈中建立Activation Object(第2、三部分),並將Activation Object放置與做用域鏈頂點,在其中保存了函數執行時全部可訪問函數內部的數據。
  3. 當執行到閉包時,javascript引擎發現了閉包函數的存在,按照一般的手法,將閉包函數解析,爲閉包函數對象建立[[scope]]屬性,初始化做用域鏈(此時閉包函數對象的做用域鏈中有兩個對象,一個是assignEvents函數執行時的Activation Object,還有一個是全局對象,圖4的四、五、6部分。)。咱們看到圖中閉包函數對象的做用域鏈和assignEvents函數的執行上下文做用域鏈相同?爲何相同呢?咱們來分析一下,閉包函數是在assignEvents函數執行的過程當中被發現而且解析的,而函數執行時的做用域是Activation Object,那麼結果就很明顯了,閉包函數被解析的時候它的做用域正是assignEvents做用域鏈中的第一個做用域對象Activation Object,固然,因爲做用域鏈的關係,全局對象做用域也被引入到閉包函數的做用域鏈中。     那麼咱們如今考慮另外一個問題,閉包做用域鏈中的Activation Object,是引用了assignEvents函數的Activation Object,仍是拷貝了一個副本到閉包的做用域鏈中了?咱們能夠作一個小的測試,在有多個閉包同時引用外層函數局部變量(i)的狀況下,若是其中一個閉包改變了i的內容,而其餘閉包中的i的內容沒有發生改變,則說明產生了拷貝,反之,則引用了同一個Activation Object。

 

 

4. function fn(){

                     var i = 0;

                     (function(){++i;console.log(i)})();

                     (function(){++i;console.log(i)})();

                     }

                      

                     fn();

                     //1

                     //2

  1. 咱們發現變量i從1變爲了2,說明兩個閉包引用的是同一個變量i,也就說明他們引用的fn的Activation Object是同一個,其實徹底能夠換一種很是簡單的方式來解釋:全局對象確定是同一個吧?
  2. 下面討論當閉包函數執行時的狀況,由於在詞法分析的時候閉包函數就已經在做用域鏈中保存了對assignEvents函數的Activation Object的引用,因此當assignEvents函數執行完畢以後,閉包函數雖然尚未開始執行,但依然能夠訪問assignEvents的局部數據(並非由於閉包函數要訪問assignEvents的局部變量id,因此當assignEvents函數執行完畢以後依然保持了對局部變量id的引用。而是不論是否存在變量引用,都會保存對assignEvents的Activation Object做用域對象的引用。由於在詞法分析時,閉包函數沒有執行,函數內部根本就不知道是否要對assignEvents的局部變量進行訪問和操做,因此只能先把assignEvents的Activation Object做用域對象保存起來,當閉包函數執行時,若是須要訪問assignEvents的局部變量,那麼再去做用域鏈中搜索)。  
  3. 閉包函數執行時建立了本身的Execution Context和Activation Object,在運行期上下文的做用域鏈中保存了本身的Activation Object,外層函數assignEvents的Execution Context的Activation Object,以及Global Object,如圖:
相關文章
相關標籤/搜索