詳釋JavaScript執(zhí)行環(huán)境與執(zhí)行棧
執(zhí)行環(huán)境
執(zhí)行環(huán)境 ( 也稱"執(zhí)行上下文" ) 可以說是 JavaScript 最重要的一個概念。那么執(zhí)行環(huán)境到底是什么呢?一句話就可以概括:代碼 ( 包括函數(shù) ) 執(zhí)行時所需要的所有信息就是執(zhí)行環(huán)境。由于 ES 歷經(jīng)多個版本,所以執(zhí)行環(huán)境的標準也一直在變,下面列出了三個主要的版本內(nèi)容:
ES3 標準中的執(zhí)行環(huán)境
- scope:作用域,如果有作用域嵌套的情況就稱作"作用域鏈"。
- variable object:變量對象,用于存儲標識符的特殊對象。
- this value:this 值。
*標識符:包括變量、函數(shù)名、屬性名和函數(shù)的參數(shù)。
ES5 標準中的執(zhí)行環(huán)境
- variable environment:變量環(huán)境,當聲明變量時使用。
- lexical environment:詞法環(huán)境,當獲取標識符值時使用。
- this value:this 值。
ES6 標準中的執(zhí)行環(huán)境
- variable environment:變量環(huán)境,當聲明變量時使用。
- lexical environment:詞法環(huán)境,當獲取標識符值或者 this 值時使用。
*在 ES6 中,執(zhí)行環(huán)境中實際增加了不少內(nèi)容,我們這里只介紹了普通函數(shù)執(zhí)行時所需要的內(nèi)容。
執(zhí)行棧
當打開網(wǎng)頁或瀏覽器時,宿主環(huán)境(1)會將代碼傳遞給引擎(2)去執(zhí)行,引擎首先會創(chuàng)建一個全局執(zhí)行環(huán)境。全局環(huán)境中的代碼自上而下有順序的執(zhí)行,當遇到一個函數(shù)時,函數(shù)的環(huán)境被創(chuàng)建,函數(shù)中的代碼開始執(zhí)行;而在函數(shù)執(zhí)行之后,控制權(quán)又返還給之前的環(huán)境。ES 這種類似于" 棧 "(3)的控制機制,稱為執(zhí)行棧。
(1) 宿主環(huán)境:瀏覽器或者 Node 環(huán)境。
(2) 引擎:從頭到尾負責整個 JavaScript 代碼的編譯及執(zhí)行過程。
(3) 棧:一種遵循" 后進先出 "原則的有序數(shù)據(jù)集合,可以簡單理解為使用 push() 和 pop() 操作數(shù)組。
例子:
console.log(1);
function pFn() {
console.log(2);
(function cFn() {
console.log(3);
}());
console.log(4);
}
pFn();
console.log(5);
//輸出:1 2 3 4 5
示意圖:

我們可以通過瀏覽器,直觀的看一下執(zhí)行棧的形式:

