關於做用域閉包的一些理解

閉包

紅寶書上對閉包的定義:有權訪問另一個函數做用域中變量的函數。html

MDN對閉包的定義是:是可以訪問自由變量的函數。算法

自由變量:是指在當前函數中可使用的(可是既不是arguments也不是本函數定義的局部變量)。瀏覽器

兩個點:閉包

  1. 是個函數
  2. 能訪問另外一個函數做用域中的變量,即便外層函數的上下文已經被銷燬

就是說咱們常見的好比內部函數從外部函數返回這種狀態,該內部函數就是閉包。能夠看以下特性中的示例!函數

說明閉包的幾個特性:優化

  • 能夠訪問當前函數之外的變量
  • function outer() {
                var date = '11月1日'; function inner(str) { console.log(str + date) } return inner('today is ') } outer() function outer() { var date = '11月1日'; return function () { console.log('today is ' + date) }() } outer() // 上下兩例均返回「today is 11月1日」
  • 即便外部函數已經返回,閉包仍然可以訪問外部定義的變量
  • function outer() {
                var date = '11月1日'; function inner() { console.log('today is ' + date) } return inner } // 如下是拆成分步執行,實際等同於outer()();
    // 先執行outer()獲得一個返回值inner,此時outer函數執行完畢,跳出outer這個外層函數
    // 而後執行inner(),可是此時依然可用outer定義的變量date var getDate = outer() getDate()
  • 閉包能夠修改外部函數的變量的值
  • function outer() {
                var date = '11月1日'; function inner(newDate) { date = newDate // 將傳入的值替換掉外層的date console.log('today is ' + date) } return inner } var getDate = outer() getDate('191101') // 「today is 191101」

閉包的做用域鏈:

如下例分析:this

        var scope = 'global';
        function checkscope() { var scope = 'local'; function fun() { return scope; } return fun; } var check = checkscope(); console.log(check()); // 'local'

執行過程:spa

  1. 進入全局代碼,建立全局執行上下文並壓入執行上下文棧
  2. 全局上下文初始化
  3. 執行checkscope函數,建立checkscope執行上下文並將其壓入執行棧
  4. checkscope函數上下文初始化,建立變量對象、做用域鏈、this
  5. checkscope函數執行,執行完後checkscope執行上下文從執行棧中彈出
  6. 因爲checkscope函數返回了一個f函數,所以建立f()執行上下文,將fun()的執行上下文壓入執行棧,而後執行fun(),一樣的,建立變量對象、做用域鏈、this
  7. fun()執行完畢後,從執行棧中彈出。

這個流程能夠看到,checkscope執行完畢後是帶着返回值彈出了執行棧的,在fun執行的時候checkscope函數的上下文已經被銷燬了,可是,函數fun執行上下文維護了一個做用域鏈,結構以下:code

funContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

指向關係是:當前做用域 -> checkscope -> 全局,不論checkscope 是否被銷燬,fun函數均可以經過fun的做用域鏈找到它,這是閉包實現的關鍵htm

有關全局環境下函數嵌套與非嵌套時做用域鏈的指向分析,參考:深刻淺出圖解做用域鏈和閉包

 

一些常見的閉包考題

題1:

        var data = [];

        for (var i = 0; i < 3; i++) {
            data[i] = function () {
                console.log(i);
            };
        }

        data[0]();  
        data[1]();
        data[2]();

答案很顯然:三、三、3

上面這個題的分析參照個人另外一篇博客分析:let和const,裏面詳細的分析了這個題的過程~

如何才能讓這個題輸出咱們想要的0、一、2呢?

解法一:博客當中給出了使用let的寫法,很簡單隻須要將for中的var替換成let便可

解法二:

還有別的方法嗎?本片將採用閉包的方式解決這個問題~~~回想一下閉包的狀態是什麼?內層函數可使用外層函數定義的變量呀!

因此第一步:咱們在function中return一個新的函數,在內層函數中訪問變量 i。

而後考慮咱們如何才能把當前的i傳到外層function中呢?馬上咱們聯想到利用參數!

第二步:外層函數自執行,將 i 做爲參數傳入外層的function中

所以獲得以下優化後的代碼:

        var data = [];

        for (var i = 0; i < 3; i++) {
            data[i] = (function (i) {
                return function () {
                    console.log(i);
                }
            })(i)
        }

        data[0]();
        data[1]();
        data[2]();

結果爲:0、一、2

正是咱們要的結果啦~

解法三:

這裏還能夠改爲咱們常見的定時器寫法

        for (var i = 0; i < 3; i++) {
            (function (i) {
                setTimeout(function () {
                    console.log(i)
                }, 100 * i)
            })(i)
        }

解法三其實和解法二的本質相同,都是將變量 i 的值複製給外層function的參數 i ,在函數內部又建立一個用於訪問 i 的匿名函數,這樣每一個函數都有一個 i 的副本,就不會相互影響!

題2:這兩段代碼在checkscope執行完後,f所引用的自由變量scope會被垃圾回收嗎?why?

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

checkscope()();  

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope(); 
foo();   

結論是:第一個代碼段中的scope特定時間後會被回收,第二段代碼的自由變量不會回收

分析:

如今主流瀏覽器的垃圾回收算法是:標記清除。當垃圾回收開始時,從root開始尋找這個對象的引用是否可達,也就是找是否存在相互引用,若是引用鏈斷裂,那麼這個對象就能夠被回收!

對於第一段代碼,checkscope()執行完畢後被彈出執行棧,而且也沒有其餘引用,Root開始查找時不可達,所以閉包引用的自由變量scope過段時間能夠被回收

對於第二段代碼,因爲var foo = checkscope(),checkscope()執行完成後,將foo()執行上下文壓入執行棧,foo()指向堆中的自由變量 f ,對於Root來講可達,所以不會被回收!!

若是想要scope必定能夠被回收,只要加:foo = null;便可!

相關文章
相關標籤/搜索