從執行上下文深刻理解閉包

1.概念

關於閉包的定義我看到過好多個版本,這裏簡單的列舉一下:
MDN:包是函數和聲明該函數的詞法環境的組合。(PS:我的理解詞法環境就是變量對象)
Tyler McGinnis:子函數在其父級函數的變量環境上「關閉」(譯者注:原文爲a child function 「closing」 over the variable environment of its parent function)的概念,就叫作閉包。
w3school:閉包,指的是詞法表示包括不被計算的變量的函數,也就是說,函數可使用函數以外定義的變量。
阮一峯:他的理解是,閉包就是可以讀取其餘函數內部變量的函數。因爲在Javascript語言中,只有函數內部的子函數才能讀取局部變量,所以能夠把閉包簡單理解成"定義在一個函數內部的函數"。
一次性搞懂 JavaScript 閉包 —— 簡書:閉包簡單來講就是一個函數訪問了它的外部變量。javascript

還有《JavaScript高級編程語言》,《JavaScript權威指南》親有沒有發現每一個人感受都給閉包有一個定義,若是你是一個小白你必定和我同樣的鬱悶( ˇˍˇ )。
最近了解了一下JavaScript執行上下文以後才忽然發現這麼多概念原來講的其實都是一件事。若是你看了以後和我當初同樣,但願我下面的內容能夠幫助你,進入正題。html

2.執行上下文(或者叫做用域)

執行上下文是用來幫助Javascript引擎管理整個解析和運行代碼的複雜過程。那麼如今咱們瞭解了執行上下文的存在目的,下一個問題就是執行上下文是怎麼建立的?它們由什麼組成?java

概念:當且僅當Javascript引擎首次開始解析代碼(對應全局執行上下文)或當一個函數被調用時,纔會建立執行上下文。git

全局執行上下文:當Javascript引擎運行代碼,第一個被建立的執行上下文叫作「全局執行上下文」。最初,這個全局上下文由這二位組成:一個全局對象和一個this變量。this引用的是全局對象,若是在瀏覽器中運行Javascript,那麼這個全局對象就是window對象,若是在Node環境中運行,這個全局對象就是global對象。 在全局執行上下文的建立階段,Javascript引擎會:github

  1. 建立一個全局對象;
  2. 建立this對象,指向window;
  3. 給函數分配內存;
  4. 給變量分配內存;
  5. 給變量賦默認值undefined,把全部函數聲明放進內存。

函數執行上下文:當函數被調用,它就被建立出來了。函數執行上下文中應該建立的應該是arguments對象,因此當建立函數執行上下文時,Javascript引擎會:編程

  1. 1.建立一個全局對象
  2. 建立一個arguments對象;
  3. 建立this對象,指向函數調用對象;
  4. 給函數分配內存;
  5. 給變量(包括內部定義的變量和參數變量)分配內存;
  6. 給變量賦默認值undefined,把全部函數聲明放進內存。

關於變量對象的建立有什麼疑問能夠看看JavaScript深刻之變量對象 json

舉個栗子:咱們來講明一下:
實際操做瀏覽器

這裏有幾處重要細節須要注意。首先,傳入函數的全部參數都做爲局部變量存在於該函數的執行上下文中。在例子中,handle同時存在與全局執行上下文和getURL執行上下文中,由於咱們把它傳入了getURL函數作爲參數。其次,在函數中聲明的變量存在於函數的執行上下文中。閉包

做用域鏈:Javascript中一切皆對象,這些對象有一個[[Scope]]屬性,該屬性包含了函數被建立時的做用域中對象的集合,這個集合被稱爲函數的做用域鏈(Scope Chain),它決定了哪些數據能被函數訪問。當函數建立的時候,它的[[scope]]屬性自動添加好全局做用域。之因此要強調建立是由於JavaScript採用詞法做用域(lexical scoping),也就是靜態做用域.編程語言

舉個栗子:咱們來經過簡單的代碼說明一下做用域鏈:
實際操做

function a () {
  console.log('In fn a')
  function b () {
    console.log('In fn b')
    function c () {
      console.log('In fn c')
    }
    c()
  }
  b()
}

a()
複製代碼

從圖中能夠清楚的發如今函數的執行過程當中,最開始建立了一個全局執行上下文,而後沒執行一個函數就會建立一個函數執行上下文,當開始執行函數 C() 的時候 C 函數有一個[[scope]]屬性,裏面的值會是:

//c的做用域鏈
[
 0:{
     arguments:{length:0},
     this:window
 },
 1:{
     arguments:{length:0},
     this:window,
     c:fn()
 }
 2:{
     arguments:{length:0},
     this:window,
     b:fn()
 },
 3:{
     this:window,
     a:fn()
 }
]
複製代碼

細心觀察你會發現每一個函數執行完以後,每一個函數的執行上下文會消失,事實上,Javascript引擎建立了一個叫「執行棧」(也叫調用棧)的東西。每當函數被調用,就建立一個新的執行上下文並把它加入到調用棧;每當一個函數運行完畢,就被從調用棧中彈出來。因此「一般狀況下」函數執行完畢後函數的執行上下文就會消失,閉包就是否是「一般狀況下」。

問題來了什麼叫作詞法做用域(也能夠說靜態做用域)?

咱們在舉個栗子:

var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar();

// 結果是 ???
複製代碼

