【機制】js的閉包、執行上下文、做用域鏈

1.從閉包提及

什麼是閉包html

一個函數和對其周圍狀態(詞法環境)的引用捆綁在一塊兒,這樣的組合就是閉包。
也就是說,閉包讓你能夠在一個內層函數中訪問到其外層函數的做用域。
在 JavaScript 中,每當建立一個函數,閉包就會在函數建立的同時被建立出來。git

上面是MDN對閉包的解釋,這幾句話可能不太好懂,不要緊,咱們先來看下能懂的:github

  • 閉包是和函數有關
  • 這個函數能夠訪問它外層函數的做用域
  • 從定義看,每一個函數均可以稱爲閉包

雖然從定義來看,全部函數均可以稱爲閉包,可是當咱們在討論它的時候,通常是指這種狀況:瀏覽器

//code-01
    function cat() {
      var name = "小貓";
      function say() {
        console.log(`my name is ${name}`);
      }
      return say;
    }
    var fun = cat();
    //---cat函數已經執行完,下面卻還可以訪問到 say函數的內部變量 name
    fun();
    //> my name is 小貓

當一個函數的返回值是一個內部函數時(cat函數返回say函數),在這個函數已經執行完畢後,這個返回的內部函數還能夠訪問到已經執行完畢的函數的內部變量,就像 code-01中fun能夠訪問到cat函數的name,通常咱們談論的閉包就是指這種狀況。
那麼這是什麼緣由呢?這就涉及到函數的做用域鏈執行上下文的概念了,咱們下面分別來講。閉包

2.執行上下文

定義
什麼是執行上下(Execution context )呢?簡單來講就是全局代碼或函數代碼執行的時候的環境,它包含三個部份內容:函數

  • 1.變量對象(Variable object,vo),
  • 2.做用域鏈(Scope chain,sc)
  • 3.this的指向(這篇先不談)

咱們用一個對象來表示:oop

EC = {
      vo:{},
      sc:[],
      this
  }

而後代碼或函數須要什麼變量的時候,就會在這裏面找。this

建立時間
執行上下文(EC)是何時建立的呢?這裏分爲兩種狀況:code

  • 全局代碼:代碼開始執行,可是尚未執行具體代碼以前
  • 函數代碼:函數要執行的時候,可是還沒值執行具體代碼以前

其實若是把全局的代碼理解爲一個大的函數,這二者就能夠統一了。
每個函數都會建立本身的執行上下文,他們以棧的形式存儲在一塊兒,當函數執行完畢,則把它本身的執行上下文出棧,這就叫執行上下文棧(Execution context stack,ECS)
下面咱們經過一段代碼實例來看一下htm

聲明語句與變量提高
具體分析以前,咱們先來講聲明語句,什麼是聲明語句呢?

  • 聲明語句是用來聲明一個變量,函數,類的語句
  • 好比:var,let,const,function,class
  • 其中 var 和 function 會形成變量提高,其餘不會,若是var和function同名的話,則函數聲明優先
    那什麼是變量提高呢?
// code-02
    console.log(varVal); // 輸出undefined
    console.log(fun); // 輸出  fun(){console.log('我是函數體') },
    //console.log(letVal) //報錯 letVal is not defined

    var varVal = "var 聲明的變量";
    let letVal = "let 聲明的變量";

    function fun() {
      console.log("我是函數體");
    }
    var fun = "function"; //與函數同名,函數優先,可是能夠從新賦值

    console.log(varVal); // >> "var 聲明的變量"
    console.log(letVal); // >> "let 聲明的變量"
    //fun(); // 報錯,由於fun被賦值爲'function'字符串了

    var name = "xiaoming";

在js執行代碼的時候,會先掃一遍代碼,把var,function的聲明先執行,var聲明的變量會先賦值爲undefined,function聲明的函數會直接就是函數體,這就叫變量提高,而其餘的聲明,好比let,則不會。
因此在變量賦值以前,console.log(varVal),console.log(fun)能夠執行,而console.log(letVal)則會報錯。
其中fun被從新聲明爲'function'字符串,可是在變量提高的時候,函數優先,因此console.log(fun)打印出來的是函數體,而代碼執行賦值語句的時候,fun被賦值成了字符串,因此fun()會報錯

代碼執行過程分析--變量對象
咱們先上一段簡單的代碼,經過這段代碼,來分析一下 執行上下文建立和做用的過程,對其內容咱們先只涉及變量對象vo:

//code-03
var name = 'xiaoming'

function user(name){
   var age = 27
   console.log(`我叫${name},今年${age}`)
}
user(name)
console.log(name)

咱們如今來分析一下這段代碼執行過程當中,執行上下文的做用過程,會加入變量對象vo,做用域鏈scope會在下面講,this的指向此次不講,因此就不加上去了

1.代碼執行以前,先建立 全局的執行上下文G_EC,並壓入執行上下棧ECS

ECS = [
  G_EC : {
    vo:{
      name:undefined,
      user(name){ 
        var age = 27
        console.log(`我叫${name},今年${age}`)
      },
    },
    sc
  }
]

