JS 總結之閉包

《JS 總結之函數、做用域鏈》一文中知道做用域鏈的做用,保證了對全部變量對象的有序訪問。html

👩‍🎨‍ 問題

函數外的是沒法訪問函數內部的變量,有時候要用到怎麼辦?咱們的主角,閉包就是能夠解決這個問題。git

👷 什麼是閉包

引用 MDN 上的解釋:github

閉包是函數和聲明該函數的詞法環境的組合。閉包

引用 《JavaScript 高級程序設計(第 3 版)》上的解釋:函數

閉包是指有權訪問另外一個函數做用域中的變量的函數。ui

🐳 相同點

這兩個解釋都在說着同一件事,閉包能訪問聲明時函數所在的環境中的變量和函數this

那具體是由於什麼纔會讓閉包訪問到環境中的變量和函數,這得靠兩兄弟:變量對象做用域鏈spa

變量對象翻譯

當執行函數的時候,會建立和初始化一個函數執行環境,在函數執行環境中,全局執行環境的變量對象(Variable Object,縮寫爲 VO)不能直接訪問,此時由激活對象(Activation Object,縮寫爲 AO)扮演 VO 的角色。設計

變量對象專門用來梳理和記住這些變量和函數,是做用域鏈造成的前置條件 。但咱們沒法直接使用這個變量對象,該對象主要是給 JS 引擎使用的。具體能夠查看《JS 總結之變量對象》

變量對象就至關於一個存放的倉庫,獲取到裏面的東西,還得須要去獲取這些的路徑,這就是做用域鏈的事情了。

做用域鏈

然而,光有變量對象可完成不了閉包的造成,怎樣才能讓函數訪問到,這得靠做用域鏈,做用域鏈的做用就是讓函數找到變量對象裏面的變量和函數。具體能夠查看《JS 總結之函數、做用域鏈》

🐬 不一樣點

雖然都是講閉包,但 MDN 上面講的是聲明該函數的詞法環境,而 JS 高程講的是訪問另外一個函數做用域中,從解釋上的不一樣,閉包便有了理論中的閉包( MDN )和實踐中的閉包( JS 高程)之分。

🕵 理論中的閉包

根據 MDN 的解釋寫個例子:

var a = 1
function fn() {
  console.log(a)
}
fn()
複製代碼

函數 fn 和函數 fn 的詞法做用域構成了一個閉包。可是這不是普通的函數嗎?

在《JavaScript 權威指南》中也獲得證明:

從技術的角度講,全部 JavaScript 函數都是閉包

👨‍💻‍ 實踐中的閉包

湯姆大叔翻譯的文章中講,實踐中的閉包須要知足如下兩個條件:

  1. 即便建立它的上下文已經銷燬,它仍然存在(好比,內部函數從父函數中返回)
  2. 在代碼中引用了自由變量

什麼是上下文?即函數的執行環境。

什麼是自由變量?即函數的詞法做用域中的變量和函數,而不是函數自己的參數或者局部變量,或者說是所在函數的變量對象中的變量和函數。

這兩點和 JS 高程中講的閉包的解釋不謀而合。

如今寫個符合的例子:

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

var b = fn()
b()
複製代碼

當執行 b 的時候,建立它的執行環境 fn 早已經摧毀,但函數 b 還能訪問到變量 a。

好吧,我有點亂!

要完全明白這個是咋回事,要結合執行環境活動變量做用域鏈來看,讓咱們來看看這個例子的執行過程:

🍑 執行過程

  1. 執行全局代碼,建立全局執行環境 globalContext,將全局執行環境推入環境棧
環境棧 = [globalContext]
複製代碼
  1. 初始化全局執行環境
globalContext = {
  VO: [global],
  Scope: [globalContext.VO],
  this: globalContext.VO
}
複製代碼
  1. 初始化的同時,建立 fn 函數,複製 全局執行環境的做用域鏈 到 fn 的 [[scope]] 屬性
fn.[[scope]] = [
  globalContext.VO
]
複製代碼
  1. 執行 fn 函數,建立 fn 的執行環境 fnContext,並推入環境棧
環境棧 = [fnContext, globalContext]
複製代碼
  1. 初始化 fnContext:
  • 複製 fn 的 [[scope]] 建立 fn 做用域鏈
  • 建立變量對象,初始化變量對象,加入形參,函數,變量等,因爲是函數,變量對象爲活動對象
  • 將活動對象推入 fnContext 的 Scope 頂端
fnContext = {
  AO: {
    arguments: {},
    scope: undefined,
    fn1: reference to function fn1(){}
  },
  Scope: [AO, globalContext.VO],
  this: undefined
}
複製代碼
  • 初始化同時,建立 fn1 函數,複製 fnContext 的做用域鏈 到 fn1 的 [[scope]] 屬性
fn1.[[scope]] = [
  fnContext.AO, globalContext.VO
]
複製代碼
  1. 執行完畢,推出 fnContext
環境棧 = [globalContext]
複製代碼
  1. 執行函數 b,也就是被返回的 fn1,建立 fn1 的執行環境 fn1Context,並推入環境棧
環境棧 = [
  fn1Context,
  globalContext
]
複製代碼
  1. 初始化 fn1Context:
  • 複製 fn1 的 [[scope]] 建立 fn1 做用域鏈
  • 建立變量對象,初始化變量對象,加入形參,函數,變量等,因爲是函數,變量對象爲活動對象
  • 將活動對象推入 fn1Context 的 Scope 頂端
fn1Context = {
  AO: {
    arguments: {}
  },
  Scope: [AO, fnContext.AO, globalContext.VO],
  this: undefined
}
複製代碼
  1. 執行完畢,推出 fn1Context
環境棧 = [globalContext]
複製代碼

🍓 柳暗花明

當執行函數 b 的時候,建立它的執行環境 fn 早已摧毀(步驟 6),只留下了它的活動變量 fnContext.AO 於內存中(步驟 5):

fn1Context = {
  Scope: [AO, fnContext.AO, globalContext.VO]
}
複製代碼

fnContext.AO 存在 fn1 的做用域鏈中,因此能訪問到fn1 的詞法環境,這便造成了閉包。所以,閉包是變量對象和做用域鏈共同做用的結果。

🚀 參考

相關文章
相關標籤/搜索