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