作用域
作用域是指程序源代碼中定義變量的區(qū)域,JavaScript 采用詞法作用域(lexical scoping),也就是靜態(tài)作用域。
靜態(tài)作用域 & 動(dòng)態(tài)作用域
靜態(tài) :因?yàn)?JavaScript 采用的是詞法作用域,函數(shù)的作用域在函數(shù)定義的時(shí)候就決定了。
動(dòng)態(tài) :而與詞法作用域相對(duì)的是動(dòng)態(tài)作用域,函數(shù)的作用域是在函數(shù)調(diào)用時(shí)才決定的。
看個(gè)例子??:
var value = 1
function foo () {
console.log(value)
}
function bar () {
var value = 2
foo()
}
bar() // 輸出神馬呢?
// 假設(shè):JavaScript采用的是靜態(tài)作用域。
// 那么函數(shù)的作用域在函數(shù)定義時(shí)就決定了。
// 執(zhí)行到foo函數(shù),先從foo函數(shù)內(nèi)部查找局部變量value。
// 如果沒(méi)有就根據(jù)書(shū)寫(xiě)位置,查找上面一層代碼,也就是value = 1。
// 所以結(jié)果是1。
// 假設(shè):JavaScript采用的是動(dòng)態(tài)作用域。
// 那么函數(shù)的作用域在函數(shù)調(diào)用時(shí)決定。
// 執(zhí)行到foo函數(shù),先從foo函數(shù)內(nèi)部查找局部變量value。
// 如果沒(méi)有就從調(diào)用函數(shù)的作用域,也就是bar函數(shù)內(nèi)部查找value變量,也就是value = 2。
// 所以結(jié)果是2。
前面說(shuō)了:JavaScript采用的是靜態(tài)作用域,所以這個(gè)例子的結(jié)果是 1。
如果問(wèn)到 JavaScript 的執(zhí)行順序,那么直觀的印象就是順序執(zhí)行。然而 JavaScript 引擎并非一行一行的分析和執(zhí)行,而是一段一段的分析執(zhí)行。執(zhí)行一段代碼的時(shí)候,會(huì)進(jìn)行一個(gè)“準(zhǔn)備工作”,比如變量提升、函數(shù)提升。
那這個(gè)“一段”是怎么劃分的?怎樣“準(zhǔn)備工作”呢?
當(dāng)執(zhí)行到一個(gè)函數(shù)的時(shí)候,就會(huì)進(jìn)行“準(zhǔn)備工作”,用個(gè)更專(zhuān)業(yè)的說(shuō)法就是“執(zhí)行上下文(execution context)”。
執(zhí)行上下文棧
那可是函數(shù)有很多,怎么管理這么多的執(zhí)行上下文呢?
JavaScript 引擎創(chuàng)建了 執(zhí)行上下文棧(Execution context stack, ECS)來(lái)管理執(zhí)行上下文。
模擬執(zhí)行上下文棧的行為,我們定義執(zhí)行上下文棧是一個(gè)數(shù)組:
ECStack = []
當(dāng) JavaScript 開(kāi)始要解釋執(zhí)行代碼的時(shí)候,最先遇到的是全局代碼,所以初始化的時(shí)候首先會(huì)向執(zhí)行上下文棧壓入一個(gè)全局執(zhí)行上下文,用 globalContext 表示,并且只有當(dāng)整個(gè)應(yīng)用程序結(jié)束的時(shí)候,ECStask才會(huì)被清空,所以ECStask最底部永遠(yuǎn)有個(gè) globalContext:
ECStask = [
globalContext
]
??例如這段代碼:
function fn3 () {
console.log('fn3')
}
function fn2 () {
fn3()
}
function fn1 () {
fn2()
}
fn1()
當(dāng)執(zhí)行一個(gè)函數(shù)的時(shí)候,就會(huì)創(chuàng)建一個(gè)執(zhí)行上下文,并且壓入執(zhí)行上下文棧,當(dāng)函數(shù)執(zhí)行完畢的時(shí)候,就會(huì)將函數(shù)的執(zhí)行上下文從棧中彈出。所以:
// 偽代碼
// fn1()
ECStask.push(<fn1> functionContext)
// fn1調(diào)用了fn2,創(chuàng)建fn2的執(zhí)行上下文
ECStask.push(<fn2> functionContext)
// fn2調(diào)用了fn3,再創(chuàng)建fn3的執(zhí)行上下文
ECStask.push(<fn3> functionContext)
// fn3執(zhí)行完畢
ECStack.pop();
// fn2執(zhí)行完畢
ECStack.pop();
// fn1執(zhí)行完畢
ECStack.pop();
再看兩個(gè)相似的例子??:
var scope = 'blobal scope'
function checkscope () {
var scope = 'local scope'
function f () {
return scope
}
return f()
}
checkscope()
// 模擬執(zhí)行上下文代碼:
// ECStask.push(<checkscope> functionContext)
// ECStask.push(<f> functionContext)
// ECStack.pop()
// ECStack.pop()
var scope = 'blobal scope'
function checkscope () {
var scope = 'local scope'
function f () {
return scope
}
return f
}
checkscope()()
// 模擬執(zhí)行上下文代碼:
// ECStask.push(<checkscope> functionContext)
// ECStack.pop()
// ECStask.push(<f> functionContext)
// ECStack.pop()
對(duì)于每個(gè)執(zhí)行上下文都有三個(gè)重要屬性:
- 變量對(duì)象(Variable object,VO)
- 作用域鏈(Scope chain)
- this
變量對(duì)象
變量對(duì)象是與 執(zhí)行上下文 相關(guān)的數(shù)據(jù)作用域,存儲(chǔ)了在上下文中定義的變量和函數(shù)聲明。
因?yàn)椴煌瑘?zhí)行上下文的變量對(duì)象不同,所以來(lái)了解全局上下文的變量對(duì)象和函數(shù)上下文的變量對(duì)象。
全局上下文
全局上下文中的變量對(duì)象就是 全局對(duì)象
執(zhí)行全局代碼時(shí),創(chuàng)建全局執(zhí)行上下文,全局上下文被壓入執(zhí)行上下文棧,然后初始化:
// 壓入執(zhí)行上下文棧
ECStack = [
globalContext
]
// 全局上下文初始化
globalContext = {
VO: [global],
Scope: [globalContext.VO],
this: globalContext.VO
}
函數(shù)上下文
在函數(shù)上下文中,我們用活動(dòng)對(duì)象(activation object, AO)來(lái)表示變量對(duì)象。
活動(dòng)對(duì)象是在進(jìn)入函數(shù)上下文時(shí)刻被創(chuàng)建的,它通過(guò)函數(shù)的 arguments 屬性初始化。arguments 屬性值是 Arguments 對(duì)象。
執(zhí)行過(guò)程
執(zhí)行上下文的代碼會(huì)分成兩個(gè)階段進(jìn)行處理:
- 進(jìn)入執(zhí)行上下文
- 代碼執(zhí)行
進(jìn)入執(zhí)行上下文
當(dāng)進(jìn)入執(zhí)行上下文時(shí),這時(shí)候還沒(méi)有執(zhí)行代碼,變量對(duì)象會(huì)包括:
-
函數(shù)的所有形參(如果是函數(shù)上下文)
- 由名稱和對(duì)應(yīng)組成的一個(gè)變量對(duì)象的屬性被創(chuàng)建
- 沒(méi)有實(shí)參,屬性值設(shè)為 undefined
-
函數(shù)聲明
- 由名稱和對(duì)應(yīng)值(函數(shù)對(duì)象function-object)組成一個(gè)變量對(duì)象的屬性被創(chuàng)建
- 如果變量對(duì)象已經(jīng)存在相同名稱的屬性,則完全替換這個(gè)屬性
-
變量聲明
- 由名稱和對(duì)應(yīng)值(undefined)組成一個(gè)變量對(duì)象的屬性被創(chuàng)建
- 如果變量名稱跟已經(jīng)聲明的形式參數(shù)或函數(shù)相同,則變量聲明不會(huì)干擾已經(jīng)存在的這類(lèi)屬性
例子??來(lái)了:
function foo (a) {
var b = 2
function c () {}
var d = function () {}
b = 3
}
foo(1)
在進(jìn)入執(zhí)行上下文后,這時(shí)候的AO是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c () {},
d: undefind
}
在代碼執(zhí)行階段,會(huì)順序執(zhí)行代碼,根據(jù)代碼修改變量對(duì)象的值,所以執(zhí)行代碼后的AO是:
AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: 3,
c: reference to function c () {},
d: reference to FunctionExpression "d"
}
所以變量對(duì)象總結(jié)幾句是:
- 全局上下文的變量對(duì)象初始化是全局對(duì)象
- 函數(shù)上下文的變量對(duì)象初始化只包括 Arguments 對(duì)象
- 在進(jìn)入執(zhí)行上下文時(shí)會(huì)給變量對(duì)象添加形參、函數(shù)聲明、變量聲明等初始化屬性值
- 在代碼執(zhí)行階段,會(huì)再次修改變量對(duì)象的屬性值
作用域鏈
當(dāng)在查找變量的時(shí)候,會(huì)先從當(dāng)前上下文的變量對(duì)象中查找,如果沒(méi)有找到,就會(huì)從父級(jí)執(zhí)行上下文的變量對(duì)象中查找,一直找到全局上下文的變量對(duì)象,也就是全局對(duì)象。這樣由多個(gè)執(zhí)行上下文的變量對(duì)象構(gòu)成的鏈就叫做作用域鏈。
下面我們以一個(gè)函數(shù)的創(chuàng)建和激活兩個(gè)時(shí)期來(lái)講解作用域鏈?zhǔn)侨绾蝿?chuàng)建和變化的。
函數(shù)創(chuàng)建
上文講到 JavaScript 是靜態(tài)作用域,函數(shù)的作用域在函數(shù)定義的時(shí)候就決定了。
這是因?yàn)楹瘮?shù)有一個(gè)內(nèi)部屬性[[scope]],當(dāng)函數(shù)創(chuàng)建的時(shí)候,就會(huì)保存所有父變量對(duì)象到里面,可以理解為[[scope]]就是所有父變量對(duì)象的層級(jí)鏈(并不代表完整的作用域鏈)。
例子??:
function foo () {
function bar () {
...
}
}
// 函數(shù)創(chuàng)建時(shí),各自的[[scope]]為:
// foo.[[scope]] = [
// globalContext.VO
// ]
//
// bar.[[scope]] = [
// fooContext.AO,
// blobalContext.VO
// ]
函數(shù)激活
當(dāng)函數(shù)激活時(shí)進(jìn)入函數(shù)上下文,創(chuàng)建VO/AO后,就會(huì)將活動(dòng)對(duì)象添加到作用域鏈的前端。
這時(shí)候執(zhí)行上下文的作用域鏈,我們命名為 Scope:
Scope = [AO].concat([[Scope]])
作用域鏈創(chuàng)建完畢~
縷縷順
因?yàn)槿绻苯诱f(shuō)完函數(shù)的作用域就講作用域鏈的話,里面的執(zhí)行上下文就會(huì)懵。所以先說(shuō)的函數(shù)執(zhí)行里面的執(zhí)行上下文才說(shuō)的作用域鏈。有點(diǎn)亂沒(méi)關(guān)系,現(xiàn)在來(lái)縷縷順,當(dāng) js 解釋器開(kāi)始工作的時(shí)候:
- 創(chuàng)建全局執(zhí)行上下文,壓入執(zhí)行上下文棧,并初始化
- 函數(shù)被創(chuàng)建的時(shí)候,就有內(nèi)部屬性作用域鏈 [[scope]]
- 函數(shù)被調(diào)用時(shí):
- 創(chuàng)建函數(shù)執(zhí)行上下文,壓入執(zhí)行上下文棧中
- 函數(shù)執(zhí)行上下文棧初始化:
- 復(fù)制函數(shù) [[scope]] 屬性創(chuàng)建作用域鏈
- 用 arguments 創(chuàng)建活動(dòng)對(duì)象
- 初始化活動(dòng)對(duì)象,即加入形參、函數(shù)聲明、變量聲明
- 將活動(dòng)對(duì)象 (AO) 壓入作用域鏈頂端
- 函數(shù)代碼執(zhí)行
- 函數(shù)執(zhí)行結(jié)束的時(shí)候,位于棧頂?shù)膱?zhí)行上下文被彈出,繼續(xù)執(zhí)行新的位于棧頂?shù)膱?zhí)行上下文
這種方式保證了只有位于棧頂?shù)膱?zhí)行上下文才會(huì)被執(zhí)行,也就是實(shí)現(xiàn)了單線程。
一個(gè)大例子??
以下面為例,結(jié)合變量對(duì)象和執(zhí)行上下文棧,我們總結(jié)一下函數(shù)執(zhí)行上下文中作用域鏈和變量對(duì)象的創(chuàng)建過(guò)程:
var scope = 'blobal scope'
function checkscope () {
var scope = 'local scope'
function f () {
return scope
}
return f()
}
checkscope()
- 執(zhí)行全局代碼,創(chuàng)建全局執(zhí)行上下文,全局執(zhí)行上下文被壓入執(zhí)行上下文棧
ECStask = [
blobalContext
]
- 全局上下文初始化
blobalContext = {
VO: [vlobal],
Scope: [globalContext.VO],
this: globalContext.VO
}
- 初始化的同時(shí),checkscope 函數(shù)被創(chuàng)建,保存作用域鏈到內(nèi)部屬性[[scope]]
checkscope.[[scope]] = [
blobalContext.VO
]
- 執(zhí)行 checkscope 函數(shù),創(chuàng)建 checkscope 函數(shù)執(zhí)行上下文,壓入執(zhí)行上下文棧
ECStask = [
checkscopeContext,
globalContext
]
-
checkscope 函數(shù)執(zhí)行上下文初始化:
- 復(fù)制函數(shù) [[scope]] 屬性創(chuàng)建作用域鏈
- 用 arguments 創(chuàng)建活動(dòng)對(duì)象
- 初始化活動(dòng)對(duì)象 (AO),即加入形參、函數(shù)聲明、變量聲明
- 將活動(dòng)對(duì)象壓入 checkscope 作用域鏈頂端
- 同時(shí) f 函數(shù)被創(chuàng)建,保存作用域鏈到 f 函數(shù)的內(nèi)部屬性 [[scope]]
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope: undefined,
f: reference to function f () {}
},
Scope: [AO, blobalContext.VO],
this: undefined
}
fscope.[[scope]] = [
checkscopeContext.AO,
blobalContext.VO
]
- 執(zhí)行 f 函數(shù),創(chuàng)建 f 函數(shù)執(zhí)行上下文,f 函數(shù)執(zhí)行上下文被壓入執(zhí)行上下文棧
ECStask = [
fContext,
checkscopeContext,
globalContext
]
-
f 函數(shù)執(zhí)行上下文初始化,和第5步相似:
- 復(fù)制函數(shù)[[scope]]屬性創(chuàng)建作用域鏈
- 用 arguments 創(chuàng)建活動(dòng)對(duì)象
- 初始化活動(dòng)對(duì)象(AO),即加入形參、函數(shù)聲明、變量聲明
- 將活動(dòng)對(duì)象壓入 f 作用域鏈頂端
fContext = {
AO: {
arguments: {
length: 0
}
},
Scope: [AO, checkscopeContext.AO, blobalContext.VO],
this: undefined
}
- f 函數(shù)執(zhí)行,沿著作用域鏈查找 scope 值,返回 scope 值
- f 函數(shù)執(zhí)行完畢,f 函數(shù)上下文從執(zhí)行上下文棧中彈出
ECStack = [
checkscopeContext,
globalContext
]
- checkscope 函數(shù)執(zhí)行完畢,checkscope 執(zhí)行上下文從執(zhí)行上下文棧中彈出
ECStack = [
globalContext
]