JavaScript中執行環境和棧

在這篇文章中,我會深刻理解JavaScript最根本的組成之一 : "執行環境(執行上下文)"。文章結束後,你應該對解釋器試圖作什麼,爲何一些函數/變量在未聲明時就能夠調用而且他們的值是如何肯定的有一個清晰的認識。javascript

 

什麼是執行環境(執行上下文)
當代碼在JavaScript中運行的時候,代碼在環境中被執行是很是重要的,它會被評估爲如下之一類型來運行:
全局代碼:默認環境,你的代碼第一時間在這兒運行。
函數代碼:當執行流進入一個函數體的時候。
Eval代碼:在eval()函數中的文本。java

你能夠在網上查找關於做用域的大量資料,這篇文章的目的就是讓事情變得更容易理解。讓咱們把執行環境做爲環境/做用域,當前代碼被評估在這個環境/做用域中。如今,讓咱們來看一個例子,代碼被評估某個類型,這個例子中類型包括全局和函數環境:瀏覽器

這裏並無什麼特別的,咱們有一個全局環境,全局環境由紫色邊框表示,還有三個不一樣的函數環境分別由綠色邊框,藍色邊框和橙色邊框表示。這裏只能由一個全局環境,在你的程序中,全局環境能夠被其餘環境訪問。函數

你能夠由不少的函數環境,每一個函數都會建立一個新的函數環境,在新的函數環境中,會建立一個私有做用域,在這個函數中建立的任何聲明都不能被當前函數做用域以外的地方訪問。在上面例子中,一個函數能夠訪問當前環境外部定義的變量,可是在外部卻沒法訪問函數內部聲明的變量。爲何這樣?這段代碼到底是如何評估的?this


 執行環境棧線程

JavaScript解釋器在瀏覽器中是單線程的,這意味着瀏覽器在同一時間內只執行一個事件,對於其餘的事件咱們把它們排隊在一個稱爲 執行棧的地方。下表是一個單線程棧的抽象視圖。3d

咱們已經知道,當瀏覽器第一次加載你的script,它默認的進了全局執行環境。若是在你的全局代碼中你調用了一個函數,那麼順序流就會進入到你調用的函數當中,建立一個新的執行環境而且把這個環境添加到執行棧的頂部。指針

若是你在當前的函數中調用了其餘函數,一樣的事會再次發生。執行流進入內部函數,而且建立一個新的執行環境,把它添加到已經存在的執行棧的頂部。瀏覽器始終執行當前在棧頂部的執行環境。一旦函數完成了當前的執行環境,它就會被彈出棧的頂部, 把控制權返回給當前執行環境的下個執行環境。下面例子展現了一個遞歸函數和該程序的執行棧:
```javascript
(function foo(i) {
if (i === 3) {
return;
}
else {
foo(++i);
}
}(0));
```對象


這段代碼簡單地調用了本身三次,由1遞增i的值。每次函數foo被調用,一個新的執行環境就會被調用。一旦一個環境完成了執行,它就會被彈出執行棧而且把控制權返回給當前執行環境的下個執行環境直到再次到達全局執行環境blog

記住執行棧,這兒有五個關鍵點 
1. 單線程
2. 同步執行
3. 一個全局環境
4. 無限的函數環境
5. 函數被調用就會建立一個新的執行環境,甚至調用本身。


 執行環境的詳情

如今咱們直到,一個函數被調用就會建立一個新的執行環境。然而解釋器的內部,每次調用執行環境會有兩個階段:

1. 建立階段
- 當函數被調用,可是爲執行內部代碼以前:
- 建立一個[做用域鏈](http://davidshariff.com/blog/javascript-scope-chain-and-closures/)。
- 建立變量,函數和參數。
- 肯定this的值。

2. 激活/代碼執行階段
> - 賦值,引用函數,解釋/執行代碼。

這可能意味着每一個執行環境在概念上做爲一個對象並帶有三個屬性
```javascript
executionContextObj = {
scopeChain: { /* variableObject + all parent execution context's variableObject */ },
//做用域鏈:{變量對象+全部父執行環境的變量對象}
variableObject: { /* function arguments / parameters, inner variable and function declarations */ },
//變量對象:{函數形參+內部的變量+函數聲明(但不包含表達式)}
this: {}
}
```
----
### 活動/變量 對象(AO/VO)
當函數被調用,executionContextObj就被建立,該對象在實際函數執行前就已建立。這就是已知的第一個階段建立階段.在第一階段,解釋器建立了executionContextObj對象,經過掃描函數,傳遞形參,函數聲明和局部變量聲明。掃描的結果成爲了變量對象executionContextObj中。

- 這有一個解釋器是如何評估代碼的僞概述:
1. 找到一些代碼來調用函數
2. 在執行函數代碼前,建立執行環境
3. 進入建立階段:
- 初始化做用域鏈
- 建立變量對象:
- 建立arguments對象,檢查環境中的參數,初始化名和值,建立一個參考副本
- 掃描環境中內的函數聲明:
- 某個函數被發現,在變量對象建立一個屬性,它是函數的確切名。它是一個指針在內存中,指向這個函數。
- 若是這個函數名已存在,這個指針的值將會重寫。
- 掃描環境內的變量聲明
- 某個變量聲明被發現,在變量對象中建立一個屬性,他是變量的名,初始化它的值爲undefined
- 若是變量名在變量對象中已存在,什麼也不作,繼續掃描。
- 在環境中肯定this的值。
4. 激活/代碼執行階段:在當前上下文上運行/解釋函數代碼,並隨着代碼一行行執行指派變量的值

看下面例子:
```javascript
function foo(i) {
var a = 'hello';
var b = function privateB() {

};
function c() {

}
}

foo(22);
```
On calling foo(22), the creation stage looks as follows:
在調用foo(22)時,建立階段像下面這樣:
```javascript
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: undefined,
b: undefined
},
this: { ... }
}
```
正如你看到的,建立階段處理了定義屬性的名,可是並不把值賦給變量,不包括形參和實參。一旦建立階段完成,執行流進入函數而且激活/代碼執行階段,在函數執行結束以後,看起來像這樣:
```javascript
fooExecutionContext = {
scopeChain: { ... },
variableObject: {
arguments: {
0: 22,
length: 1
},
i: 22,
c: pointer to function c()
a: 'hello',
b: pointer to function privateB()
},
this: { ... }
}
```
### 進階一言

你能夠在網上找到大量的術語來描述JavaScript進階。解釋變量和函數聲明被提高到它們函數做用域的頂端。然而,沒有一個詳細的解釋爲何這樣, 如今你配備了關於解釋器怎麼建立活動對象的新知識,這會很明白這是爲何。看看下面例子:
```javascript
​(function() {

console.log(typeof foo); // function pointer
console.log(typeof bar); // undefined

var foo = 'hello',
bar = function() {
return 'world';
};

function foo() {
return 'hello';
}

}());​
```
如今咱們能解答的問題有:

爲何在聲明foo以前咱們就能夠調用?
若是咱們按照建立階段進行,咱們知道變量在激活/執行階段以前已經被建立了。所以,在函數流開始執行,foo已經在活動對象中被定義了。
foo被聲明瞭兩次, 爲何foo展示出來的是functiton,而不是undefined或者string
咱們從建立階段知道,儘管foo被聲明瞭兩次,函數在活動對象中是在變量以前被建立的,而且若是屬性名在活動對象已經存在,咱們會簡單地繞過這個聲明。

因此,引用函數foo()是在活動對象上第一次被建立的, 當咱們解釋到 var foo的時候,咱們發現屬性名foo已經存在,因此代碼不會作任何處理,只是繼續進行

爲何bar是undefined?

bar確實是一個變量,而且值是一個函數。咱們知道變量是在建立階段被建立的,可是它們的值被初始化爲undefined


總結:
但願如今你能很好的理解JavaScript解釋器是如何評估你的代碼。
瞭解執行環境和棧可讓你知道你的代碼評估的值爲什麼與你預期值不一樣的緣由。

你認爲解釋器內部工做開支太多?或者你應該有必要的JavaScript知識?瞭解了執行環境可能會幫助你寫出更好的JavaScript?

[原文地址](http://davidshariff.com/blog/what-is-the-execution-context-in-javascript/)

相關文章
相關標籤/搜索