一文帶你掌握JavaScript中的執(zhí)行上下文和作用域
執(zhí)行上下文
我們先來看段代碼
var foo = function () { console.log("foo1") } foo() // foo1 var foo = function () { console.log("foo2") } foo() // foo2
那這段代碼呢?
function foo() { console.log("foo1") } foo() // foo2 function foo() { console.log("foo2") } foo()// foo2
是不是有點懵逼了呢?第一段代碼比較好理解,但是第二段代碼為什么會打印兩個"foo2"呢?
這是因為JavaScript引擎并非一行一行分析和執(zhí)行程序的。當執(zhí)行一段代碼的時候,會有一些準備工作。那JavaScript引擎到底準備了哪些工作?
下面我們來一點點分析
console.log(a) // undefined var a = 10
這段代碼我們在定義a之前打印了a,但是并沒有報錯,說明在執(zhí)行console.log(a)
的時候,a就已經(jīng)被聲明了,也就是我們常說的變量提升,這就是準備工作。
var a console.log(a) a = 10
首先會把a的定義提前聲明,而不是賦值。
下面我們看下對于函數(shù)聲明和函數(shù)表達式,JavaScript引擎是如何做準備的。
console.log(add2(1, 2)) // 3 function add2(a, b) { return a + b } console.log(add1(1, 2)) // 報錯:add1 is not a function var add1 = function (a, b) { return a + b }
我們發(fā)現(xiàn),用函數(shù)語句創(chuàng)建的add2,函數(shù)名稱和函數(shù)體都被提前,在聲明它之前使用它。而函數(shù)表達式只是變量聲明提前了,變量賦值仍然在之前的位置?,F(xiàn)在回到剛開始那段代碼是不是就理解了呢?
所以JavaScript引擎都做好了哪些準備工作呢?
- 變量、函數(shù)表達式——變量提前聲明,默認為undefined
- 函數(shù)聲明——提前聲明并賦值
其實還有一個this也是提前就準備好了,并且也賦值了。
當執(zhí)行一個函數(shù)的時候,就會進行準備工作,這里的“準備工作”,就是“執(zhí)行上下文”
執(zhí)行上下文棧
執(zhí)行上下文棧管理執(zhí)行上下文。JavaScript代碼有兩種執(zhí)行上下文:全局執(zhí)行上下文和函數(shù)執(zhí)行上下文,還有一個是eval(我們先不考慮)。全局執(zhí)行上下文只有一個,函數(shù)執(zhí)行上下文是在每次函數(shù)執(zhí)行調(diào)用的時候,就會創(chuàng)建一個新的。
每個執(zhí)行上下文都有三個屬性:
- 變量對象(
Variable object
, VO) - 作用域鏈(
Scope chain
) - this
變量對象
變量對象是與執(zhí)行上下文相關(guān)的數(shù)據(jù)作用域,存儲了在上下文中定義的變量和函數(shù)聲明。
不同執(zhí)行上下文的變量對象不同,下面來看看全局上下文的變量對象和函數(shù)上下文的變量對象
全局上下文
- 全局對象是預(yù)定義的對象,作為JavaScript的全局函數(shù)和全局屬性的占位符。通過使用全局對象,可以訪問所有其他所有預(yù)定義對象、函數(shù)和屬性
- 在頂層的JavaScript代碼中,可以用關(guān)鍵字this引用全局對象。因為全局對象是作用域鏈的頭,意味著所有非限定性的變量和函數(shù)名都會作為該對象的屬性來查詢
- 例如,當JavaScript代碼引用parseInt()函數(shù)時,它引用的是全局對象的parseInt屬性。
函數(shù)上下文
在函數(shù)上下文中,我們用活動對象(activation object
, AO)來表示變量對象。
活動對象和變量對象其實是一個東西,只是變量對象是規(guī)范上的或者說是引擎實現(xiàn)上的,不可在JavaScript環(huán)境中訪問,只有到當進入一個執(zhí)行上下文中,這個執(zhí)行上下文的變量對象才會被激活,所以才叫activation object,而只有被激活的變量對象,也就是活動對象上的各種屬性才能被訪問。
活動對象是在進入函數(shù)上下文時候才被創(chuàng)建,它通過函數(shù)的arguments屬性初始化。arguments屬性值是Arguments對象。
執(zhí)行過程
執(zhí)行上下文的代碼會分成兩個階段進行處理:
- 進入執(zhí)行上下文
- 代碼執(zhí)行
進入執(zhí)行上下文
當調(diào)用函數(shù)后,進入執(zhí)行上下文,在執(zhí)行代碼之前,變量對象會包含:
函數(shù)的所有形參
- 由名稱和對應(yīng)的值組成一個變量對象的屬性被創(chuàng)建
- 沒有實參,屬性值設(shè)為undefined
函數(shù)聲明
- 由名稱和對應(yīng)值(函數(shù)對象)組成一個變量對象的屬性被創(chuàng)建
- 如果變量對象已經(jīng)存在相同名稱的屬性,則完全替換這個屬性
變量聲明
- 由名稱和對應(yīng)值(undefined)組成一個變量對象的屬性被創(chuàng)建
- 如果變量名稱跟已經(jīng)聲明的形式參數(shù)或函數(shù)相同,則變量聲明不會干擾已經(jīng)存在的這類屬性 比如:
function foo(a) { var b = 2 function c() {} var d = function () {} b = 3 } foo(1)
進入執(zhí)行上下文后,AO的值:
AO={ arguments: { 0:1, length:1 }, a: 1, b:undefined, c: reference to function c(){}, d:undefined }
代碼執(zhí)行
在代碼執(zhí)行階段,會按照順序執(zhí)行代碼,根據(jù)代碼,修改變量對象的屬性的值
AO={ arguments: { 0:1, length:1 }, a: 1, b: 3, c: reference to function c(){}, d: reference to FunctionExpression "d" }
小小總結(jié)一下變量對象:
- 全局上下文的變量對象初始化是全局對象
- 函數(shù)上下文的變量對象初始化包括Arguments對象
- 進入執(zhí)行上下文時會給變量對象添加形參,函數(shù)聲明,變量聲明等初始的屬性值
- 在代碼執(zhí)行階段,會再次修改變量對象的屬性值。
下面我們看下執(zhí)行上下文棧是如何工作的
function fun3() { console.log("fun3") } function fun2() { fun3() } function fun1() { fun2() } fun1()
我們用數(shù)組模擬執(zhí)行上下文棧,最先遇到的是全局代碼,初始化的時候,會向執(zhí)行上下文棧中壓入全局執(zhí)行上下文globalContext
Stack=[ globalContext ]
當執(zhí)行一個函數(shù)時候,就會創(chuàng)建一個執(zhí)行上下文,并且壓入執(zhí)行上下文棧中,當函數(shù)執(zhí)行完畢后,就會將函數(shù)的執(zhí)行上下文從棧中彈出。上下文所在其所有的代碼執(zhí)行完畢后會被銷毀。
// 執(zhí)行fun1 Stack.push(<fun1>functionContext); // fun1中調(diào)用了fun2 Stack.push(<fun2>functionContext); //fun2中調(diào)用了fun3 Stack.push(<fun3>functionContext); //fun3執(zhí)行完畢 彈出 Stack.pop() //fun2執(zhí)行完畢 彈出 Stack.pop() //fun1執(zhí)行完畢 彈出 Stack.pop()
最后Stack底層永遠有個全局執(zhí)行上下文globalContext。
作用域
作用域是指程序源代碼中定義變量的區(qū)域。作用域規(guī)定了如何查找變量,也就是確定當前執(zhí)行代碼對變量的訪問權(quán)限。JavaScript采用詞法作用域,也就是靜態(tài)作用域。
靜態(tài)作用域和動態(tài)作用域
JavaScript采用的是詞法作用域,函數(shù)的作用域是在函數(shù)定義的時候決定的。詞法作用域相對的是動態(tài)作用域,函數(shù)的作用域是在函數(shù)調(diào)用的時候才決定的。
作用域鏈
查找變量的時候,會先從當前上下文的變量對象中查找,如果沒有找到就會從父級執(zhí)行上下文的變量對象中查找,一直找到全局上下文的變量對象,也就是全局對象。這樣由多個執(zhí)行上下文的變量對象構(gòu)成的鏈表就叫做作用域鏈。
函數(shù)創(chuàng)建
上面提到,函數(shù)的作用域在函數(shù)定義的時候就已經(jīng)決定了。這是因為函數(shù)有一個內(nèi)部屬性[[scope]],當函數(shù)創(chuàng)建的時候,就會保存所有父變量對象到其中,可以理解[[scope]]就是所有父變量對象的層級鏈,但是[[scope]]并不代表完整的作用域鏈。我們來看個代碼:
function foo(){ function bar(){ } }
函數(shù)創(chuàng)建時,各自的[[scope]]為
foo.[[scope]] = [ globalContext.VO ] bar.[[scope]] = [ fooContext.AO, globalContext.VO ]
當函數(shù)激活,進入函數(shù)體,創(chuàng)建VO/AO后,就會將活動對象添加到作用鏈的前端。
總結(jié)
執(zhí)行上下文和作用域的區(qū)別:
1.全局作用域除外,每個函數(shù)都會創(chuàng)建自己的作用域,作用域在函數(shù)定義時就已經(jīng)確定了,而不是在函數(shù)調(diào)用時。
全局執(zhí)行上下文環(huán)境是在全局作用域確定之后,js代碼馬上執(zhí)行之前創(chuàng)建的。
函數(shù)執(zhí)行上下文是在調(diào)用函數(shù)時,執(zhí)行函數(shù)體代碼之前創(chuàng)建的。
2.作用域是靜態(tài)的,只要函數(shù)定義好了就一直存在,且不會再變化。
執(zhí)行上下文環(huán)境是動態(tài)的,調(diào)用函數(shù)時創(chuàng)建,函數(shù)調(diào)用結(jié)束上下文環(huán)境就會被釋放。
以上就是一文帶你掌握JavaScript中的執(zhí)行上下文和作用域的詳細內(nèi)容,更多關(guān)于JavaScript執(zhí)行上下文 作用域的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript使用setTimeout實現(xiàn)倒計時效果
這篇文章主要為大家詳細介紹了JavaScript使用setTimeout實現(xiàn)倒計時效果,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-02-02淺談javascript六種數(shù)據(jù)類型以及特殊注意點
這篇文章主要介紹了javascript六種數(shù)據(jù)類型以及特殊注意點,有需要的朋友可以參考一下2013-12-12