參考 :javascript
每當js引擎執行一段新的js代碼時,它都會建立一個全新的執行上下文,由執行上下文來跟蹤整個代碼的執行狀況、當前執行函數、做用域、this指向和變量映射。js引擎經過讀取執行上下文就能夠管理追蹤該段js代碼的執行狀況,那麼多個上下文是經過什麼來管理的呢?答案是棧,多個上下文的管理是經過上下文棧來管理的。若是解析到一段跟當前上下文無關的新代碼,那麼js引擎就會在上下文棧裏建立一個新的上下文對象,並插入棧頂的位置。當執行完上下文的時候就會在上下文棧裏把這個對象踢出棧,恢復下一個棧。html
咱們來看下面這段代碼,在全局狀況下會有個全局上下文,放在棧底。在執行fn的時候,會在棧頂插入一個新的上下文,而且將當前執行上下文指向這裏,當解析到whileFn函數時,又重複剛剛的步驟。這個時候fn上下文會 暫停 並記錄執行節點,去執行whileFn上下文,當執行結束後又會 恢復 fn上下文,而且在上次暫停的地方繼續執行。因而可知執行上下文跟蹤代碼時,會有代碼執行狀態(code evaluation state),用來表示執行狀態是暫停、恢復等,以及指示暫停節點。除此以外執行上下文還會記錄當前執行的函數(function)以及做用域(realm)java
js執行代碼
var global = 'global'
console.log(global)
function fn() {
let inner = 'inner'
let time = 0
function whileFn() {
while(time < 10) {
++time
}
}
console.log(inner)
whileFn()
console.log(time)
}
fn()
複製代碼
另外能夠看下這段代碼及其對應的執行棧示意圖es6
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
複製代碼
除此以外,執行上下文還會建立一個詞法做用域,別看這個名詞感受很流弊的樣子,其實它只是用於存儲標識符和實際引用之間的映射,相似於字典。當js引擎執行到一段代碼時,遇到變量名或者函數名時,就會在這個字典裏進行查找並調用出裏邊的值。詞法做用域還分爲詞法環境LexicalEnvironment 和變量環境VariableEnvironment ,在ES5的時候這二者還有着複雜的區別和定義,可是在ES6時,這二者的區別基本就是存儲變量的聲明關鍵字不一樣,前者是用來存放 let / const / function / class 等聲明的標識符映射,然後者是用來存儲 var 聲明的標識符映射。緩存
詞法做用域可分爲全局做用域、模塊做用域和函數做用域。閉包
// global
import module from './module'
console.log('global environment', this)
function() {
console.log('function environment', this)
}
// module
console.log('module environment', this)
複製代碼
剛剛有稍微講到這幾個的區別,接下來咱們結合詞法做用域的概念來具體說說。這幾個聲明的主要區別是初始化變量的機制和存儲標識符映射的環境不一樣:app
js引擎在建立當前執行上下文時,會初始化詞法做用域,在 變量環境 裏建立 var 變量的標識符映射並初始化爲undefined。var比較特殊,能夠屢次執行 var 聲明語句賦值相同的變量,js引擎會作相應的建立 / 修改變量的操做ide
js引擎在建立當前執行上下文時,會初始化詞法做用域,在 詞法環境 裏建立 let / const / function / class 標識符映射,可是隻會建立 let / const / class 變量,不作初始化操做,而且禁止訪問。只有在當前執行上下文執行階段,執行 詞法綁定 時,纔會初始化爲對應的 value 或者undefined,此時纔會容許訪問變量。 let / const / class 關鍵字不容許在同一做用域下重複聲明相同變量。函數
var、let、const、class和function的建立機制是,在執行上下文的建立階段時,js引擎就會在內部建立一個詞法做用域,它就像是一個標識符查找的字典。這個字典會在建立階段就將聲明的變量、函數和類都建立,可是var 聲明的變量會被初始化爲undefined,而 let / const / class 聲明的變量不會被初始化,而且禁止訪問,function 關鍵字的聲明會直接將函數體賦值給對應的函數名。並且除了var以外,後面幾個關鍵字聲明的標識符映射是存放在詞法環境裏,而var聲明的標識符映射是存放在變量環境裏。ui
結合上面內容,咱們來看看閉包是什麼,咱們常見的閉包寫法,就是新建一個閉包函數工廠,在函數裏定義變量,而後再返回一個引用這些變量的函數,以下所示:
function makeAdder(x) {
return function(y) {
return x + y;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
複製代碼
在makeAdder函數內會建立一個內部的詞法做用域,存儲傳入的x變量,那麼每次調用makeAdder函數就會建立一個擁有獨立內部詞法做用域的函數,add5函數會綁定x=5的做用域,add10綁定的是x=10的做用域。所以add5(2)和add10(2)返回的結果是不同的。 咱們能夠看到,閉包是一個綁定了詞法做用域的函數,經過閉包能夠訪問內部詞法做用域定義的局部變量。這樣子經過閉包咱們能夠實現私有方法,避免污染全局環境或者保證方法只能在內部調用,封裝細節,以及避免昂貴的計算過程,緩存計算結果。參考下面代碼:
var makeCounter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var counter1 = makeCounter();
var counter2 = makeCounter();
複製代碼
每次調用makeCounter函數會建立獨立的閉包,在閉包裏會有私有變量privateCounter和私有方法changeBy,外部調用時不須要知道這兩個東西,只須要接收increment和decrement方法,不須要知道細節實現
綁定事件監聽回調,其實也是一種閉包,只是沒有那麼明顯而已。看看下面這段代碼
<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
複製代碼
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [
{'id': 'email', 'help': 'Your e-mail address'},
{'id': 'name', 'help': 'Your full name'},
{'id': 'age', 'help': 'Your age (you must be over 16)'}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();
複製代碼
這裏的運行結果你們都知道了,可是運行機制就有點意思了。這是由於整個onfocus方法綁定的事件回調是一個閉包,在每一個input控件裏觸發的onfocus回調函數裏,它會在setupHelp這個函數上下文裏查找item變量,因爲遍歷以後i都是2,因此此時item都是{'id': 'age', 'help': 'Your age (you must be over 16)'}這條記錄。這就致使了每次showHelp的時候,對應的item都會綁定到同一個對象上去。這個時候的解決方法有兩個,要麼給onfocus綁定另外一個閉包,要麼就是用let關鍵字聲明一個item,保證這個item只會存在for循環的塊做用域裏。
// 新的閉包
function makeHelpCallback(help) {
return function() {
showHelp(help);
};
}
...
function setupHelp() {
...
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
}
}
// let關鍵字
...
function setupHelp() {
...
for (var i = 0; i < helpText.length; i++) {
let item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
複製代碼
最後,咱們把執行上下文、詞法做用域,還有var / let / const結合起來看的話,js引擎解析代碼的總體過程以下: js引擎執行一段和當前上下文無關的代碼時,會建立一個對應的執行上下文用於追蹤管理代碼,並將當前執行上下文指向該上下文。執行上下文的建立過程當中會建立對應的詞法做用域,詞法做用域有兩種:詞法環境和變量環境,分別用於存儲 let / const / function / class 和 var 聲明的變量。詞法做用域會經過內部的環境記錄來存儲標識符與實際變量的映射關係,一個外部引用用於查找非當前做用域的變量時進行逐級溯源查找,以及綁定當前做用域的this指針。當建立完以後,會進入執行上下文的執行階段,最終執行代碼,獲取執行結果。
舉個例子,咱們看看下面的代碼:
const a = 20;
let b = 30;
var c = '';
function foo(a, b) {
var d = a + b;
return d
}
foo(a, b);
複製代碼
那麼當js引擎解析到上述代碼時,首先,會建立一個全局執行上下文用於執行代碼,接着在全局上下文的建立階段,會建立一個全局的詞法做用域用於存儲標識符映射,此時,var 變量會被初始化爲 undefined,而 let / const 變量則是 uninitialized,而且禁止訪問。以下:
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
a: uninitialized,
b: uninitialized,
foo: <ref. to foo function>,
}
outer: <null>,
this: <global object>
}
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
c: undefined
}
outer: <null>,
ThisBinding: <global object>
}
}
複製代碼
接着,全局執行上下文進入執行階段,進行詞法綁定,將標識符和實際變量進行綁定。以下:
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
a: 20,
b: 30,
foo: <ref. to foo function>,
}
outer: <null>,
this: <global object>
}
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
c: ''
}
outer: <null>,
ThisBinding: <global object>
}
}
複製代碼
在執行階段,當執行到 foo(2, 3) 時,進入 foo 函數,會建立一個函數執行上下文,對應地建立函數詞法做用域。函數詞法做用域,除了存儲內部聲明的變量外,還會存儲整個函數的實參和實參數量。函數上下文建立階段的函數詞法做用域以下:
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
g: undefined
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>
}
}
複製代碼
當 foo 函數開始執行時,會執行變量的詞法綁定,此時,函數上下文執行階段的詞法做用域以下:
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
d: 60
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>
}
}
複製代碼