編譯原理
我們知道,執(zhí)行環(huán)境中有很多非常有用的" 工具 “,這些” 工具 “會協(xié)助引擎完成整個函數(shù)的執(zhí)行工作。例如,ES3 標準中的作用域,它會協(xié)助引擎查找當前環(huán)境中所有標識符的定義的位置;變量對象,幫助引擎保存環(huán)境中的變量和函數(shù)。當然,這些工作大部分情況下發(fā)生在代碼執(zhí)行前的幾微秒之內(nèi),稱之為” 編譯階段 "。JavaScript 的整個編譯階段比較復(fù)雜,一般會經(jīng)歷詞法分析、語法分析、代碼生成、性能優(yōu)化等步驟,這里不做深入討論。
下面我們舉例說明,看看當函數(shù) fn 執(zhí)行的時候,引擎是如何工作的:
var b=1;
function fn(){
var a = 1;
return a+b;
}
fn();
1、首先,遇到 var a,引擎會詢問作用域是否已經(jīng)有一個該名稱的變量存在于同一個作用域中。如果存在,引擎會忽略該聲明,繼續(xù)進行編譯;很顯然不存在,所以引擎會在當前作用域中聲明一個新的變量,并命名為 a ( 此時還沒有賦值,默認為 undefined )。
2、第二步,又遇到 a,引擎會首先詢問作用域,在當前的作用域中是否存在一個叫作 a 的變量,很顯然存在,所以引擎就會使用這個變量;遇到 b,引擎對作用域做出同樣的詢問,很顯然不存在,所以引擎會到外層嵌套的作用域中繼續(xù)查找,在全局作用域找到了該變量,引擎就會將 1 賦值給變量 b 。
3、經(jīng)過以上兩步,函數(shù) fn 環(huán)境中出現(xiàn)的所有標識符的值已經(jīng)基本鎖定,那么引擎就會立即自上而下開始執(zhí)行代碼。為變量 a 賦值 1,計算 1+1 的值并返回它。
4、最后一步,函數(shù) fn 的環(huán)境銷毀,退出執(zhí)行棧,將控制權(quán)返還給全局環(huán)境。
變量提升的原因
在編譯階段,引擎會聲明變量和函數(shù),但不會對變量進行賦值,這主要是出于對性能的考慮。變量被聲明,但是不一定會在后面使用到,如果沒有使用卻賦了值,只是白白浪費內(nèi)存而已。上面例子中的全局變量 b ,在函數(shù) fn 沒有執(zhí)行之前,也不會賦值,直到函數(shù)中使用了這個變量,才不得不去加載數(shù)字 1。簡單的說,var a 這段代碼發(fā)生在編譯階段,而 =1 這段代碼會根據(jù)實際情況,發(fā)生在執(zhí)行階段,這也就是" 變量提升 "的原因。另外需要注意的是,函數(shù)聲明的是整個函數(shù)體( 因為函數(shù)聲明不存在賦值操作),而且優(yōu)先級高于同名的變量。
例子1:
console.log(fn()); //輸出:1
console.log(n); //輸出:undefined
function fn() {
return 1;
}
var n = 2;
由于聲明發(fā)生在賦值的前面,上面例子1的代碼可以理解為下面的形式:
function fn() {
return 1;
}
var n;
console.log(fn()); //輸出:1
console.log(n); //輸出:undefined
n = 2;
由于函數(shù)聲明優(yōu)先級高,因此同名變量聲明會被忽略,上面例子2的代碼可以理解為下面的形式:
function fn() {
console.log(1);
}
//由于函數(shù)聲明優(yōu)先級高,因此這個變量聲明會被忽略
//var fn;
fn(); //輸出:1
fn = function() {
console.log(2);
}
*變量提升并非物理意義上的順序改變,代碼執(zhí)行的順序還是按照你書寫代碼時的順序在執(zhí)行。只是由于,變量聲明發(fā)生在代碼的編譯階段,而變量賦值卻發(fā)生在代碼的執(zhí)行階段,時間上的差異導致了這種現(xiàn)象。
運行時流程圖
綜合以上的內(nèi)容,JavaScript 的運行時流程圖如下:

- 深入理解JavaScript 中的執(zhí)行上下文和執(zhí)行棧
- js bind 函數(shù) 使用閉包保存執(zhí)行上下文
- JS ES6中setTimeout函數(shù)的執(zhí)行上下文示例
- 跟我學習javascript的執(zhí)行上下文
- 淺析JavaScript作用域鏈、執(zhí)行上下文與閉包
- 深入理解JavaScript系列(11) 執(zhí)行上下文(Execution Contexts)
- 對于Javascript 執(zhí)行上下文的全面了解
- 深入探討JavaScript的最基本部分之執(zhí)行上下文
- 簡單了解JavaScript中的執(zhí)行上下文和堆棧
- 一篇文章弄懂javascript中的執(zhí)行棧與執(zhí)行上下文
相關(guān)文章
js動態(tài)調(diào)用css屬性的小規(guī)律及實例說明
本篇文章主要介紹了js動態(tài)調(diào)用css屬性的小規(guī)律及實例說明。需要的朋友可以過來參考下,希望對大家有所幫助2013-12-12

