古往今來(lái)最難的學(xué)的武功【葵花寶典】(javascript)算其一。
欲練此功必先自宮,愿少俠習(xí)的此功,笑傲江湖。
你將了解
- 執(zhí)行棧(Execution stack)
- 執(zhí)行上下文(Execution Context)
- 作用域鏈(scope chains)
- 變量提升(hoisting)
- 閉包(closures)
- this 綁定
執(zhí)行棧
又叫調(diào)用棧,具有 LIFO(last in first out 后進(jìn)先出)結(jié)構(gòu),用于存儲(chǔ)在代碼執(zhí)行期間創(chuàng)建的所有執(zhí)行上下文。
當(dāng) JavaScript 引擎首次讀取你的腳本時(shí),它會(huì)創(chuàng)建一個(gè)全局執(zhí)行上下文并將其推入當(dāng)前的執(zhí)行棧。每當(dāng)發(fā)生一個(gè)函數(shù)調(diào)用,引擎都會(huì)為該函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文并將其推到當(dāng)前執(zhí)行棧的頂端。
引擎會(huì)運(yùn)行執(zhí)行上下文在執(zhí)行棧頂端的函數(shù),當(dāng)此函數(shù)運(yùn)行完成后,其對(duì)應(yīng)的執(zhí)行上下文將會(huì)從執(zhí)行棧中彈出,上下文控制權(quán)將移到當(dāng)前執(zhí)行棧的下一個(gè)執(zhí)行上下文。
我們通過下面的示例來(lái)說(shuō)明一下
function one() {
console.log('one')
two()
}
function two() {
console.log('two')
}
one()
當(dāng)程序(代碼)開始執(zhí)行時(shí) javscript 引擎創(chuàng)建 GobalExecutionContext (全局執(zhí)行上下文)推入當(dāng)前的執(zhí)行棧,此時(shí) GobalExecutionContext 處于棧頂會(huì)立刻執(zhí)行全局執(zhí)行上下文 然后遇到 one() 引擎都會(huì)為該函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文 oneFunctionExecutionContext 并將其推到當(dāng)前執(zhí)行棧的頂端并執(zhí)行,然后遇到two() twoFunctionExecutionContext 入棧并執(zhí)行至出棧,回到 oneFunctionExecutionContext 繼續(xù)執(zhí)行至出棧 ,最后剩余一個(gè) GobalExecutionContext 它會(huì)在程序關(guān)閉的時(shí)候出棧。
然后調(diào)用棧如下圖:

如果是這樣的代碼
function foo() {
foo()
}
foo()
如下

