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