2.代碼開始執行,name被賦值,執行user(name)
3.函數執行的時候,具體代碼還沒執行以前,建立函數執行上下文user_EC,並壓入ECS

ECS = [
  user_EC : {
    vo:{
      name:undefined,
      age:undefined,
    },
    sc
  },
  G_EC : {
    vo:{
      name:'xiaoming',
      user(name){ 
        var age = 27
        console.log(`我叫${name},今年${age}`)
      }
    },
    sc
  }
]

4.開始執行函數代碼,給形參name賦值,變量age賦值,執行console.log的時候須要變量nameage,因而從它本身的執行上下文user_EC中的變量對象vo裏開始查找

ECS = [
  user_EC : {
    vo:{
      name:'xiaoming',
      age:27,
    },
    sc
  },
  G_EC : {
    vo:{
      name:'xiaoming',
      user(name){ 
        var age = 27
        console.log(`我叫${name},今年${age}`)
      }
    },
    sc
  }
]

5.發現找到了,因而打印 我叫xiaoming,今年27,至此函數user執行完畢了,因而把其對應的執行上下文user_EC出棧

ECS = [
  G_EC : {
    vo:{
      name:'xiaoming',
      user(name){ 
        var age = 27
        console.log(`我叫${name},今年${age}`)
      }
    },
    sc
  }
]

6.代碼繼續執行,console.log(name),發現須要變量那麼,因而從它本身的執行上下文中的變量對象開始查找,也就是G_EC中的vo,順利找到,因而打印"xiaoming"
7.至此代碼執行結束,但全局的執行上下文好像要等到當前頁面關閉纔出棧(瀏覽器環境)

3.做用域鏈

上面咱們分析代碼執行過程的時候,有說到若是要用到變量的時候,就從當前執行上下文中的變量對象vo裏查找,咱們恰好是都有找到。
那麼若是當前執行上下文中的變量對象中沒有須要用的變量呢?根據咱們的經驗,它會從父級的做用域來查找,那麼這是根據什麼來查找的呢?
全部接下來咱們繼續來看 '做用域鏈'(scope chain,sc),它也是執行上下文得另外一個組成部分。
** 函數做用域 **
在說執行上下中的做用域鏈以前,咱們要先來看看函數做用域,那麼這是個什麼東西呢?

  • 每個函數都有一個內部屬性【scope】
  • 它是函數建立的時候構建的
  • 它是一個列表,會把函數的全部父輩的執行上下中的變量對象存在其中
    舉個例子:
//code-04
function fun_1(){
  function fun_2(){}
}

1.咱們看上面的代碼,當fun_1函數建立的時候,它的父級執行上下文是全局執行上下文 G_EC,因此fun_1的函數做用域【scope】爲:

fun_1.scope = [
  G_EC.vo
]

2.當fun_2函數建立的時候,它的全部父級執行上下文有兩個,一個是全局執行上下文 G_EC, 還有一個是函數fun_1的執行上下文 fun_1_EC, 因此fun_2的函數做用域【scope】爲:

fun_1.scope = [
  fun_1_EC.vo,
  G_EC.vo
]

執行上下文的做用域鏈
上面咱們說的是函數做用域,它包含了全部父級執行上下的變量對象,可是咱們發現它沒有包含函數本身的變量對象,由於這個時候函數只是聲明瞭,尚未執行,而函數的執行上下文是在函數執行的時候建立的。
當函數執行的時候,會建立函數的執行上下文,從上面咱們知道,這個時候會建立執行上下文變量對象vo,而賦值執行上下文做用域鏈sc的時候,會把vo加在scope前面,做爲一個隊列,賦值給做用域鏈
就是說:EC.sc = [EC.vo,...fun.scope],咱們下面舉例說明,這段代碼與code-03的區別只是不給函數傳參,因此會用到父級做用域的變量。

//code-05
var name = 'xiaoming'

function user(){
   var age = 27
   console.log(`我叫${name},今年${age}`)
}
user()
console.log(name)

1.代碼執行以前,先建立 全局的執行上下文G_EC,並壓入執行上下棧ECS,同時賦值變量對象vo、做用域鏈sc,注意:當函數user被聲明的時候,會帶有函數做用域user.scope

ECS = [
  G_EC : {
    vo:{
      name:undefined,
      user // user.scope:[G_EC.vo]
    },
    sc:[G_EC.vo]
  }
]

2.代碼開始執行,name被賦值,執行user()
3.函數執行的時候,具體代碼還沒執行以前,建立函數執行上下文user_EC,並壓入ECS,同時賦值變量對象vo和做用域鏈sc:

ECS = [
  user_EC : {
    vo:{
      age:undefined,
    },
    sc:[user_EC.vo, ...user.scope]
  },
  G_EC : {
    vo:{
      name:'xiaoming',
      user // user.scope:[G_EC.vo]
    },
    sc:[G_EC.vo]
  }
]

