這裏有一份簡潔的前端知識體系等待你查收,看看吧,會有驚喜哦~若是以爲不錯,懇求star哈~javascript
一段 JS 代碼可能會包含函數調用的相關內容,你可能據說過不少概念,諸如閉包、做用域鏈、執行上下文、this值。前端
實際上,儘管它們是表示不一樣的意思的術語,所指向的幾乎是同一部分知識,那就是函數執行過程相關的知識。咱們能夠簡單看一下圖。java
咱們先來說講這個有點複雜的概念:閉包。git
在編程語言領域,閉包表示一種函數。github
在上世紀60年代,主流的編程語言是基於lambda演算的函數式編程語言,最初的閉包定義,是「帶有一系列信息的λ表達式」。對函數式語言而言,λ表達式其實就是函數。編程
因此,閉包其實只是一個綁定了執行環境的函數,閉包與普通函數的區別是,它攜帶了執行的環境。瀏覽器
咱們來看下古典的閉包定義跟 JS 中的閉包定義,觀察他們的區別。閉包
古典的閉包定義中,閉包包含兩個部分:app
JS 中閉包組成部分:編程語言
有些人會把 JS 執行上下文,或者做用域(Scope,ES3中規定的執行上下文的一部分)這個概念看成閉包。實際上JS 中跟閉包對應的概念就是「函數」。
這裏給閉包作個簡單的定義:函數 A 內部有一個函數 B,函數 B 訪問到函數 A 中的變量,那麼函數 B 就是閉包。
咱們能夠這樣理解:
相比普通函數,JS 閉包的主要複雜性來自於它攜帶的「環境部分」。固然,發展到今天的 JS ,它所定義的環境部分,已經比當初經典的定義複雜了不少。
JS 中與閉包「環境部分」相對應的術語是「詞法環境」,可是 JS 函數比λ函數要複雜得多,咱們還要處理this、變量聲明、with等等一系列的複雜語法,λ函數中可沒有這些東西,因此,在 JS 的設計中,詞法環境只是 JS 執行上下文的一部分。
JS 標準把一段代碼(包括函數),執行所需的全部信息定義爲:「執行上下文」。
由於這部分術語經歷了比較多的版本和社區的演繹,因此定義比較混亂,這裏咱們先來理一下 JS 中的概念。
ES3
執行上下文在ES3中,包含三個部分。
注意:網上流傳甚廣的,用global object,和active object 來解釋閉包、做用域、執行上下文,這是ES3裏的解釋法,如今已經解釋不了不少語法了。
ES5
在ES5中,咱們改進了命名方式,把執行上下文最初的三個部分改成下面這個樣子。
ES2018
在ES2018中,執行上下文又變成了這個樣子,this值被納入lexical environment,可是增長了很多內容。
咱們在這裏介紹執行上下文的各個版本定義,是考慮到你可能會從各類網上的文章中接觸這些概念,若是不把它們理清楚,咱們就很難分辨對錯。若是是咱們本身使用,建議統一使用最新的ES2018中規定的術語定義。
接下來,咱們從代碼實例出發,推導函數執行過程當中須要哪些信息,它們又對應着執行上下文中的哪些部分。
好比,咱們看如下的這段 JS 代碼:
var b = {}
let c = 1
this.a = 2;
複製代碼
要想正確執行它,咱們須要知道如下信息:
這些信息就須要執行上下文來給出了,這段代碼出如今不一樣的位置,甚至在每次執行中,會關聯到不一樣的執行上下文,因此,一樣的代碼會產生不同的行爲。
這裏咱們先講var聲明與賦值,let,realm三個特性來分析執行上下文中提供的信息。
咱們來分析一段代碼: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是 ES6開始引入的新的變量聲明模式,比起var的諸多弊病,let作了很是明確的梳理和規定。
爲了實現let,JS 在運行時引入了塊級做用域。也就是說,在let出現以前,JS 的 if 、for 等語句皆不產生做用域。
簡單統計了下,如下語句會產生let使用的做用域:
在最新的標準(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 這兩個函數生成的做用域裏面分別定義了一個變量。
在規範中做用域更官方的叫法是詞法環境,沒錯,就是上文提到的詞法環境,包含在執行上下文中。
做用域其實由兩部分組成:
規範中定義了查找一個變量的過程:先查看當前做用域裏面的 Environment Record 是否有此變量的信息,若是找到了,則返回當前做用域內的這個變量。若是沒有查找到,則順着 outer 到父做用域裏面的 Environment Record 查找,以此遞歸。
因此咱們一般所說的函數內同名變量遮蔽全局變量就是這麼回事。不過若是你在變量查找的時候指定某個做用域中的 Environment Record,那麼也是能夠的,譬如:window.name 【其實 window 對象就是全局做用域的 Environment Record 對象,可是普通函數做用域的 Environment Record 對象是獲取不到的】。
執行上下文是用於跟蹤代碼的運行狀況,而做用域用於獲取變量或者this值。從職責上看,他們幾乎是沒有啥交集的。那麼爲啥一般二者會被同時提到呢?由於在一個函數被執行時,建立的執行上下文對象除了保存了些代碼執行的信息,還會把當前的做用域保存在執行上下文中。因此它們的關係只是存儲關係。
結合做用域和執行上下文,咱們再來看下變量查找的過程。其實第一步不是到做用域裏面找 Environment Record,而是先從當前的執行上下文中找保存的做用域(對象),而後再是經過做用域鏈向上查找變量。
在這篇文章中,咱們梳理了一些概念:有編程語言的概念閉包,也有各個版本中的 JS 標準中的概念:執行上下文、做用域、this值等等。
以後咱們又從代碼的角度,分析了一些執行上下文中所須要的信息,並從var、let、對象字面量等語法中,推導出了詞法做用域、變量做用域、Realm的設計。
最後,咱們對比了執行上下文跟做用域的關係。