js中的閉包

Alice_Xu發表於2018-04-11

在上一篇文章“執行環境和作用域”中,我試著梳理了執行環境和作用域的關係。但實際上,文章中並沒有提到作用域,而是介紹了執行環境和作用域鏈。這裡先把上篇文章的坑填了。

上篇文章的最後,我提了一個問題。這裡把程式碼稍微修改下,如下:

var name = 'window';

outer();

function outer(){
    var name = 'outer';
    inner();    //輸出什麼?
}

function inner(){
    console.log(name);
}複製程式碼

這段程式碼很簡單,但很容易迷惑人。很多人可能會認為,應該輸出“outer”,但實際上結果確實“window”。這是為什麼呢?

1、作用域和執行上下文的區別

作用域和執行上下是兩個不同的概念。執行上下文在上篇文章中已經解釋過了:在函式執行時建立。那麼,作用域怎麼理解呢?

作用域可以理解為一套規則,這套規則用來管理引擎如何在當前作用域以及巢狀的子作用域中根據識別符號名稱進行變數查詢。同執行環境一樣,作用域只有兩種(不考慮eval):全域性作用域與函式作用域。

在js中程式碼整個的執行分為兩個階段:程式碼編譯和程式碼執行。程式碼編譯由編譯器完成,將程式碼翻譯成可執行程式碼。程式碼執行由js引擎完成,主要任務是執行可執行的程式碼。在程式碼編譯階段,作用域規則就已經被確定了。到程式碼執行時,執行上下文被建立,同時,作用域鏈作為作用域規則的具體實現被構建出來。過程如下圖:

js中的閉包
0.jpg

再回頭去看開頭的問題,就不難理解了:在編譯階段,inner函式的相關的作用域規則就已經確定了,在而outer函式中執行時,只是具體地實現了相關的作用域的規則,也就是構建作用域鏈,而這個作用域鏈上面沒有outer函式執行環境相對應的變數物件,而是有全域性執行環境對應的window物件,因此,結果是‘window’。

2、閉包

閉包在js高程中的解釋是:有權訪問另一個函式作用域中的變數的函式。簡單說就是,假設函式a是定義在函式b中的函式,那麼函式a就是一個閉包。正常情況下,在函式的外部訪問不到函式內部的變數,但有了閉包就可以間接的實現訪問內部變數的需要。也就是說,閉包是連線函式內部和外部的橋樑。這就是閉包的第一個作用:訪問函式內部的變數。還有另外一個作用就是:讓被引用的變數值始終保持在記憶體中。

在上一篇文章中提到程式碼當執行到一個函式時,會建立一個臨時的活動物件,並把這個物件作為變數物件推入環境棧中。當這個函式執行完的時候,這個物件就會出桟,並被銷燬。但當閉包中引用了函式中的變數時,那麼,這個變數就會儲存在記憶體中。也就是上面提到的閉包的第二個作用。之所以為這樣,是因為JavaScript的回收機制。

基本所有瀏覽器都是使用“標記清除”的方式回收記憶體。也就是說,當變數進入執行環境的時候(在函式中宣告一個變數),就給變數新增標記,而當函式執行完的,變數不再被引用的時候,再新增刪除的標記,垃圾收集器就會自動清楚這個變數佔有的記憶體。但在閉包中引用了函式中的變數,而閉包又被當作結果返回時,閉包中的因為被引用就不會被清除。例如,下面的程式碼:

function fn1(){
    var a = 1;
    return function(){
        console.log(++a);
    }

}

var fn2 = fn1();

fn2();        //輸出2

fn2();        //輸出3複製程式碼

在這段程式碼中,fn1中的閉包函式被當作結果返回,在閉包中的引用的變數a因為被引用而沒有被清除,一直儲存在記憶體當中,所以執行fn2的時候會輸出不斷增加的結果:2和3。

使用閉包時需要注意的問題

1、由於閉包會使得函式中被引用的變數一直儲存在記憶體中,消耗記憶體,所以謹慎使用閉包,否則會造成網頁的效能問題。
2、閉包會改變父函式內部變數的值。如果父函式再次被執行的,而在外部已經執行過閉包修改變數的值,那麼,這次執行的結果就會和上次的不一樣。

最後再來一段程式碼,想下輸出的程式碼,沒疑問的化,閉包的執行機制基本就掌握了:

var name = "window";

var obj = {
  name : "obj",
  getName : function(){
    return function(){
      return this.name;
    };
  }
};
  
console.log(obj.getName()());複製程式碼

寫在結尾:
如果覺得我寫的文章對你有幫助,歡迎掃碼關注我的公眾號:海痕筆記
微訊號:haihenbiji

js中的閉包

相關文章