傻傻分不清的javascript运行机制
學(xué)習(xí)到j(luò)avascript的運行機(jī)制時,有幾個概念經(jīng)常出現(xiàn)在各種文章中且容易混淆。Execution Context(執(zhí)行環(huán)境或執(zhí)行上下文),Context Stack (執(zhí)行棧),Variable Object(VO: 變量對象),Active Object(AO: 活動對象),LexicalEnvironment(詞法環(huán)境),VariableEnvironment(變量環(huán)境)等,特別是 VO,AO以及LexicalEnvironment,VariableEnvironment的區(qū)別很多文章都沒有涉及到。因此我查看了一些國內(nèi)外的文章,結(jié)合自身理解寫下了下面的筆記。雖然因為自身不足導(dǎo)致理解上的偏差,但是依然相信讀完下文會對理解javascript的一些概念如變量提升,作用域和閉包有很大的幫助。
一, 執(zhí)行環(huán)境和執(zhí)行棧
了解javascript的運行機(jī)制,首先必須掌握兩個基本的概念。Execution Context(執(zhí)行環(huán)境或執(zhí)行上下文)和Context Stack (執(zhí)行棧)
1. 何為執(zhí)行環(huán)境(執(zhí)行上下文)(Execution Context)
我們知道javascript是單線程語言,也就是同一時間只能執(zhí)行一個任務(wù)。當(dāng)javascript解釋器初始化代碼后,默認(rèn)會進(jìn)入全局的執(zhí)行環(huán)境,之后每調(diào)用一個函數(shù),javascript解釋器會創(chuàng)建一個新的執(zhí)行環(huán)境。
var a = 1; // 1.初始化默認(rèn)進(jìn)入全局執(zhí)行環(huán)境function b() { // 3.進(jìn)入b 的執(zhí)行環(huán)境function c() { // 5. 進(jìn)入c的執(zhí)行環(huán)境···}c() // 4.在b的執(zhí)行環(huán)境里調(diào)用c, 創(chuàng)建c的執(zhí)行環(huán)境}b() // 2. 調(diào)用b 創(chuàng)建 b 的執(zhí)行環(huán)境 執(zhí)行環(huán)境的分類:
- 全局執(zhí)行環(huán)境:簡單的理解,一個程序只有一個全局對象即window對象,全局對象所處的執(zhí)行環(huán)境就是全局執(zhí)行環(huán)境。
- 函數(shù)執(zhí)行環(huán)境:函數(shù)調(diào)用過程會創(chuàng)建函數(shù)的執(zhí)行環(huán)境,因此每個程序可以有無數(shù)個函數(shù)執(zhí)行環(huán)境。
- Eval執(zhí)行環(huán)境:eval代碼特定的環(huán)境。
2. 如何單線程運行(Context Stack)
從一個簡單的例子開始講起
function foo(i) {if (i < 0) return;console.log('begin:' + i);foo(i - 1);console.log('end:' + i);
}
foo(2); 如何存儲代碼運行時的執(zhí)行環(huán)境(全局執(zhí)行環(huán)境,函數(shù)執(zhí)行環(huán)境)呢,答案是執(zhí)行棧。而棧遵循的是先進(jìn)后出的原理,javascript初始化完代碼后,首先會創(chuàng)建全局執(zhí)行環(huán)境并推入當(dāng)前的執(zhí)行棧,當(dāng)調(diào)用一個函數(shù)時,javascript引擎會創(chuàng)建新的執(zhí)行環(huán)境并推到當(dāng)前執(zhí)行棧的頂端,在新的執(zhí)行環(huán)境中,如果繼續(xù)發(fā)生一個新函數(shù)調(diào)用時,則繼續(xù)創(chuàng)建新的執(zhí)行環(huán)境并推到當(dāng)前執(zhí)行棧的頂端,直到再無新函數(shù)調(diào)用。最上方的函數(shù)執(zhí)行完成后,它的執(zhí)行環(huán)境便從當(dāng)前棧中彈出,并將控制權(quán)移交到當(dāng)前執(zhí)行棧的下一個執(zhí)行環(huán)境,直到全局執(zhí)行環(huán)境。當(dāng)程序或瀏覽器關(guān)閉時,全局環(huán)境也將退出并銷毀。
因此輸出的結(jié)果為:
begin:2
begin:1
begin:0
end:0
end:1
end:2 3. 如何創(chuàng)建執(zhí)行環(huán)境
我們現(xiàn)在知道每次調(diào)用函數(shù)時,javascript 引擎都會創(chuàng)建一個新的執(zhí)行環(huán)境,而如何創(chuàng)建這一系列的執(zhí)行環(huán)境呢,答案是執(zhí)行器會分為兩個階段來完成, 分別是創(chuàng)建階段和激活(執(zhí)行)階段。而即使步驟相同但是由于規(guī)范的不同,每個階段執(zhí)行的過程有很大的不同。
3.1 ES3 規(guī)范
創(chuàng)建階段:
- 1.創(chuàng)建作用域鏈。
- 2.創(chuàng)建變量對象VO(包括參數(shù),函數(shù),變量)。
- 3.確定this的值。
激活/執(zhí)行階段:
- 完成變量分配,執(zhí)行代碼。
3.2 ES5 規(guī)范
創(chuàng)建階段:
- 1.確定 this 的值。
- 2.創(chuàng)建詞法環(huán)境(LexicalEnvironment)。
- 3.創(chuàng)建變量環(huán)境(VariableEnvironment)。
激活/執(zhí)行階段:
- 完成變量分配,執(zhí)行代碼。
我們從規(guī)范上可以知道,ES3和ES5在執(zhí)行環(huán)境的創(chuàng)建階段存在差異,當(dāng)然他們都會在這個階段確定this 的值
(關(guān)于this 的指向問題我們以后會在專門的文章中分析各種this 的指向問題,這里便不做深究)。我們將圍繞這兩個規(guī)范不同點展開。盡管ES3的一些規(guī)范已經(jīng)被拋棄,但是掌握ES3 創(chuàng)建執(zhí)行環(huán)境的過程依然有助于我們理解javascript深層次的概念。
二, Variable Object(VO: 變量對象),Active Object(AO: 活動對象)
2.1 基本概念
VO 和 AO 是ES3規(guī)范中的概念,我們知道在創(chuàng)建過程的第二個階段會創(chuàng)建變量對象,也就是VO,它是用來存放執(zhí)行環(huán)境中可被訪問但是不能被 delete 的函數(shù)標(biāo)識符,形參,變量聲明等,這個對象在js環(huán)境下是不可訪問的。而AO 和VO之間區(qū)別就是AO 是一個激活的VO,僅此而已。
變量對象(Variable) object)是說JS的執(zhí)行上下文中都有個對象用來存放執(zhí)行上下文中可被訪問但是不能被delete的函數(shù)標(biāo)示符、形參、變量聲明等。它們會被掛在這個對象上,對象的屬性對應(yīng)它們的名字對象屬性的值對應(yīng)它們的值但這個對象是規(guī)范上或者說是引擎實現(xiàn)上的不可在JS環(huán)境中訪問到活動對象
激活對象(Activation object)有了變量對象存每個上下文中的東西,但是它什么時候能被訪問到呢?就是每進(jìn)入一個執(zhí)行上下文時,這個執(zhí)行上下文兒中的變量對象就被激活,也就是該上下文中的函數(shù)標(biāo)示符、形參、變量聲明等就可以被訪問到了
2.2 執(zhí)行細(xì)節(jié)
如何創(chuàng)建VO對象可以大致分為四步
- 1.創(chuàng)建arguments對象
- 2.掃描上下文的函數(shù)聲明(而非函數(shù)表達(dá)式),為發(fā)現(xiàn)的每一個函數(shù),在變量對象上創(chuàng)建一個屬性——確切的說是函數(shù)的名字——其有一個指向函數(shù)在內(nèi)存中的引用。如果函數(shù)的名字已經(jīng)存在,引用指針將被重寫。
- 3.掃描上下文的變量聲明,為發(fā)現(xiàn)的每個變量聲明,在變量對象上創(chuàng)建一個屬性——就是變量的名字,并且將變量的值初始化為undefined。如果變量的名字已經(jīng)在變量對象里存在,將不會進(jìn)行任何操作并繼續(xù)掃描。
注意: 整個過程可以大概描述成: 函數(shù)的形參=>函數(shù)聲明=>變量聲明, 其中在創(chuàng)建函數(shù)聲明時,如果名字存在,則會被重寫,在創(chuàng)建變量時,如果變量名存在,則忽略不會進(jìn)行任何操作。
一個簡單的例子
function foo(i) {var a = 'hello';var b = function privateB() {};function c() {}
}foo(22); 執(zhí)行的偽代碼
// 創(chuàng)建階段
fooExecutionContext = {scopeChain: { ... },variableObject: {arguments: {0: 22,length: 1},i: 22,c: pointer to function c()a: undefined,b: undefined},this: { ... }
}
// 激活階段
fooExecutionContext = {scopeChain: { ... },variableObject: {arguments: {0: 22,length: 1},i: 22,c: pointer to function c()a: 'hello',b: pointer to function privateB()},this: { ... }
} 三, LexicalEnvironment(詞法環(huán)境),VariableEnvironment(變量環(huán)境)
3.1 基本概念
詞法環(huán)境和變量環(huán)境是ES5以后提到的概念,官方對詞法環(huán)境的解釋如下。
詞法環(huán)境是一種規(guī)范類型,基于 ECMAScript 代碼的詞法嵌套結(jié)構(gòu)來定義標(biāo)識符與特定變量和函數(shù)的關(guān)聯(lián)關(guān)系。詞法環(huán)境由環(huán)境記錄(environment record)和可能為空引用(null)的外部詞法環(huán)境組成。
簡單的理解,詞法環(huán)境是一個包含標(biāo)識符變量映射的結(jié)構(gòu)。(這里的標(biāo)識符表示變量/函數(shù)的名稱,變量是對實際對象【包括函數(shù)類型對象】或原始值的引用)。
ES3的VO,AO為什么可以被拋棄?個人認(rèn)為有兩個原因,第一個是在創(chuàng)建過程中所執(zhí)行的創(chuàng)建作用域鏈和創(chuàng)建變量對象(VO)都可以在創(chuàng)建詞法環(huán)境的過程中完成。第二個是針對es6中存儲函數(shù)聲明和變量(let 和 const)以及存儲變量(var)的綁定,可以通過兩個不同的過程(詞法環(huán)境,變量環(huán)境)區(qū)分開來。
3.2 詞法環(huán)境(lexicalEnvironment)
詞法環(huán)境由兩個部分組成
- 環(huán)境記錄(enviroment record),存儲變量和函數(shù)聲明
- 對外部環(huán)境的引用(outer),可以通過它訪問外部詞法環(huán)境
對外部環(huán)境的引用關(guān)系到作用域鏈,之后再分析,我們先來看看環(huán)境記錄的分類。
環(huán)境記錄分兩部分
- 聲明性環(huán)境記錄(declarative environment records): 存儲變量、函數(shù)和參數(shù), 但是主要用于函數(shù) 、catch詞法環(huán)境。
注意:函數(shù)環(huán)境下會存儲arguments的值。而詳細(xì)的過程可以參考VO 的執(zhí)行細(xì)節(jié),基本大同小異 - 對象環(huán)境記錄(object environment records), 主要用于with 和全局的詞法環(huán)境
偽代碼如下
// 全局環(huán)境
GlobalExectionContext = {
// 詞法環(huán)境LexicalEnvironment: { EnvironmentRecord: { ···}outer: <null> }
}
// 函數(shù)環(huán)境
FunctionExectionContext = {
// 詞法環(huán)境LexicalEnvironment: { EnvironmentRecord: { // 包含argument}outer: <Global or outer function environment reference> }
} 3.3 變量環(huán)境(objectEnvironment)
變量環(huán)境也是個詞法環(huán)境,主要的區(qū)別在于lexicalEnviroment用于存儲函數(shù)聲明和變量( let 和 const )綁定,而ObjectEnviroment僅用于存儲變量( var )綁定。
3.4 偽代碼展示
ES5規(guī)范下的整個創(chuàng)建過程可以參考下方的偽代碼
let a = 20;
const b = 30;
var c;function d(e, f) { var g = 20; return e * f * g;
}c = d(20, 30); // 全局環(huán)境
GlobalExectionContext = {this: <Global Object>,// 詞法環(huán)境LexicalEnvironment: { EnvironmentRecord: { Type: "Object", // 環(huán)境記錄分類: 對象環(huán)境記錄a: < uninitialized >, // 未初始化b: < uninitialized >, d: < func > } outer: <null> },VariableEnvironment: { EnvironmentRecord: { Type: "Object", // 環(huán)境記錄分類: 對象環(huán)境記錄c: undefined, // undefined} outer: <null> }
}
// 函數(shù)環(huán)境
FunctionExectionContext = { this: <Global Object>,LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 環(huán)境記錄分類: 聲明環(huán)境記錄Arguments: {0: 20, 1: 30, length: 2}, // 函數(shù)環(huán)境下,環(huán)境記錄比全局環(huán)境下的環(huán)境記錄多了argument對象}, outer: <GlobalLexicalEnvironment> },VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // 環(huán)境記錄分類: 聲明環(huán)境記錄g: undefined }, outer: <GlobalLexicalEnvironment> }
} 四,作用域鏈
前面講創(chuàng)建過程中,我們留下了一個伏筆,ES3規(guī)范中有創(chuàng)建作用域鏈的過程,而ES5中在創(chuàng)建詞法環(huán)境或變量環(huán)境的過程中,也有生成外部環(huán)境的引用的過程。那這個過程有什么作用呢。我們通過一個簡單的例子來說明。
function one() {var a = 1;two();function two() {var b = 2;three();function three() {var c = 3;alert(a + b + c); // 6}}}one(); 當(dāng)執(zhí)行到three 的執(zhí)行環(huán)境時,此時 a和b 都不在c 的變量內(nèi),因此作用域鏈則起到了引用外部執(zhí)行環(huán)境變量的作用。ES3中創(chuàng)建的作用域鏈如圖:
當(dāng)解釋器執(zhí)行alert(a + b + c),他首先會找自身執(zhí)行環(huán)境下是否有a這個變量的存在,如果不存在,則通過查看作用域鏈,判斷a是否在上一個執(zhí)行環(huán)境內(nèi)部。它檢查是否a存在于內(nèi)部,若找不到,則沿著作用域鏈往上一個執(zhí)行環(huán)境找,直到找到,或者到頂級的全局作用域。同理ES6規(guī)范中也可以這樣分析。
因此這會引入一個javascript一個重要的概念,閉包。從上面對執(zhí)行環(huán)境的解釋我們可以這樣理解,閉包就是內(nèi)部環(huán)境通過作用域鏈訪問到上層環(huán)境的變量。因此也存在無法進(jìn)行變量回收的問題,只要函數(shù)的作用域鏈在,變量的值便因為閉包無法被回收。
注意: 此作用域鏈和原型鏈的作用域鏈不是同一個概念。
五, 小結(jié)
通過對javascript運行機(jī)制的介紹,對一些javasript高級概念有了更深的認(rèn)識,特別是對一些云里霧里的概念區(qū)別有了更深刻的認(rèn)識。不同規(guī)范下,不同概念的解釋更有利于深挖javascript底層的執(zhí)行思想。我相信這是理解javascipt語言最重要的一步。
參考資料:
- https://stackoverflow.com/questions/20139050/what-really-is-a-declarative-environment-record-and-how-does-it-differ-from-an-a#
- http://davidshariff.com/blog/what-is-the-execution-context-in-javascript/
- http://davidshariff.com/blog/javascript-scope-chain-and-closures/
- https://github.com/yued-fe/y-translation/blob/master/en/understanding-execution-context-and-execution-stack-in-javascript.md
本文為博主原創(chuàng)文章,轉(zhuǎn)載請注明出處 https://juejin.im/post/5c20526b6fb9a049b7805ff9
轉(zhuǎn)載于:https://www.cnblogs.com/kidflash/p/10168153.html
總結(jié)
以上是生活随笔為你收集整理的傻傻分不清的javascript运行机制的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python3模块Crypto改为pyc
- 下一篇: 注解--python库--matplot