???????JavaScript中執(zhí)行上下文阁危,變量對(duì)象,作用域與作用域鏈等是一系列基礎(chǔ)性的且十分重要的概念汰瘫。他們之間有著千絲萬縷的聯(lián)系狂打,深入理解這些概念以及他們之間的聯(lián)系有助于我們理解在js開發(fā)中遇到一些問題,例如:
1. es6之前混弥,使用var聲明的變量為什么會(huì)存在變量提升趴乡,過程是什么樣的,es6為什么新增let和const?
2. 塊級(jí)作用域解決了什么問題蝗拿?
3. this指向是什么時(shí)候確定晾捏,怎么確定的?
一.執(zhí)行上下文
???????執(zhí)行上下文哀托,通俗的講就是當(dāng)前js代碼的執(zhí)行環(huán)境惦辛。那如何確定當(dāng)前代碼的執(zhí)行環(huán)境?這里則要提到另一個(gè)概念仓手,執(zhí)行環(huán)境棧胖齐。JavaScript引擎在執(zhí)行代碼之前, 首先會(huì)創(chuàng)建一個(gè)執(zhí)行環(huán)境棧. 然后創(chuàng)建全局執(zhí)行環(huán)境并將它壓入棧中作為棧底。之后每遇到一個(gè)函數(shù)執(zhí)行時(shí)嗽冒,都會(huì)為該函數(shù)創(chuàng)建執(zhí)行上下文呀伙。并將其推入執(zhí)行環(huán)境棧中。JavaScript中的運(yùn)行環(huán)境主要是全局環(huán)境和局部環(huán)境(函數(shù))添坊。棧遵循先入后出的原則剿另,因此處于棧頂?shù)膱?zhí)行環(huán)境將優(yōu)先執(zhí)行。棧底永遠(yuǎn)是全局環(huán)境,當(dāng)瀏覽器窗口關(guān)閉驰弄,全局環(huán)境才會(huì)出棧麻汰。當(dāng)調(diào)用一個(gè)函數(shù)時(shí),一個(gè)新的執(zhí)行上下文就會(huì)被創(chuàng)建戚篙。一個(gè)執(zhí)行上下文的生命周期可以分為如下幾個(gè)階段:
1 創(chuàng)建階段
???????在這個(gè)階段中五鲫,執(zhí)行上下文會(huì)分別創(chuàng)建變量對(duì)象,確定作用域鏈岔擂,確定this指向(即this指向是在函數(shù)被調(diào)用時(shí)才被確定)位喂。
2 代碼執(zhí)行階段
???????完成變量賦值,以及執(zhí)行其他代碼乱灵。
3 銷毀階段
???????函數(shù)執(zhí)行完畢后, 執(zhí)行環(huán)境棧便會(huì)把這個(gè)函數(shù)的執(zhí)行環(huán)境彈出, 并將控制權(quán)返回給之前的執(zhí)行環(huán)境塑崖。上面提到 執(zhí)行上下文有3個(gè)重要屬性:
- 1.變量對(duì)象(VO)
- 2.作用域鏈(Scope chain)
- 3.this
下面做逐一分析。
二.變量對(duì)象痛倚。
???????每個(gè)執(zhí)行環(huán)境都有一個(gè)與之相關(guān)聯(lián)的對(duì)象规婆,執(zhí)行環(huán)境中聲明的變量和函數(shù)都在其中,不能直接訪問該對(duì)象蝉稳,這個(gè)對(duì)象就是變量對(duì)象(VO)抒蚜。由此我們便知道VO中存儲(chǔ)了在執(zhí)行環(huán)境中定義的變量和函數(shù)聲明.通常情況下,一個(gè)VO對(duì)象有以下信息:
- 變量
- 函數(shù)
- 形參
變量對(duì)象的創(chuàng)建會(huì)經(jīng)歷以下幾個(gè)過程。
一耘戚、建立arguments對(duì)象:檢查當(dāng)前上下文中的參數(shù)嗡髓,建立該對(duì)象下的屬性與屬性值。
二收津、檢查當(dāng)前上下文的函數(shù)聲明饿这,在變量對(duì)象中以函數(shù)名建立一個(gè)屬性,屬性值為指向該函數(shù)所在內(nèi)存地址的指針撞秋。
三长捧、檢查當(dāng)前上下文中的變量聲明,每找到一個(gè)變量聲明部服,就在變量對(duì)象中以變量名建立一個(gè)屬性唆姐,屬性值為undefined(變量提升),const/let 聲明的變量沒有賦值廓八,不能提前使用奉芦,在當(dāng)前階段(執(zhí)行上下文創(chuàng)建)若 var 聲明的變量與函數(shù)同名,以函數(shù)值為準(zhǔn)剧蹂。
???????需要注意的是声功,未進(jìn)入執(zhí)行階段之前,變量對(duì)象中的屬性都不能訪問宠叼。但是進(jìn)入執(zhí)行階段之后先巴,變量對(duì)象轉(zhuǎn)變?yōu)榱?strong>活動(dòng)對(duì)象(AO)其爵,里面的屬性都能被訪問了,然后開始進(jìn)行執(zhí)行階段的操作伸蚯。所以活動(dòng)對(duì)象實(shí)際就是變量對(duì)象在真正執(zhí)行時(shí)的另一種形式摩渺。
下面用一段偽代碼來描述一下變量對(duì)象的存在形式。
function foo(){
function bar (){
console.log(name)
}
bar()
var name = '小明'
}
foo()
/* foo調(diào)用時(shí)剂邮,其執(zhí)行上下文被創(chuàng)建
foo.EC = {
VO:{ //變量對(duì)象
this:window, //獨(dú)立調(diào)用摇幻,非嚴(yán)格模式下指向window(瀏覽器環(huán)境)
arguments:[],函數(shù)參數(shù)
bar: function
name: undefined //var聲明的變量 將被提升并賦值為undefined
},
[[scope chain]]:{} // 作用域鏈 下一章節(jié)做詳細(xì)說明
}
*/
???????需要注意一點(diǎn),let和const聲明的變量挥萌,在被變量對(duì)象收集后不會(huì)被賦值為undefined绰姻,因此不能在其聲明前調(diào)用。const引瀑、let會(huì)在聲明的地方到塊級(jí)頂部形成暫時(shí)性死區(qū)狂芋,在這區(qū)間使用該變量都會(huì)報(bào)錯(cuò)。
???????另外憨栽,全局執(zhí)行上下文比較特殊帜矾,它的變量對(duì)象,就是window對(duì)象(瀏覽器環(huán)境)徒像。this也是指向window黍特。
三.作用域與作用域鏈。
1.作用域
???????通俗的講即當(dāng)前執(zhí)行上下文中的變量和函數(shù)的可訪問(可查找)范圍锯蛀。它可以理解為一種約束或一套規(guī)則,定義在當(dāng)前作用域中的變量和函數(shù)在外部是不可訪問的次慢,例如我們在函數(shù)中聲明的變量一般情況下在函數(shù)外是無法訪問的(特殊情況后面會(huì)介紹)旁涤。
???????在es6之前,作用域有兩種迫像,即全局作用域和局部作用域(函數(shù)作用域)劈愚。es6加入了塊級(jí)作用域,什么是塊闻妓?任何一對(duì)花括號(hào)中的語句集都屬于一個(gè)塊菌羽,在這其中定義的所有變量在塊外都是不可見的,我們稱之為塊級(jí)作用域由缆。由于es6之前沒有塊級(jí)作用域注祖,因此塊中聲明的變量在塊外仍然是可以訪問的,這就會(huì)造成一些問題例如:
- 1.變量提升導(dǎo)致局部作用域的變量覆蓋全局變量均唉。
var name = '小明'
function foo(){
console.log(name) // undefined var定義的變量是晨,沒有塊的概念,因此if代碼塊中的name可以被外部訪問舔箭。被提升并被賦值為undefined
if (true)
{
var name = '小紅'
}
}
foo() //
而有了塊級(jí)作用域罩缴,則不會(huì)有這個(gè)問題。可以使用let或const聲明塊級(jí)變量來解決上述問題箫章。
var name = '小明'
function foo(){
console.log(name) // 小明
if (true)
{
const name = '小紅'
}
}
foo() //
- 2.循環(huán)中的計(jì)數(shù)變量泄露成全局變量
for (var i = 0; i < 5; i++) {
console.log(i)
}
console.log(i) // 5 當(dāng)i=5時(shí)循環(huán)結(jié)束烙荷,此時(shí)外部可以訪問到i
for (let i = 0; i < 5; i++) {
console.log(i)
}
console.log(i) // i is not defined 塊級(jí)變量外部無法訪問
由此可見有了塊級(jí)作用域,之前用立即執(zhí)行函數(shù)生成私有作用域的做法已經(jīng)不再需要了檬寂。
2.作用域鏈
???????一般情況下當(dāng)前執(zhí)行上下文的變量取值會(huì)到當(dāng)前上下文的變量對(duì)象中取值奢讨。 但是如果在當(dāng)前變量對(duì)象中沒有查到值,就會(huì)向上層環(huán)境中變量對(duì)象的去查,直到查到全局作用域,這個(gè)查找過程形成的鏈條就叫做作用域鏈。由此可見作用域鏈?zhǔn)怯僧?dāng)前環(huán)境與上層環(huán)境的一系列變量對(duì)象組成的焰薄。它和執(zhí)行上下文有關(guān),用于在處理標(biāo)識(shí)符(變量名或函數(shù)名)的時(shí)候進(jìn)行變量查詢拿诸。作用域鏈在函數(shù)調(diào)用的時(shí)候創(chuàng)建出來, 它包含了活動(dòng)對(duì)象(變量對(duì)象在執(zhí)行階段變?yōu)榛顒?dòng)對(duì)象)和該函數(shù)的內(nèi)部[[Scope]]屬性。函數(shù)的Scope 屬性是函數(shù)定義的時(shí)候創(chuàng)建的塞茅,這個(gè)屬性對(duì)應(yīng)的是一個(gè)對(duì)象列表亩码。該列表中存儲(chǔ)著與之相關(guān)聯(lián)作用域的變量對(duì)象,該對(duì)象僅能js內(nèi)部訪問野瘦。因此描沟,如果用一個(gè)數(shù)組scopeList來模擬作用域鏈,則scopeList[0]即代表當(dāng)前上下文的活動(dòng)對(duì)象鞭光,其余元素[scope]屬性中的所有對(duì)象的順序排列吏廉。該數(shù)組的最末端是全局變量對(duì)象。
下面用一段偽代碼描述作用域鏈的存在形式惰许。
var a = 1
function foo() {
var b = 2
console.log(a)
function bar() {
var c = 3
console.log(b)
}
bar()
}
foo()
/*
js默認(rèn)進(jìn)入全局執(zhí)行環(huán)境
foo的定義階段
foo.[[scope]] = { 該屬性保存了與該函數(shù)相關(guān)的變量對(duì)象席覆,顯然
foo的上層環(huán)境是window,window變量對(duì)象沒有arguments屬性
GO:{
this:window,
window:{...},
a:undefined
}
}
foo進(jìn)入執(zhí)行階段 bar函數(shù)才會(huì)被定義
此時(shí)由于foo函數(shù)已經(jīng)執(zhí)行汹买,因此其變量對(duì)象已經(jīng)被創(chuàng)建
bar.[[scope]] = {
AO(foo):{
this:window, 函數(shù)被調(diào)用時(shí)確定this指向佩伤,顯然foo為獨(dú)立調(diào)用,非嚴(yán)格模式下this指向window
arguments:[],
b:2
},
GO:{
this:window,
window:{...},
document:{...},
a:1
}
}
*/
// 執(zhí)行階段
// foo函數(shù)調(diào)用時(shí)
/*
foo.EC = { //foo函數(shù)的執(zhí)行上下文
AO:{
this:window,
arguments:[],
b:2,
bar:function
},
[[scope chain]]:{ foo的作用域鏈
AO:AO, // 推入作用域鏈頂部的活動(dòng)對(duì)象
GO:{....} // [[scope]]中的全局對(duì)象
}
}
bar.EC = {
AO:{
this:window,
arguments:[],
c:3
},
[[scope chain]]:{
AO(bar):AO,
AO(foo):{
this:window,
arguments:[],
b:2
},
GO:{
this:window,
window:{...},
document:{...},
a:1
}
}
}
*/
???????簡單總結(jié)一下晦毙,函數(shù)定義的時(shí)生巡,會(huì)創(chuàng)建一個(gè)[[scope]]屬性 。這個(gè)屬性對(duì)應(yīng)的是與當(dāng)前函數(shù)相關(guān)執(zhí)行環(huán)境的變量對(duì)象列表见妒。 函數(shù)調(diào)用時(shí)孤荣,當(dāng)前函數(shù)的執(zhí)行上下文被創(chuàng)建,創(chuàng)建變量對(duì)象须揣,確定作用域鏈 盐股,確定this指向。 變量對(duì)象主要保存當(dāng)前上下文的參數(shù)返敬,變量以及函數(shù)遂庄。作用域鏈頂部是當(dāng)前上下文的活動(dòng)對(duì)象,其余元素是scope屬性的對(duì)象列表劲赠,最底層是全局變量對(duì)象涛目。而this指向則主要看是誰調(diào)用的該函數(shù)(或者說函數(shù)調(diào)用時(shí)被哪個(gè)對(duì)象所擁有)秸谢。
參考:
https://www.bilibili.com/video/BV1hE411A7D3
http://www.reibang.com/p/330b1505e41d