一、前言
var a = 'heihei', b = 'xixi'
function foo () {
console.log(a)
}
function bar () {
var a = 'houhou'
foo()
console.log(b)
}
bar()
// heihei
// xixi
如果您很快就能得出上述結果,那相信您的功底非常之紮實,如果無法確定,那麼這篇文章通讀之後,相信可以幫您解疑。
二、執行上下文
JS中可執行代碼有三種:全局代碼
,函數代碼
,eval代碼
。代碼執行前需要準備的執行環境
也稱爲執行上下文
,所以也分爲全局執行上下文
,函數執行上下文
,eval執行上下文
。
2.1 全局執行上下文
全局執行上下文中的變量對象就是全局對象,預置了很多屬性和函數。在瀏覽器中是window
,在NodeJS中是global
2.2 函數執行上下文
2.2.1 執行上下文初始化
- 複製函數
[[scope]]
屬性創建作用域鏈(下面會講到) - 用
arguments
創建活動對象 - 初始化活動對象,即加入形參、函數聲明、變量聲明
- 將活動對象壓入作用域鏈頂端
2.2.2 變量聲明,聲明提升
變量對象
被激活爲激活對象
,此時發生"hoist"
聲明提升
-
函數的所有形參
- 鍵爲
arguments
,其值也是一個對象(類數組對象,有length
屬性),按形參順序賦值,值爲實參 - 若無實參,屬性值設爲
undefined
- 鍵爲
-
函數聲明
- 在變量對象中添加以函數名命名的屬性,它的值是一個指向堆區(堆內存)函數
Function
對象的引用。 - 如果這個函數名字在變量對象屬性中已存在,這個引用指針就會被重寫,指向堆區中當前的函數對象。
- 在變量對象中添加以函數名命名的屬性,它的值是一個指向堆區(堆內存)函數
-
變量聲明
- 由名稱和對應值(
undefined
)組成一個變量對象的屬性被創建; - 如果變量名稱跟已經聲明的形參或函數相同,則變量聲明不會干擾已經存在的這類屬性(即形參和函數聲明的優先級高於變量聲明提升)
- 由名稱和對應值(
注意
: 整個過程可以大概描述成:函數的形參=>函數聲明=>變量聲明
, 其中在創建函數聲明時,如果名字存在,則會被重寫,在創建變量時,如果變量名存在,則忽略不會進行任何操作。
2.2.3 當代碼執行時
根據代碼修改激活對象對象中對應的值。如果當前執行上下文中的變量對象沒有該屬性,就去父級的執行上下文變量對象中尋找,直至到全局執行上下文。找不到就報錯
2.3 eval執行上下文
eval執行上下文比較特殊,它取決於eval函數是直接調用還是間接調用。
引用MDN上的說法:
如果間接的使用
eval()
,比如通過一個引用來調用它,而不是直接的調用eval
。 從 ECMAScript 5 起,它工作在全局作用域下,而不是局部作用域中。
- 當直接調用時,
eval
執行上下文爲執行時所處的執行上下文,具有和這個執行上文相同的作用域 - 當間接調用時,
eval
執行上下文爲全局執行上下文
function foo () {
var x = 2, y = 4
console.log(eval('x + y')) // 直接調用,執行上下文爲當前函數執行上下文,結果是 6
var geval = eval // 等價於在全局作用域調用
console.log(geval('x + y')) // 間接調用,執行上下文爲全局執行上下文,x is not defined,實際上y也是not defined
console.log(window.eval('x + y')) // 這也是間接調用
}
foo()
三、執行上下文棧
JS通過執行上下文棧
來管理上述這些執行上下文
。
- JavaScript 開始要解釋執行代碼的時候,最先遇到的就是全局代碼,所以首先就會創建一個全局執行上下文,並壓入執行上下文棧,我們用
globalContext
表示它,並且只有當整個應用程序結束的時候,ECStack
纔會被清空,所以程序結束之前,ECStack
最底部永遠有個globalContext
- 執行函數時,就會生成一個函數執行上下文並推入執行上下文棧,當函數執行完成就會把這個函數執行上下文彈出,並將控制權移交至執行棧中下一個執行環境,直至全局執行上下文
globalContext
- 當程序結束或者瀏覽器關閉,全局執行上下文也會從執行棧中彈出並銷燬
function foo (a) {
console.log(a)
}
function bar (b) {
foo(b)
}
bar('hehe')
四、變量對象(variable object)與激活對象(activation object)
變量對象
(variable object, VO):每個執行上下文都一個與之對應的變量對象,它是與執行上下文相關的數據作用域,存儲了在上下文中的函數標識符、形參、變量聲明等。但在規範上或者引擎實現上,這個對象是不能在JS環境中訪問的激活對象
(activation object, AO):當進入某個函數執行上下文中
時,其對應變量對象會被激活,變量對象上的屬性才能被訪問,所以稱之爲激活對象。
激活對象
就是在函數執行上下文中被激活成可訪問的變量對象
。
五、詞法環境(lexical environment)
根據詞法環境規範定義:
- A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code. A Lexical Environment consists of an Environment Record and a possibly null reference to an outer Lexical Environment.
- 詞法環境是一種規範類型,基於 ECMAScript 代碼的詞法嵌套結構來定義標識符與特定變量和函數的關聯關係。詞法環境由
環境記錄
(environment record)和可能爲空引用(null)的外部詞法環境
組成。
環境記錄
:主要是聲明性環境記錄
(declarative Environment Records)和對象環境記錄
(object Environment Records),其次還有全局環境記錄
(global Environment Records)和函數環境記錄
(function Environment Records)。
-
聲明性環境記錄
(declarative Environment Records):存儲變量、函數和參數, 用於函數聲明、變量聲明和catch語句。 -
對象環境記錄
(object Environment Records):用於像with這樣綁定對象標識符(作用域)的語句。 -
全局環境記錄
和函數環境記錄
:是特殊的聲明性環境記錄,形式上可理解爲對應的變量對象。
外部詞法環境
:構成作用域鏈的關鍵
六、作用域與作用域鏈
6.1 作用域
- 詞法作用域(靜態作用域):函數的作用域在函數定義的時候就決定了。JS使用的是詞法作用域。
- 動態作用域:函數的作用域是在函數調用的時候才決定的。
作用域(Scope)用於規定如何查找變量,也就是確定當前執行上下文中對變量的訪問權限。
6.2 作用域鏈
在函數中有一個內部屬性,當函數創建的時候,就會保存所有父變量對象到其中,在查找變量值的時候,會先從[[scope]]
頂部即當前上下文的變量對象(作用域)中查找,如果沒有找到,就會根據當前執行上下文中的[[scope]]
對外部執行環境的引用順序,從父級(詞法層面上的父級)執行上下文的變量對象中查找,一直找到全局上下文的變量對象,也就是全局對象。這樣由多個執行上下文的變量對象構成的鏈表就叫做作用域鏈
。
注意
:
- 當進入函數執行上下文(函數激活)時,會將該函數的變量對象推入到作用域鏈前端。
- 正式由於作用域與作用域鏈的這種關係,在當前函數執行上下文的活動對象中必定存在this和arguments,所以this和arguments的搜索在當前執行執行上下文就停止了。
總結
回過頭來看前言中的問題,按照下面的流程進行(只列出關鍵部分):
- 創建全局執行上下文,推入執行上下文棧中
ECStack = [
globalContext
]
- 初始化全局執行上下文
globalContext = {
VO: [global], // 指向全局對象
Scope: [globalContext.VO], // 可訪問權限
this: globalContext.VO
}
- 同時
foo
函數和bar
函數被創建,生成內部作用域鏈。
foo.[[scope]] = [
globalContext.VO
]
bar.[[scope]] = [
globalContext.VO
]
- 經過代碼執行,全局執行環境的變量對象已經賦值。根據
2.2節
中所述,執行bar
函數前,創建bar
函數執行上下文,並推入執行上下文棧中。
ECStack = [
barContext,
globalContext
]
- 初始化
bar
函數執行上下文
barContext = {
AO: {
arguments: {
length: 0
},
a: undefined,
foo: <reference to function foo() {}>
},
Scope: [barContext.AO, globalContext.VO],
this: undefined
}
- 中斷
bar
函數執行,開始執行foo
函數,同理,創建foo
函數執行上下文,並推入執行上下文棧中
ECStack = [
fooContext,
barContext,
globalContext
]
- 初始化
foo
函數執行上下文
fooContext = {
AO: {
arguments: {
length: 0
}
},
Scope: [fooContext.AO, globalContext.VO],
this: undefined
}
foo
函數執行,foo
函數執行上下文中的激活對象沒有屬性a
,所以沿着作用域鏈[[scope]]
找到全局執行上下文中的變量對象,其指向全局對象,故輸出'heihei'
。執行完畢彈出foo
函數執行上下文並銷燬。
ECStack = [
barContext,
globalContext
]
- 繼續
bar
函數執行,bar
函數執行上下文中的激活對象沒有屬性b
,所以沿着作用域鏈[[scope]]
找到全局執行上下文中的變量對象,其指向全局對象,故輸出'xixi'
。執行完畢彈出bar
函數執行上下文並銷燬。
ECStack = [
globalContext
]