4.開始執行函數代碼,給變量age賦值,執行console.log的時候須要變量nameage,這裏咱們上面說是從變量對象裏找,這裏更正一下,實際上是從做用域鏈中查找

ECS = [
  user_EC : {
    vo:{
      age:27,
    },
    sc:[user_EC.vo, ...user.scope]
  },
  G_EC : {
    vo:{
      name:'xiaoming',
      user, // user.scope:[G_EC.vo]
    },
    sc:[G_EC.vo]
  }
]

5.咱們發如今做用域鏈的第一個對象中(user_EC.vo)找到了age,可是沒有name,因而開始查找做用域鏈的第二個對象,依次往下找,若是都沒找到,則會報錯。
這裏的話,咱們發現做用域鏈的第二個元素user.scope析構出來的,也就是G_EC.vo,這個裏面有找到name='xiaoming'
因而打印 我叫xiaoming,今年27,至此函數user執行完畢了,因而把其對應的執行上下文user_EC出棧

ECS = [
  G_EC : {
    vo:{
      name:'xiaoming',
      user, // user.scope:[G_EC.vo]
    },
    sc:[G_EC.vo]
  }
]

6.代碼繼續執行,console.log(name),發現須要變量那麼,因而從它本身的執行上下文中的做用域鏈開始查找,在第一個元素G_EC.vo就順利找到,因而打印"xiaoming"
7.至此代碼執行結束,

4.迴歸到閉包的問題

到此爲止咱們介紹完了執行上下文,那麼如今咱們迴歸到剛開始的閉包爲何能訪問到已經執行完畢了的函數的內部變量問題。咱們再來回顧一下代碼:

//code-06
    function cat() {
      var name = "小貓";
      function say() {
        console.log(`my name is ${name}`);
      }
      return say;
    }
    var fun = cat();
    fun();

咱們來照上面的步驟來分析下代碼:
1.代碼執行以前,先建立 全局的執行上下文G_EC,並壓入執行上下棧ECS,同時賦值變量對象vo、做用域鏈sc

ECS = [
  G_EC : {
    vo:{
      fun:undefined,
      cat, // cat.scope:[G_EC.vo]
    },
    sc:[G_EC.vo]
  }
]

2.代碼開始執行,執行cat()函數
3.函數執行的時候,具體代碼還沒執行以前,建立函數執行上下文cat_EC,並壓入ECS,同時賦值變量對象vo和做用域鏈sc:

ECS = [
  cat_EC : {
    vo:{
      name:undefined,
      say, // say.scope:[cat_EC.vo,G_EC.vo]
    },
    sc:[cat_EC.vo, ...cat.scope]
  },
  G_EC : {
    vo:{
      fun:undefined,
      cat, // cat.scope:[G_EC.vo]
    },
    sc:[G_EC.vo]
  }
]

4.開始執行函數代碼,給變量name賦值,而後返回say函數,這個時候函數執行完畢,它的值被付給變量fun,它的執行上下文出棧

ECS = [
  G_EC : {
    vo:{
      fun:say, // say.scope:[cat_EC.vo,G_EC.vo]
      cat // cat.scope:[G_EC.vo]
    },
    sc:[G_EC.vo]
  }
]

5.代碼繼續執行,到了fun(),
6.當函數要執行,還沒執行具體代碼以前,建立函數執行上下文fun_EC,並壓入ECS,同時賦值變量對象vo和做用域鏈sc:

ECS = [
  fun_EC : {
    vo:{},
    sc:[fun_EC.vo, ...fun.scope]//fun==cat,因此fun.scope = say.scope = [cat_EC.vo,G_EC.vo]
  },
  G_EC : {
    vo:{
      fun:say, // say.scope:[cat_EC.vo,G_EC.vo]
      cat // cat.scope:[G_EC.vo]
    },
    sc:[G_EC.vo]
  }
]

7.函數fun開始執行具體代碼:console.log(my name is ${name}),發現須要變量name,因而從他的fun_EC.sc中開始查找,第一個fun_EC.vo沒有,因而找第二個cat_EC.vo,發現這裏有name="小貓",
因而打印 my name is 小貓,至此函數fun執行完畢了,因而把其對應的執行上下文fun_EC出棧

ECS = [
  G_EC : {
    vo:{
      fun:say, // say.scope:[cat_EC.vo,G_EC.vo]
      cat // cat.scope:[G_EC.vo]
    },
    sc:[G_EC.vo]
  }
]

8.至此代碼執行結束

到這裏咱們知道閉包爲何能夠訪問到已經執行完畢的函數的內部變量,是由於在的執行上下文中的做用域鏈中保存了變量的引用,而保存的引用的變量不會被垃圾回收機制所銷燬。

閉包的優缺點
優勢:

  1. 能夠建立擁有私有變量的函數,使函數具備封裝性
  2. 避免全局變量污染

缺點:

  1. 增大內存消耗

參考
1.JavaScript深刻之詞法做用域和動態做用域
2.JavaScript深刻之執行上下文棧
3.setTimeout和setImmediate到底誰先執行,本文讓你完全理解Event Loop

相關文章
相關標籤/搜索