javascript閉包—圍觀大神如何解釋閉包

nd發表於2017-03-16

閉包的概念已經出來很長時間了,網上資源一大把,本著拿來主意的方法來看看。

這一篇文章 學習Javascript閉包(Closure) 是大神阮一峰的博文,作者循序漸進,講的很透徹。下面一一剖析。

1.變數的作用域

變數的作用域有區域性和全域性兩種,在javascript的函式內部可以訪問全域性變數,如下:

  // 函式內部可以直接讀取全域性變數
  var n = 99;
  function f1() {
    alert(n);
  }
  f1();

在f1函式中可以訪問到全域性變數n。輸出如下:

反過來就不行了,在函式外部不能讀取函式內部的變數,例如這樣:

  //函式外部無法讀取函式內部的區域性變數,這裡會報錯
  function f2() {
    var m = 99;
  }
  alert(m) //報錯

javascript還有一個比較特殊的地方,在函式內部如果沒有使用var,const,let修飾符宣告變數,那麼這個變數不再是區域性變數而是一個全域性變數,如下:

  function f2() {
    m = 99;
  }
  f2();
  alert(m)

輸出99:

是不是很神奇,但是這個經常給人造成困惑。

 

2.如何從外部讀取函式內部的區域性變數

很多場合下要訪問函式內部的區域性變數,變通的方式是在函式內部定義函式,如下:

  function f3() {
    var a = 999;

    function f4() {
      alert(a);
    }
    return f4
  }
  var result = f3();
  result();

函式f4包含在函式f3裡面,所以f4範圍內可以訪問到f3中的那個變數a,反過來是不行的,Javascript語言特有的"鏈式作用域"結構(chain scope),子物件會一級一級地向上尋找所有父物件的變數。所以,父物件的所有變數,對子物件都是可見的,反之則不成立。

 

3.閉包的概念

可以簡單理解成定義在函式內部的函式,這個內部函式可以把內部函式作用域內的變數傳播到外面。由於在javascript中只有內部的子函式才能讀取區域性變數,因此可以把閉包簡單的理解成“定義在一個函式內部的函式”。本質上,閉包就是將函式內部和函式外部連線起來的橋樑。

 

4.閉包的作用

閉包的第一個用處就是讀取函式內部的變數,另一個作用就是讓這些變數始終儲存在記憶體中。來看下面的程式碼:

  function f5() {
    var b = 111;
    nAdd = function () {
      b += 1;
    }
    function f6() {
      alert(b);
    }
    return f6
  }
  var result1 = f5();
  result1();
  nAdd();
  result1();

上面程式碼兩次彈出框,第一次是輸出111,第二次是112,這就證明函式函式f5內的區域性變數b一直儲存在記憶體中,並沒有在f5呼叫後被自動清除。

這就說明,第一次呼叫result1();的時候給變數b賦值了,然後呼叫全域性函式nAdd的時候變數b仍然還在記憶體中,給他加1就變成112了。原因就在於f5是f6的父函式,而f6被賦給了一個全域性變數,這導致f6始終在記憶體中,而f6的存在依賴於f5,因此f5也始終在記憶體中,不會在呼叫結束後,被垃圾回收機制(garbage collection)回收。

這段程式碼中另一個值得注意的地方,就是"nAdd=function(){n+=1}"這一行,首先在nAdd前面沒有使用var關鍵字,因此nAdd是一個全域性變數,而不是區域性變數。其次,nAdd的值是一個匿名函式(anonymous function),而這個匿名函式本身也是一個閉包,所以nAdd相當於是一個setter,可以在函式外部對函式內部的區域性變數進行操作。

5.注意 

1)由於閉包會使得函式中的變數都被儲存在記憶體中,記憶體消耗很大,所以不能濫用閉包,否則會造成網頁的效能問題,在IE中可能導致記憶體洩露。解決方法是,在退出函式之前,將不使用的區域性變數全部刪除。其實一般不會這樣用!!!
2)閉包會在父函式外部,改變父函式內部變數的值。所以,如果你把父函式當作物件(object)使用,把閉包當作它的公用方法(Public Method),把內部變數當作它的私有屬性(private value),這時一定要小心,不要隨便改變父函式內部變數的值。其實一般不會這樣用!!!

最後作者給出了思考題目,如下:

  var name = 'The Window';
  var object = {
    name: 'My Object',
    getNameFunc: function () {
      // 這裡的this是函式
      console.info(this)
      return function () {
        //匿名函式的執行環境是windows
        console.info(this)
        return this.name;
      }
    }
  };
  alert(object.getNameFunc()());

其實從變數的值已經看到答案了,在物件object內部的函式getNameFunc裡面返回了一個匿名函式這個匿名函式的作用域是window,所以這裡輸出的是'The Window',如下:

我在日誌裡面加上的除錯語句可以看出端倪:

看下面的程式碼:

  var name1 = "The Window";
  var object1 = {
    name1: 'My Object1',
    getNameFunc:function () {
      var that = this;
      return function () {
        return that.name1
      }
    }
  }
  alert(object1.getNameFunc()());

這裡在函式內部使用var that = this語句先把當前上下文的物件儲存下來,在匿名函式中使用that.name1,這樣就是當前物件中的name1,於是輸出了“MyObject1”。其實可以用es6中的箭頭函式,如下:

  var name1 = "The Window";
  var object1 = {
    name1: 'My Object1',
    getNameFunc:function () {
      return  () => {
        return this.name1
      }
    }
  }
  alert(object1.getNameFunc()());

箭頭函式會繫結object1的作用域,於是仍任是object1的屬性name1,和上面輸出的結果一樣。

6.深入的理解

 在知乎上也有人在討論這個問題,知乎你懂的,比較嚴謹,如何通俗易懂的解釋javascript裡面的‘閉包’?這一篇問答裡有人給出了其他的解釋。

 1.每次定義一個函式,都會產生一個作用域鏈(scope chain)。當JavaScript尋找變數varible時(這個過程稱為變數解析),總會優先在當前作用域鏈的第一個物件中查詢屬性varible ,如果找到,則直接使用這個屬性;否則,繼續查詢下一個物件的是否存在這個屬性;這個過程會持續直至找到這個屬性或者最終未找到引發錯誤為止。
------有道理,關鍵是弄懂這個作用域鏈,說白了就是花括號的層級及各種函式的的上下文作用域,比如在函式中定義變數不用var,let它居然是全域性的,這個是javascript比較特殊的地方,強型別語言估計早就報錯了。

2.JavaScript中的函式執行在它們被定義的作用域裡,而不是它們被執行的作用域裡。
------有道理,上面的最後一個程式碼段中的例子,本來執行object1.getNameFunc()()這一句的時候,執行作用域中的name1是var name1 = "The Window";這個,但是彈出來的確實定義getNameFunc這個函式的作用域內的name1: 'My Object1',

3.子函式能夠訪問父函式的區域性變數,反之則不行。而那個子函式就是閉包!
------有道理,就是上面阮大師反覆說明的

好了,就這麼多了。

 

相關文章