閉包和執行上下文

這裏有一份簡潔的前端知識體系等待你查收,看看吧,會有驚喜哦~若是以爲不錯,懇求star哈~javascript


概述

一段 JS 代碼可能會包含函數調用的相關內容,你可能據說過不少概念,諸如閉包、做用域鏈、執行上下文、this值。前端

實際上,儘管它們是表示不一樣的意思的術語,所指向的幾乎是同一部分知識,那就是函數執行過程相關的知識。咱們能夠簡單看一下圖。java



咱們先來說講這個有點複雜的概念:閉包。git


閉包

在編程語言領域,閉包表示一種函數。github

在上世紀60年代,主流的編程語言是基於lambda演算的函數式編程語言,最初的閉包定義,是「帶有一系列信息的λ表達式」。對函數式語言而言,λ表達式其實就是函數。編程

因此,閉包其實只是一個綁定了執行環境的函數,閉包與普通函數的區別是,它攜帶了執行的環境。瀏覽器

咱們來看下古典的閉包定義跟 JS 中的閉包定義,觀察他們的區別。閉包

古典的閉包定義中,閉包包含兩個部分:app

  1. 環境部分
    • 環境
    • 標識符列表
  2. 表達式部分

JS 中閉包組成部分:編程語言

  1. 環境部分
    • 環境:函數的詞法環境(執行上下文的一部分)
    • 標識符列表:函數中用到的未聲明的變量(也就是函數裏不帶var/let/const的變量)
  2. 表達式部分:函數體

有些人會把 JS 執行上下文,或者做用域(Scope,ES3中規定的執行上下文的一部分)這個概念看成閉包。實際上JS 中跟閉包對應的概念就是「函數」。

這裏給閉包作個簡單的定義:函數 A 內部有一個函數 B,函數 B 訪問到函數 A 中的變量,那麼函數 B 就是閉包。

咱們能夠這樣理解:

  1. 首先,函數B綁定了函數A的語法環境,該閉包無論在何處聲明,函數B綁定的環境都不會改變。
  2. 其次,函數B用到了未聲明的變量,這些變量來自函數A。

執行上下文:執行的基礎設施

相比普通函數,JS 閉包的主要複雜性來自於它攜帶的「環境部分」。固然,發展到今天的 JS ,它所定義的環境部分,已經比當初經典的定義複雜了不少。

JS 中與閉包「環境部分」相對應的術語是「詞法環境」,可是 JS 函數比λ函數要複雜得多,咱們還要處理this、變量聲明、with等等一系列的複雜語法,λ函數中可沒有這些東西,因此,在 JS 的設計中,詞法環境只是 JS 執行上下文的一部分。

JS 標準把一段代碼(包括函數),執行所需的全部信息定義爲:「執行上下文」。

由於這部分術語經歷了比較多的版本和社區的演繹,因此定義比較混亂,這裏咱們先來理一下 JS 中的概念。


ES3

執行上下文在ES3中,包含三個部分。

  1. scope:做用域,也經常被叫作做用域鏈。
  2. variable object:變量對象,用於存儲變量的對象。
  3. this value:this值。

注意:網上流傳甚廣的,用global object,和active object 來解釋閉包、做用域、執行上下文,這是ES3裏的解釋法,如今已經解釋不了不少語法了。


ES5

在ES5中,咱們改進了命名方式,把執行上下文最初的三個部分改成下面這個樣子。

  1. lexical environment:詞法環境,當獲取變量時使用。
  2. variable environment:變量環境,當聲明變量時使用。
  3. this value:this值。

ES2018

在ES2018中,執行上下文又變成了這個樣子,this值被納入lexical environment,可是增長了很多內容。

  1. lexical environment:詞法環境,當獲取變量或者this值時使用。
  2. variable environment:變量環境,當聲明變量時使用
  3. code evaluation state:用於恢復代碼執行位置。
  4. Function:執行的任務是函數時使用,表示正在被執行的函數。
  5. ScriptOrModule:執行的任務是腳本或者模塊時使用,表示正在被執行的代碼。
  6. Realm:使用的基礎庫和內置對象實例。
  7. Generator:僅生成器上下文有這個屬性,表示當前生成器。

咱們在這裏介紹執行上下文的各個版本定義,是考慮到你可能會從各類網上的文章中接觸這些概念,若是不把它們理清楚,咱們就很難分辨對錯。若是是咱們本身使用,建議統一使用最新的ES2018中規定的術語定義。

接下來,咱們從代碼實例出發,推導函數執行過程當中須要哪些信息,它們又對應着執行上下文中的哪些部分。

好比,咱們看如下的這段 JS 代碼:

var b = {}
let c = 1
this.a = 2;
複製代碼

要想正確執行它,咱們須要知道如下信息:

  1. var 把 b 聲明到哪裏;
  2. b 表示哪一個變量;
  3. b 的原型是哪一個對象;
  4. let 把 c 聲明到哪裏;
  5. this 指向哪一個對象。

這些信息就須要執行上下文來給出了,這段代碼出如今不一樣的位置,甚至在每次執行中,會關聯到不一樣的執行上下文,因此,一樣的代碼會產生不同的行爲。

這裏咱們先講var聲明與賦值,let,realm三個特性來分析執行上下文中提供的信息。


var

咱們來分析一段代碼:var b = 1;

一般咱們認爲它聲明瞭b,而且爲它賦值爲1,var聲明做用域是函數執行的做用域。也就是說,var會穿透for 、if等語句。

在只有var,沒有let的舊 JS 時代,誕生了一個技巧,叫作:當即執行的函數表達式(IIFE),經過建立一個函數,而且當即執行,來構造一個新的域,從而控制var的範圍。