答案是 1 (小夥伴你答對了麼?)
解釋:因也很簡單,由於JavaScript採用的是詞法做用域(若是不明白能夠看看《JavaScript深刻之詞法做用域和動態做用域》),函數的做用域基於函數建立的位置。函數foo() 定義在全局做用域下,當打印value時沿着做用於鏈查找就找到了全局執行上下文,而不是bar函數執行上下文。因此結果是1。

而引用《JavaScript權威指南》的回答就是: JavaScript 函數的執行用到了做用域鏈,這個做用域鏈是在函數定義的時候建立的。嵌套的函數 f() 定義在這個做用域鏈裏,其中的變量 scope 必定是局部變量,無論什麼時候何地執行函數 f(),這種綁定在執行 f() 時依然有效。

3.閉包

閉包就是否是「一般狀況下」,若是你在一個函數中嵌入了另外一個函數,而且讓外部一個指針引用內部的函數,例外狀況就產生了。這種函數套函數的狀況下,即便父級函數的執行上下文從調用棧彈出了,子級函數仍然可以訪問父級函數的做用域。
實際操做

makeAdder執行上下文從調用棧彈出後,Javascript Visualizer建立了一個Closure Scope(閉包做用域)。Closure Scope中的變量環境和makeAdder執行上下文中的變量環境相同。這是由於咱們在函數中嵌入了另外一個函數。在本例中,inner函數嵌在makeAdder中,因此inner在makeAdder變量環境的基礎上建立了一個閉包。由於閉包做用域的存在,即便makeAdder已經從調用棧彈出了,inner仍然可以訪問到x變量(經過做用域鏈)。

如今是否是感受本身明白了一點什麼是閉包呢?反正閉包的定義我還下不了,可是我仍是要粗略的表達一下我本身的想法就是:閉包就是使一個函數做爲另外一個函數的返回,從而達到內部函數能夠讀取外部函數內部的變量和讓外部函數中的變量的值始終保持在內存中的做用的一個寫法。(PS.不知道你們可不能夠接受,不喜勿噴!!!)

4.小檢驗

function func() {
    var arr = [];
    for(var i = 0;i<3;i++){
        arr.push(()=> {
            console.log(i);
        })
    }
    return arr
}
var result = func();
result.forEach((item)=> {
    item();
})
複製代碼

輸出結果是:3 3 3 有沒有答對呢 ?
解釋:首先在func函數執行上下文中建立了 i 變量(這裏涉及到變量提高的知識,不瞭解本身能夠看一下),當執行匿名的函數要console.log(i)的時候發如今改匿名函數的執行上下文沒有這個變量,則沿着做用域鏈向上查找,發如今func的做用域中有i,這個i的值是3(for循環最後結束後i記錄爲3)。

咱們使用閉包進行以下修改,親,你再猜猜?

function func() {
    var arr = [];
    for(var i = 0;i<3;i++){
        (function(){
            arr.push(()=> {
                console.log(i);
            })
        })()
    }
    return arr
}
var result = func();
result.forEach((item)=> {
    item();
})
複製代碼

答案是 3 3 3,解釋和上面的同樣。若是你想輸出 0 ,1 ,2 有兩種方案:閉包和使用let

//方案一
function func() {
    var arr = [];
    for(var i = 0;i<3;i++){
        (function(i){
            arr.push(()=> {
                console.log(i);
            })
        })(i)
    }
    return arr
}
var result = func();
result.forEach((item)=> {
    item();
})
//輸出 0 1 2

//方案二
function func() {
    var arr = [];
    for(let i = 0;i<3;i++){
        arr.push(()=> {
            console.log(i);
        })
    }
    return arr
}
var result = func();
result.forEach((item)=> {
    item();
})
//輸出 0 1 2
複製代碼

5.使用閉包的注意點

  1. 因爲閉包會使得函數中的變量都被保存在內存中,內存消耗很大,因此不能濫用閉包,不然會形成網頁的性能問題,在IE中可能致使內存泄露。解決方法是,在退出函數以前,將不使用的局部變量所有刪除。

  2. 閉包會在父函數外部,改變父函數內部變量的值。因此,若是你把父函數看成對象(object)使用,把閉包看成它的公用方法(Public Method),把內部變量看成它的私有屬性(private value),這時必定要當心,不要隨便改變父函數內部變量的值。

6.閉包運行機制

思考題:
代碼一

var name = "The Window";
  var object = {
    name : "My Object",

    getNameFunc : function(){
      return function(){
        return this.name;
      };

    }
  };
  alert(object.getNameFunc()());//The Window
複製代碼

代碼二

var name = "The Window";
  var object = {
    name : "My Object",

    getNameFunc : function(){
      var that=this;
      return function(){
        return that.name;
      };

    }
  };
  alert(object.getNameFunc()());//My Object
複製代碼

javascript 中this的定義:就是上下文對象,即被調用函數所處的環境,也就是說,this 在函數內部指向了調用函數的對象。若是沒有搞懂就去研究一下javascript的this吧

7.引用

  1. 【譯】終極指南:變量提高、做用域和閉包
  2. 閉包的錯誤使用
  3. 學習Javascript閉包(Closure)--阮一峯
  4. 一次性搞懂JavaScript閉包--簡書
  5. 高效使用 JavaScript 閉包
  6. JavaScript深刻之詞法做用域和動態做用域
  7. JavaScript深刻之變量對象

結束語

後面發現好的閉包的內容我還會加進來,若是有什麼不對的地方歡迎指正。

相關文章
相關標籤/搜索