當(dāng)一個(gè)遞歸沒有結(jié)束點(diǎn)的時(shí)候就會(huì)出現(xiàn)棧溢出
什么是執(zhí)行上下文
了解 JavaScript 的執(zhí)行上下文,有助于你理解更高級(jí)的內(nèi)容比如變量提升、作用域鏈和閉包。既然如此,那到底什么是“執(zhí)行上下文”呢?
執(zhí)行上下文是當(dāng)前 JavaScript 代碼被解析和執(zhí)行時(shí)所在環(huán)境的抽象概念。
Javascript 中代碼的執(zhí)行上下文分為以下三種:
- 全局執(zhí)行上下文(Global Execution Context)- 這個(gè)是默認(rèn)的代碼運(yùn)行環(huán)境,一旦代碼被載入,引擎最先進(jìn)入的就是這個(gè)環(huán)境。
- 函數(shù)執(zhí)行上下文(Function Execution Context) - 當(dāng)執(zhí)行一個(gè)函數(shù)時(shí),運(yùn)行函數(shù)體中的代碼。
- Eval - 在 Eval 函數(shù)內(nèi)運(yùn)行的代碼。
javascript 是一個(gè)單線程語(yǔ)言,這意味著在瀏覽器中同時(shí)只能做一件事情。當(dāng) javascript 解釋器初始執(zhí)行代碼,它首先默認(rèn)進(jìn)入全局上下文。每次調(diào)用一個(gè)函數(shù)將會(huì)創(chuàng)建一個(gè)新的執(zhí)行上下文。
javascript執(zhí)行棧中不同執(zhí)行上下文之間的詞法環(huán)境有一種關(guān)聯(lián)關(guān)系,從棧頂?shù)綏5祝◤木植恐钡饺郑?這種關(guān)系被叫做
作用域鏈。
簡(jiǎn)單的說(shuō),每次你試圖訪問函數(shù)執(zhí)行上下文中的變量時(shí),進(jìn)程總是從自己上下文環(huán)境中開始查找。如果在自己的上下文中沒發(fā)現(xiàn)要查找的變量,繼續(xù)搜索下一層上下文。它將檢查執(zhí)行棧中每一個(gè)執(zhí)行上下文環(huán)境,尋找和變量名稱匹配的值,直到找到為止,如果到全局都沒有則拋出錯(cuò)誤。
執(zhí)行上下文的創(chuàng)建過程
我們現(xiàn)在已經(jīng)知道,每當(dāng)調(diào)用一個(gè)函數(shù)時(shí),一個(gè)新的執(zhí)行上下文就會(huì)被創(chuàng)建出來(lái)。然而,在 javascript 引擎內(nèi)部,這個(gè)上下文的創(chuàng)建過程具體分為兩個(gè)階段:
創(chuàng)建階段 > 執(zhí)行階段
創(chuàng)建階段
執(zhí)行上下文在創(chuàng)建階段創(chuàng)建。在創(chuàng)建階段發(fā)生以下事情:
- LexicalEnvironment 組件已創(chuàng)建。
- VariableEnvironment 組件已創(chuàng)建。
因此,執(zhí)行上下文可以在概念上表示如下:
ExecutionContext = {
LexicalEnvironment = <詞法環(huán)境>,
VariableEnvironment = <變量環(huán)境>,
}
詞法環(huán)境(Lexical Environment)
官方 ES6 文檔將詞法環(huán)境定義為:
詞法環(huán)境是一種規(guī)范類型,基于 ECMAScript 代碼的詞法嵌套結(jié)構(gòu)來(lái)定義標(biāo)識(shí)符與特定變量和函數(shù)的關(guān)聯(lián)關(guān)系。詞法環(huán)境由環(huán)境記錄(environment record)和可能為空引用(null)的外部詞法環(huán)境組成。
簡(jiǎn)而言之,詞法環(huán)境是一個(gè)包含標(biāo)識(shí)符變量映射的結(jié)構(gòu)。(這里的標(biāo)識(shí)符表示變量/函數(shù)的名稱,變量是對(duì)實(shí)際對(duì)象【包括函數(shù)類型對(duì)象】或原始值的引用)
詞法環(huán)境有兩種類型
- 全局環(huán)境(在全局執(zhí)行上下文中)是一個(gè)沒有外部環(huán)境的詞法環(huán)境。全局環(huán)境的外部環(huán)境引用為 null。它擁有一個(gè)全局對(duì)象(window 對(duì)象)及其關(guān)聯(lián)的方法和屬性(例如數(shù)組方法)以及任何用戶自定義的全局變量,this 的值指向這個(gè)全局對(duì)象。
- 函數(shù)環(huán)境,用戶在函數(shù)中定義的變量被存儲(chǔ)在環(huán)境記錄中。對(duì)外部環(huán)境的引用可以是全局環(huán)境,也可以是包含內(nèi)部函數(shù)的外部函數(shù)環(huán)境。
每個(gè)詞匯環(huán)境都有三個(gè)組成部分:
1)環(huán)境記錄(environment record)
2)對(duì)外部環(huán)境的引用(outer)
3) 綁定 this
環(huán)境記錄 同樣有兩種類型(如下所示):
- 聲明性環(huán)境記錄 存儲(chǔ)變量、函數(shù)和參數(shù)。一個(gè)函數(shù)環(huán)境包含聲明性環(huán)境記錄。
- 對(duì)象環(huán)境記錄 用于定義在全局執(zhí)行上下文中出現(xiàn)的變量和函數(shù)的關(guān)聯(lián)。全局環(huán)境包含對(duì)象環(huán)境記錄
抽象地說(shuō),詞法環(huán)境在偽代碼中看起來(lái)像這樣:
詞法環(huán)境和環(huán)境記錄值是純粹的規(guī)范機(jī)制,ECMAScript 程序不能直接訪問或操縱這些值。
GlobalExectionContext = {
// 詞法環(huán)境
LexicalEnvironment:{
// 功能環(huán)境記錄
EnvironmentRecord:{
Type:"Object",
// Identifier bindings go here
}
outer:<null>,
this:<global object>
}
}
FunctionExectionContext = {
LexicalEnvironment:{
EnvironmentRecord:{
Type:"Declarative",
// Identifier bindings go here
}
outer:<Global or outer function environment reference>,
this:<取決于函數(shù)的調(diào)用方式>
}
}
變量環(huán)境:
它也是一個(gè)詞法環(huán)境,其 EnvironmentRecord 包含了由 VariableStatements 在此執(zhí)行上下文創(chuàng)建的綁定。
如上所述,變量環(huán)境也是一個(gè)詞法環(huán)境,因此它具有上面定義的詞法環(huán)境的所有屬性。
在 ES6 中,LexicalEnvironment 組件和 VariableEnvironment 組件的區(qū)別在于前者用于存儲(chǔ)函數(shù)聲明和變量( let 和 const )綁定,而后者僅用于存儲(chǔ)變量( var )綁定。
讓我們結(jié)合一些代碼示例來(lái)理解上述概念:
let a = 20
const b = 30
var c
function multiply(e, f) {
var g = 20
return e * f * g
}
c = multiply(20, 30)
執(zhí)行上下文如下所示:
GlobalExectionContext = {
LexicalEnvironment:{
EnvironmentRecord:{
Type:"Object",
// Identifier bindings go here
a:<uninitialized>,
b:<uninitialized>,
multiply:<func>
},
outer:<null>,
ThisBinding:<Global Object>
},
VariableEnvironment:{
EnvironmentRecord:{
Type:"Object",
// Identifier bindings go here
c:undefined,
}
outer:<null>,
ThisBinding:<Global Object>
}
}
在執(zhí)行階段,完成變量賦值。因此,在執(zhí)行階段,全局執(zhí)行上下文將看起來(lái)像這樣。
// 執(zhí)行
GlobalExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
a: 20,
b: 30,
multiply: < func >
}
outer: <null>,
ThisBinding: <Global Object>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// Identifier bindings go here
c: undefined,
}
outer: <null>,
ThisBinding: <Global Object>
}
}
當(dāng) multiply(20, 30)遇到函數(shù)調(diào)用時(shí),會(huì)創(chuàng)建一個(gè)新的函數(shù)執(zhí)行上下文來(lái)執(zhí)行函數(shù)代碼。因此,在創(chuàng)建階段,函數(shù)執(zhí)行上下文將如下所示:
// multiply 創(chuàng)建
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
g: undefined
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>
}
}
在此之后,執(zhí)行上下文將執(zhí)行執(zhí)行階段,這意味著完成了對(duì)函數(shù)內(nèi)部變量的賦值。因此,在執(zhí)行階段,函數(shù)執(zhí)行上下文將如下所示:
// multiply 執(zhí)行
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
g: 20
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>
}
}
函數(shù)完成后,返回的值賦值給c。因此,全局詞法環(huán)境得到了更新。之后,全局代碼完成,程序結(jié)束。
注: 在執(zhí)行階段,如果 Javascript 引擎在源代碼中聲明的實(shí)際位置找不到 let 變量的值,那么將為其分配 undefined 值。
變量提升
在網(wǎng)上一直看到這樣的總結(jié): 在函數(shù)中聲明的變量以及函數(shù),其作用域提升到函數(shù)頂部,換句話說(shuō),就是一進(jìn)入函數(shù)體,就可以訪問到其中聲明的變量以及函數(shù)。這是對(duì)的,但是知道其中的緣由嗎?相信你通過上述的解釋應(yīng)該也有所明白了。不過在這邊再分析一下。
你可能已經(jīng)注意到了在創(chuàng)建階段 let 和 const 定義的變量沒有任何與之關(guān)聯(lián)的值,但 var 定義的變量設(shè)置為 undefined。
這是因?yàn)樵趧?chuàng)建階段,代碼會(huì)被掃描并解析變量和函數(shù)聲明,其中函數(shù)聲明存儲(chǔ)在環(huán)境中,而變量會(huì)被設(shè)置為 undefined(在 var 聲明變量的情況下)或保持未初始化(在 let 和const 聲明變量的情況下)。
這就是為什么你可以在聲明之前訪問var 定義的變量(盡管是 undefined ),但如果在聲明之前訪問let 和const 定義的變量就會(huì)提示引用錯(cuò)誤的原因。
這就是我們所謂的變量提升。
思考題:
console.log('step1:',a)
var a = 'artiely'
console.log('step2:',a)
function bar (a){
console.log('step3:',a)
a = 'TJ'
console.log('step4:',a)
function a(){
}
}
bar(a)
console.log('step5:',a)
對(duì)外部環(huán)境的引用
上面代碼如果我們改用調(diào)用方式如下:
let a = 20
const b = 30
var c
function multiply() {
var g = 20
return a * b * g
}
c = multiply()
其實(shí)你會(huì)發(fā)現(xiàn)結(jié)果是一樣的
但是 multiply 的執(zhí)行上下文卻發(fā)生一些變化
// 創(chuàng)建
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
Arguments: { length: 0},
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
g: undefined
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>
}
}
// 執(zhí)行
FunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
Arguments: { length: 0},
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// Identifier bindings go here
g: 20
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>
}
}
multiply() 執(zhí)行的時(shí)候會(huì)直接在 outer: <GlobalLexicalEnvironment>,中查找a,b
對(duì)外部環(huán)境的引用意味著它可以訪問其外部詞法環(huán)境。這意味著如果在當(dāng)前詞法環(huán)境中找不到它們,JavaScript 引擎可以在外部環(huán)境中查找變量。這就是之前說(shuō)的 作用域鏈
閉包
MDN 解釋 閉包是由函數(shù)以及創(chuàng)建該函數(shù)的詞法環(huán)境組合而成。這個(gè)環(huán)境包含了這個(gè)閉包創(chuàng)建時(shí)所能訪問的所有局部變量
這是我認(rèn)為對(duì)閉包最合理的解釋了,就看你怎么理解閉包的機(jī)制了。
其實(shí)閉包與作用域鏈有著密切的關(guān)系。
首先我們來(lái)看看什么樣的代碼會(huì)產(chǎn)生閉包。
function foo() {
var name = 'artiely'
function bar() {
console.log(`hello `)
}
bar()
}
foo()
以上代碼是有閉包嗎?沒有~
function foo() {
var name = 'artiely'
function bar() {
console.log(`hello ${name}`)
}
bar()
}
foo()
我們只做了微小的調(diào)整,現(xiàn)在就有閉包了,我們只是在bar中加入了name得引用
上面的代碼還可以寫成這樣
// 或者
function foo() {
var name = 'artiely'
return function bar() {
console.log(`hello ${name}`)
}
}
foo()()
對(duì)于閉包的形成我進(jìn)行了如下的幾點(diǎn)歸納
- A 函數(shù)內(nèi)必須有 B 函數(shù)的聲明;
- B 函數(shù)必須引用 A 函數(shù)的變量;
- B 函數(shù)被調(diào)用(當(dāng)然前提是 A 函數(shù)被調(diào)用)
以上 3 點(diǎn)缺一不可
我們來(lái)分析一下上面代碼的執(zhí)行上下文
// 創(chuàng)建
fooFunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
Arguments: { length: 0},
bar: < func >,
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
name: undefined
},
outer: <GlobalLexicalEnvironment>,
ThisBinding: <Global Object or undefined>
}
}
// 執(zhí)行 略
// 創(chuàng)建
barFunctionExectionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
Arguments: { length: 0},
},
outer: <fooLexicalEnvironment>,
ThisBinding: <Global Object or undefined>,
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
},
outer: <fooLexicalEnvironment>,
ThisBinding: <Global Object or undefined>
}
}
// 執(zhí)行 略
這里因?yàn)?code>bar的創(chuàng)建存在著對(duì)fooLexicalEnvironment里變量的引用,雖然foo可能執(zhí)行已結(jié)束但變量不會(huì)被回收。這種機(jī)制被叫做閉包
閉包是由函數(shù)以及創(chuàng)建該函數(shù)的詞法環(huán)境組合而成。這個(gè)環(huán)境包含了這個(gè)閉包創(chuàng)建時(shí)所能訪問的所有局部變量
我們結(jié)合上面例子重新分解一下這句話
閉包是由函數(shù)bar以及創(chuàng)建該函數(shù)foo的詞法環(huán)境組合而成。這個(gè)環(huán)境包含了這個(gè)閉包創(chuàng)建時(shí)所能訪問的所有局部變量name
但是從chrome的理解,閉包并沒有包含所能訪問的所有局部變量,僅僅包含所被引用的變量。
this 綁定
在全局執(zhí)行上下文中,值是 this 指全局對(duì)象。(在瀏覽器中,this 指的是 Window 對(duì)象)。
在函數(shù)執(zhí)行上下文中,值 this 取決于函數(shù)的調(diào)用方式。如果它由對(duì)象引用調(diào)用,則將值 this 設(shè)置為該對(duì)象,否則,將值 this 設(shè)置為全局對(duì)象或 undefined(在嚴(yán)格模式下)。例如:
let person = {
name: 'peter',
birthYear: 1994,
calcAge: function() {
console.log(2018 - this.birthYear)
}
}
person.calcAge()
// 'this' 指向 'person', 因?yàn)?'calcAge' 是被 'person' 對(duì)象引用調(diào)用的。
let calculateAge = person.calcAge
calculateAge()
// 'this' 指向全局 window 對(duì)象,因?yàn)闆]有給出任何對(duì)象引用
注意所有的()()自調(diào)用的函數(shù) this 都是指向Global Object的既瀏覽器中的window
最后
如果本文對(duì)你有幫助或覺得不錯(cuò)請(qǐng)幫忙點(diǎn)贊,如有疑問請(qǐng)留言。
其他參考:
https://hackernoon.com/execution-context-in-javascript-319dd72e8e2c
https://hackernoon.com/javascript-execution-context-and-lexical-environment-explained-528351703922