因爲語法規定了function關鍵字開頭是函數聲明,因此要想讓函數變成函數表達式,咱們必須得加點東西,最多見的作法是加括號。

(function(){
  var a;
  //code
}());
(function(){
  var a;
  //code
})();
複製代碼

值得特別注意的是,有時候var的特性會致使聲明的變量和被賦值的變量是兩個b,JS 中有特例,那就是使用with的時候:

var b;
void function(){
  var env = {b:1};
  b = 2;
  console.log("In function b:", b);
  with(env) {
    var b = 3;
    console.log("In with b:", b);
  }
}();
console.log("Global b:", b);
複製代碼

在這個例子中,咱們利用當即執行的函數表達式(IIFE)構造了一個函數的執行環境,而且在裏面使用了咱們一開頭的代碼。

能夠看到,在Global、function、with三個環境中,b的值都不同,而在function環境中,並無出現var b,這說明with內的var b做用到了function這個環境當中。

var b = {} 這樣一句對兩個域產生了做用,從語言的角度是個很是糟糕的設計,這也是一些人堅決地反對在任何場景下使用with的緣由之一。


let

let是 ES6開始引入的新的變量聲明模式,比起var的諸多弊病,let作了很是明確的梳理和規定。

爲了實現let,JS 在運行時引入了塊級做用域。也就是說,在let出現以前,JS 的 if 、for 等語句皆不產生做用域。

簡單統計了下,如下語句會產生let使用的做用域:

  1. for;
  2. if;
  3. switch;
  4. try/catch/finally。

Realm

在最新的標準(9.0)中,JS 引入了一個新概念Realm,它的中文意思是「國度」「領域」「範圍」。這個英文的用法就有點比喻的意思,幾個翻譯都不太適合 JS 語境,因此這裏就不翻譯啦。

咱們繼續來看這段代碼:var b = {}

在 ES2016 以前的版本中,標準中甚少說起{}的原型問題。但在實際的前端開發中,經過iframe等方式建立多window環境並不是罕見的操做,因此,這才促成了新概念Realm的引入。

Realm中包含一組完整的內置對象,並且是複製關係。

對不一樣Realm中的對象操做,會有一些須要格外注意的問題,好比 instanceOf 幾乎是失效的。

如下代碼展現了在瀏覽器環境中獲取來自兩個Realm的對象,它們跟本土的Object作instanceOf時會產生差別:

var iframe = document.createElement('iframe')
document.documentElement.appendChild(iframe)
iframe.src="javascript:var b = {};"
var b1 = iframe.contentWindow.b;
var b2 = {};
console.log(typeof b1, typeof b2); //object object
console.log(b1 instanceof Object, b2 instanceof Object); //false true
複製代碼

能夠看到,因爲b一、 b2由一樣的代碼「 {} 」在不一樣的Realm中執行,因此表現出了不一樣的行爲。


做用域

咱們再來講下做用域,簡單來講做用域就是一個區域,包含了其中變量,常量,函數等等定義信息和賦值信息,以及這個區域內代碼書寫的結構信息。做用域能夠嵌套。咱們一般知道 js 中函數的定義能夠產生做用域,下面咱們用具體代碼來示例下:



全局做用域(global scope)裏面定義了兩個變量,一個函數。walk 函數生成的做用域裏面定義了一個變量,兩個函數。innerFunc 和 anotherInnerFunc 這兩個函數生成的做用域裏面分別定義了一個變量。

在規範中做用域更官方的叫法是詞法環境,沒錯,就是上文提到的詞法環境,包含在執行上下文中。

做用域其實由兩部分組成:

  1. 記錄做用域內變量信息(咱們假設變量,常量,函數等統稱爲變量)和代碼結構信息的東西,稱之爲 Environment Record。
  2. 一個引用 outer,這個引用指向當前做用域的父做用域。拿上面代碼爲例。innerFunc 的函數做用域有一個引用指向 walk 函數做用域,walk 函數做用域有一個引用指向全局做用域。全局做用域的 outer 爲 null。

規範中定義了查找一個變量的過程:先查看當前做用域裏面的 Environment Record 是否有此變量的信息,若是找到了,則返回當前做用域內的這個變量。若是沒有查找到,則順着 outer 到父做用域裏面的 Environment Record 查找,以此遞歸。

因此咱們一般所說的函數內同名變量遮蔽全局變量就是這麼回事。不過若是你在變量查找的時候指定某個做用域中的 Environment Record,那麼也是能夠的,譬如:window.name 【其實 window 對象就是全局做用域的 Environment Record 對象,可是普通函數做用域的 Environment Record 對象是獲取不到的】。


做用域和執行上下文的關係

執行上下文是用於跟蹤代碼的運行狀況,而做用域用於獲取變量或者this值。從職責上看,他們幾乎是沒有啥交集的。那麼爲啥一般二者會被同時提到呢?由於在一個函數被執行時,建立的執行上下文對象除了保存了些代碼執行的信息,還會把當前的做用域保存在執行上下文中。因此它們的關係只是存儲關係。

結合做用域和執行上下文,咱們再來看下變量查找的過程。其實第一步不是到做用域裏面找 Environment Record,而是先從當前的執行上下文中找保存的做用域(對象),而後再是經過做用域鏈向上查找變量。


結語

在這篇文章中,咱們梳理了一些概念:有編程語言的概念閉包,也有各個版本中的 JS 標準中的概念:執行上下文、做用域、this值等等。

以後咱們又從代碼的角度,分析了一些執行上下文中所須要的信息,並從var、let、對象字面量等語法中,推導出了詞法做用域、變量做用域、Realm的設計。

最後,咱們對比了執行上下文跟做用域的關係。

相關文章
相關標籤/搜索