本文內容主要涵蓋了執行上下文棧、執行上下文、變量對象、函數變量提高等內容。java
衆所周知,JavaScript
是單線程編程語言,同一時間只能作一件事情,程序執行順序由上而下,程序的執行主要依託JavaScript
引擎,JavaScript
引擎也並不是一行一行的分析執行代碼,而是一段一段的分析執行。git
JavaScript
引擎執行的代碼固然是可執行代碼,在JavaScript
中可執行代碼有三類:全局代碼、函數代碼以及Eval
代碼。github
JavaScript
程序的執行主要分語法檢查和運行兩個階段,語法檢查包括詞法分析和語法分析,目的是將JavaScript
高級語言程序轉成抽象語法樹。面試
語法檢查完成後,到了執行階段,執行階段包括預解析和執行,預解析首先會建立執行上下文(本文重點),將語法檢查正確後生成的抽象語法樹複製到當前執行上下文中,而後作屬性填充,對語法樹當中的變量名、函數聲明以及函數的形參進行屬性填充。最後就是執行。編程
JavaScript
運行原理會在後面的文章輸出,不是本文的重點,本文只需知道程序運行的大體是怎樣的過程,執行上下文什麼時候建立。數組
何爲執行上下文棧???瀏覽器
在JavaScript
解釋器運行階段(預解析)還維護了一個棧,用於管理執行上下文。在執行上下文棧中,全局執行上下文處於棧底,頂部爲當前的執行上下文。當頂部的執行完成,就會彈出棧,相似於數據結構中的棧,每當有當前的執行上下文執行完就會從棧頂彈出,這種管理執行上下文的棧叫作執行上下文棧。bash
一個執行上下文能夠激活另外一個執行上下文,相似於函數調用另外一個函數,能夠一層一層的調用下去。數據結構
激活其它執行上下文的某執行上下文被稱爲調用者(caller
),被激活的執行上下文被稱爲被調用者(callee
)。一個執行上下文便可能是調用者也有多是被調用者。dom
當一個caller
激活了一個callee
時,caller
會暫停自身的執行,將控制權交給callee
,此時該callee
被放進執行上下文棧,稱爲進行中的上下文,當這個callee
上下文結束後,把控制權交還給它的caller
,caller
會在剛纔暫停的地方繼續執行。在這個caller
結束後,會繼續觸發其餘的上下文。
執行上下文棧在JavaScript
中能夠數組模擬:
ECStack = [];
複製代碼
當瀏覽器首次載入腳本,會默認先進入到全局執行上下文,位於執行上下文棧的最底部,此時全局代碼會開始初始化,初始化生成相應的對象和函數,在全局上下文執行的過程當中可能會激活一些其餘的方法(若是有的話),而後進入它們的執行上下文,並將元素壓入執行上下文棧中。能夠把全部的程序執行看做一個執行上下文棧,棧的頂部是正在激活的上下文。以下表所示:
EC stack |
---|
Active EC |
... |
EC |
Global EC |
在程序結束以前,ECStack
最底部永遠是globalContext
:
ECStack = [
globalContext
];
複製代碼
看看下面實例一,是一個怎麼的過程:
// 實例一
function bar() {
console.log('bar');
}
function foo() {
bar();
}
foo();
複製代碼
當執行一個函數時,會建立一個執行上下文並壓入執行上下文棧中,當函數執行完畢,就將該執行上下文彈出執行上下文棧。
// 建立執行上下文棧
ECStack = [];
// foo() 建立該函數執行上下文並壓入棧中
ECStack.push(<foo> functionContext);
// foo()中調用了bar(),建立bar()執行上下文並壓入棧中
ECStack.push(<bar> functionContext);
// bar()執行完畢彈出
ECStack.pop();
// foo()執行完畢彈出
ECStack.pop();
複製代碼
執行上下文在程序運行的預解析階段建立,預解析也就是代碼的真正的執行前,能夠說是代碼執行前的準備工做,即建立執行上下文。
執行上下文有何用,主要作了三件事:
this
、做用域
和做用域鏈
也是JavaScript
中很重要的知識點,後面的文章會詳細的輸出。
何爲執行上下文?
執行上下文理解爲是執行環境的抽象概念,當JavaScript
代碼執行一段可執行代碼時,都會建立對應的執行上下文,一個執行上下文能夠抽象的理解爲object
,都包括三個重要屬性:
executionContext: {
variable object:vars, functions, arguments
scope chain: variable object + all parents scopes
thisValue: context object
}
複製代碼
全局代碼不包含函數內代碼,在初始化階段,執行上下文棧底部有一個全局執行上下文:
ECStack = [
globalContext
];
複製代碼
當進入函數代碼時,函數執行,建立該函數執行上下文並壓入棧中。須要注意的是函數代碼不包含內部函數代碼。
ECStack = [
<xxx> functionContext
...
<xxx> functionContext
globalContext
];
複製代碼
eval(...)
有些陌生,平時也不多用到,eval(...)
函數能夠接受一個字符串爲參數,並將其中的內容視爲好像在書寫就存在於程序中這個位置的代碼。
換句話說,能夠在你寫的代碼中用程序生成代碼並運行,就好像是寫在那個位置的同樣。
eval('var x = 10');
(function foo() {
eval('var y = 20');
})();
console.log(x); // 10
console.log(y); // 'y is not defined'
複製代碼
上面實例執行過程:
ECStack = [
globalContext
];
// eval('var x = 10')進棧
ECStack.push(
evalContext,
callingContext: globalContext
);
// eval出棧
ECStack.pop();
// foo funciton 進棧
ECStack.push(<foo> functionContext);
// eval('var y = 20') 進棧
ECStack.push(
evalContext,
callingContext: <foo> functionContext
);
// eval出棧
ECStack.pop();
// foo 出棧
ECStack.pop();
複製代碼
變量對象(variable object
)是與執行上下文相關的數據做用域(scope of data
),是與上下文相關的特殊對象,用與存儲被定義在上下文中的變量(variables
)和函數聲明(function declarations
)。變量對象是一個抽象的概念,不一樣的上下文,它表示使用不一樣的對象。
全局變量對象是全局上下文的變量對象。全局變量對象就是全局對象,爲啥這麼說:
全局對象(
Global object
) 是在進入任何執行上下文以前就已經建立了的對象;這個對象只存在一份,它的屬性在程序中任何地方均可以訪問,全局對象的生命週期終止於程序退出那一刻。
- 全局對象初始建立階段將
Math
、String
、Date
、parseInt
做爲自身屬性,等屬性初始化,一樣也能夠有額外建立的其它對象做爲屬性(其能夠指向到全局對象自身)- 在
DOM
中,全局對象的window
屬性就能夠引用全局對象自身- 能夠經過全局上下文的
this
來訪問全局對象,一樣也能夠遞歸引用自身- 當訪問全局對象的屬性時一般會忽略掉前綴,全局對象是不能經過名稱直接訪問的
global = {
Math: <...>, String: <...>, Date: <...>, ... window: global } console.log(Math.random()); //當訪問全局對象的屬性時一般會忽略掉前綴;初始建立階段將Math等做爲自身屬性 console.log(this.Math.random()); // 經過this來訪問全局對象 console.log(this) // window 經過全局上下文的this來訪問全局對象 var a = 1; console.log(this.a); // 1 console.log((window.a); // 1 全局對象有 window 屬性指向自身 console.log(a); // 1 當訪問全局對象的屬性時一般會忽略掉前綴 this.window.b = 2; console.log(this.b); // 2 複製代碼
上面的全局對象的定義和變量對像的定義對比,能知道全局變量對象就是全局對象,簡單的說,由於變量對象用於存儲被定義在上下文中的變量和函數聲明,全局對象在進入任何執行上下文前就已經建立了,一樣存儲着在全局範圍內定義的變量和函數聲明。
須要注意的是全局上下文的變量對象容許經過VO
屬性名稱來間接訪問,緣由就是全局變量對象就是全局對象,在其餘上下文中是不能直接VO
對象。
全局變量對象VO
會有下列屬性:
FunctionDeclaration
, FD
)var
, VariableDeclaration
)Variable object
) 當進入執行上下文時,VO
包含來下列屬性:
undefined
;FunctionDeclaration
, FD
),由名稱和對應值組成一個變量對象的屬性被建立;若是變量對象已經存在相同屬性名稱,則徹底替換這個屬性。var
, VariableDeclaration
),由名稱和對應值(undefined
)組成一個變量對象的屬性被建立;若是變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性。function foo(a) {
var b = 2;
var c = function() {};
function bar() {
console.log('bar');
}
}
foo(10);
複製代碼
當進入帶有參數10的foo
函數執行上下文時,VO
:
VO = {
a: 10,
bar: <reference to FunctionDeclaration 'bar'>,
b: undefined,
c: undefined
}
複製代碼
在函數聲明過程當中,若是變量對象已經存在相同的屬性名稱,則徹底替換這個屬性:
function foo(a) {
console.log(a);
function a() {}
}
foo(10) // function a(){}
複製代碼
在變量聲明過程當中,若是變量名稱跟已經聲明的形式參數或函數相同,則變量聲明不會干擾已經存在的這類屬性
// 與參數名同名
function foo(a) {
console.log(a);
var a = 20;
}
foo(10) // 10
// 與函數名同名
function bar(){
console.log(a)
var a = 10
function a(){}
}
bar() // 'function a(){}'
複製代碼
VO
建立過程當中,函數形參的優先級是高於函數的聲明的,結果是函數體內部聲明的function a(){}
覆蓋了函數形參a
的聲明,所以最後輸出a
是一個function
。
從上面的實例說明,函數聲明比變量聲明的優先級高,在定義的過程當中不會被變量覆蓋,除非是賦值:
function foo(a){
var a = 10
function a(){}
console.log(a)
}
foo(20) // 10
function bar(a){
var a
function a(){}
console.log(a)
}
bar(20) // 'function a(){}'
複製代碼
Activation object
)活動對象想必你們對這個概念都不陌生,可是容易和變量對象混淆。
活動對象就是函數上下文中的變量對象,只是不一樣階段的不一樣叫法,在建立函數執行上下文階段,變量對象被建立,變量對象的屬性不能被訪問,此時的函數尚未執行,當函數來到執行階段,變量對象被激活,變成了活動對象,而且裏面的屬性都能訪問到,開始進行執行階段的操做。
// 執行階段
VO -> AO
function foo(a) {
var b = 2;
var c = function() {};
function bar() {
console.log('bar');
}
}
foo(10);
VO = {
arguments: {
0: 10,
length: 1
},
a: 10,
bar: <reference to FunctionDeclaration 'bar'>,
b: undefined,
c: undefined
}
AO = {
arguments: {
0: 10,
length: 1
},
a: 10,
bar: <reference to FunctionDeclaration 'bar'>,
b: 2,
c: reference to FunctionExpression "c"
}
複製代碼
調用函數時,會爲其建立一個Arguments
對象,並自動初始化局部變量arguments
,指代該Arguments
對象。全部做爲參數傳入的值都會成爲Arguments對象的數組元素。
簡潔的總結下上面的內容:
Arguments
對象;提高一個很常見的話題,是面試中常常被問到的一部分,函數聲明優先級比變量聲明高,這句話應該是大部分同窗都會回答,爲啥,上面的內容已經很好的作出瞭解釋。看下面實例:
function test() {
console.log(foo); // function foo(){}
console.log(bar); // undefined
var foo = 'Hello';
console.log(foo); // Hello
var bar = function () {
return 'world';
}
function foo() {
return 'hello';
}
}
test();
複製代碼
// 建立階段
VO = {
arguments: {
length: 0
},
foo: <reference to FunctionDeclaration 'foo'>, // 解釋了第一個輸出是foo引用,函數聲明優先變量被建立,同名屬性不會被幹擾,在函數尚未被調用前已經被建立了,即能輸出foo的引用
bar: undefined // 解釋了第二個輸出是undefined,函數表達式仍是隻是一個變量聲明,不是函數聲明,不會被提高
}
複製代碼
// 執行階段
VO -> OV
OV = {
arguments: {
length: 0
},
foo: 'Hello', // 這裏解釋了爲何第三個輸出值爲‘Hello’,作了賦值操做
bar: reference to FunctionExpression "bar"
}
複製代碼
// 實例真實的執行順序
function test() {
function foo() {
return 'hello';
}
}
var foo;
var bar;
console.log(foo);
console.log(bar);
foo = 'Hello';
console.log(foo);
bar = function () {
return 'world';
}
}
複製代碼
須要注意的是變量提高只存在使用var
關鍵字聲明變量,若是是使用let
聲明變量不存在變量提高。
聲明變量的做用域限制在其聲明位置的上下文中,在上下文被建立的階段時建立了,若是沒有聲明的變量老是全局的,而且是在執行階段將賦值給未聲明的變量的值被隱式建立爲全局變量,能夠經過delete
操做符刪除,聲明變量不能夠。
function foo() {
console.log(a); // Uncaught ReferenceError: a is not defined;a不存在VO中
a = 1;
console.log(a);
}
foo();
function bar() {
a = 1;
console.log(a); // 1 能夠在全局變量中找到a的值
}
bar();
c = 10;
console.log(delete c); // true
var d = 10;
console.log(delete d); // false
複製代碼
若是清楚上下文相關的內容,提高的問題很好的能解答,在學習中咱們仍是須要了解一些底層的知識,這樣有助咱們更好的進步。
文章若有不正確的地方歡迎各位大佬指正,也但願有幸看到文章的同窗也有收穫,一塊兒成長!
-----------------------------本文首發於我的公衆號---------------------------
最後,歡迎你們關注公衆號 ZeroToOneMe,或者 github博客,一塊兒學習交流。