javascript作用域、閉包原理以及性能問題

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

文章內容主要參考自《High Performance JavaScript》,這本書對javascript性能方面確實講的比較深入,大家有空都可以嘗試着閱讀一下,下載地址:中英電子版

複習,筆記,更深入的理解。

歡迎拍磚指正。

作用域:

下面我們先搞明白這樣幾個概念:

  • 函數對象的[[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;
        }

 

圖 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 assignEvents() {
            var id = "xdi9592";
            document.getElementById("save-btn").onclick = function (event) {
                saveDocument(id);
            };
        }

 

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

image 圖 4

我們一起來從作用域的角度分析一下閉包的形成過程:

  1. assignEvents函數創建,詞法解析後,函數對象assignEvents的[[scope]]屬性被初始化,作用域鍊形成,作用域鏈中包含了全局對象的所有屬性和方法(注意,此時因爲assignEvents函數並未執行,所以閉包函數並沒有被解析)。
  2. assignEvents函數執行,在開始執行時,創建Execution Context(我們將圖4按照從左到右,從上到下的順序劃分爲6部分,第一部分就是運行期上下文),在運行期上下文的作用域鏈中創建Activation Object(第二、三部分),並將Activation Object放置與作用域鏈頂點,在其中保存了函數執行時所有可訪問函數內部的數據。
  3. 當執行到閉包時,javascript引擎發現了閉包函數的存在,按照通常的手法,將閉包函數解析,爲閉包函數對象創建[[scope]]屬性,初始化作用域鏈(此時閉包函數對象的作用域鏈中有兩個對象,一個是assignEvents函數執行時的Activation Object,還有一個是全局對象,圖4的4、5、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

  5. 我們發現變量i從1變爲了2,說明兩個閉包引用的是同一個變量i,也就說明他們引用的fn的Activation Object是同一個,其實完全可以換一種非常簡單的方式來解釋:全局對象肯定是同一個吧?
  1. 下面討論當閉包函數執行時的情況,因爲在詞法分析的時候閉包函數就已經在作用域鏈中保存了對assignEvents函數的Activation Object的引用,所以當assignEvents函數執行完畢之後,閉包函數雖然還沒有開始執行,但依然可以訪問assignEvents的局部數據(並不是因爲閉包函數要訪問assignEvents的局部變量id,所以當assignEvents函數執行完畢之後依然保持了對局部變量id的引用。而是不管是否存在變量引用,都會保存對assignEvents的Activation Object作用域對象的引用。因爲在詞法分析時,閉包函數沒有執行,函數內部根本就不知道是否要對assignEvents的局部變量進行訪問和操作,所以只能先把assignEvents的Activation Object作用域對象保存起來,當閉包函數執行時,如果需要訪問assignEvents的局部變量,那麼再去作用域鏈中搜索)。
  2. 閉包函數執行時創建了自己的Execution Context和Activation Object,在運行期上下文的作用域鏈中保存了自己的Activation Object,外層函數assignEvents的Execution Context的Activation Object,以及Global Object,如圖:

image 圖 5

這也就是閉包爲何能“記得”在它周圍到底發生了什麼,爲何閉包能訪問外層函數的局部數據,爲何閉包能保持這些局部數據而不在外層函數執行完畢銷燬時一起銷燬等等的原因。

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

性能問題:

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

由於作用域鏈的原因,我們訪問全局作用域的數據(這裏爲什麼不說變量呢?因爲不僅包括變量,還有函數,對象等其他內容)時,效率是最低的,而訪問局部數據時的效率是最高的。

所以一個非常經典的解決數據訪問性能問題的方案出現了:將需要訪問的數據儘量的以局部數據的方式緩存起來。這樣當標識符解析程序在作用域鏈中尋找數據時,直接就可以在作用域鏈的最上層找到想要的數據,效率自然就提升了。

這句話可以解決很多性能問題:設置緩存,將數據保存在局部變量中。

 

發佈了8 篇原創文章 · 獲贊 8 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章