本篇為 [JS201] 進階 JavaScript:那些你一直搞不懂的地方筆記之三。
先講結論:
ㄧ、什麼是 hoisting(提升)?
a. 即使宣告變數在執行順序的後面,但該變數依然會被作用,可以把它想像為這個宣告往上提升了。
b. 只有宣告會提升,賦值不會
二、一個一個來:hoisting 的順序
這裡提到 hoisting 的發生範圍與順序:
a. 範圍只在 scope 裡面
b. 順序為:fn > argument > variable
c. 兩個同樣名稱的 fn,下面會蓋掉上面的
三、hoisting 的原理為何?從 ECMAScript 下手
a. 先解題看看是否還記得 hoisting 規則
b. 從 ECMAScript 理解 js 執行時的底層運作:
b.1 會先產生 EC:執行環境
b.2 會有 variable object 存 EC 裡面的資料
b.3 進入 EC 時,會依照宣告的順序,綁定函式、參數、變數在 variable object
b.4 variable object 裡面沒有參數、變數,會被初始化成 undefined
b.5 如果新增一個函式,而該函式與 variable object 裡面的物件同名,則該函式會取代原有的值。
b.6 變數或參數如果在 variable object 已經有值了,就不會改變
四、體驗 JS 引擎的一天,理解 Execution Context 與 Variable Object
a. 每呼叫函式時,就會先初始化 vo,接著才執行裡面的程式碼。理解這個過程以後,就可以解決任何 hoisting 的問題了。
五、let 與 const 的詭異行為
這裡討論了 let and const 有沒有 hoisting,它們是有 hoisting 的,只是底層運作方式不同
六、TDZ:Temporal Dead Zone
在進入函式之後到賦值之前,這一段時間叫做 TDZ,如果這段時間存取變數的值的話,就會發生錯誤。
七、閱讀 我知道你懂 hoisting,可是你了解到多深? 的筆記
八、最後附上本篇提到 Standard ECMA-262, 3r d Edition - December 1999 的相關原文
詳細介紹
從 Hoisting 理解底層運作機制
ㄧ、什麼是 hoisting(提升)?
1. js 執行順序是一行接一行,理論上來說,應該會出現 b is not defined
,但是這裡卻出現 undefined
,這就是 hoisting
console.log(b)
var b = 10 // undefined
可以把上面的程式碼想成是下面那樣:
var b
console.log(b)
b = 10
2. 只有宣告會提升,賦值不會
Only the actual declarations are hoisted, not the assignment.
// 可以先呼叫函式,再宣告函式,是不是很方便呢,想把函式放哪就放哪
test()
function test() {
console.log(123)
}
// 賦值不會被提升喔
test() // test is not a function
var test = function () {
console.log(123)
}
二、一個一個來:hoisting 的順序
1. 提升的補充:提升只會發生在它的 scope 裡面
var a = 'global'
function test() {
console.log(a)
var a = 'local'
}
test() // undefined,不會往上找到 global 喔
上面的程式碼可以看成下面的解釋:
var a = 'global'
function test() {
var a // 這裡 a 被提升了
console.log(a)
a = 'local'
}
test() // undefined
2. hoisting 的優先順序:fn > argument > variable
a. 宣告函式 > 宣告變數,函式的優先度比較高
即使下面 var a = 'local'
比函式 a 更早宣告,但是函式的優先度還是大於變數。
function test() {
console.log(a)
var a = 'local'
function a() {
}
}
test() // [Function: a]
b. 同時宣告兩個同樣名稱的函式,下面那個會蓋掉上面的。
function test () {
console.log(a)
a()
function a() {
console.log(1)
}
function a() {
console.log(2)
}
var a = 'local'
}
test()
// [Function: a]
// 2
c. 參數也進來參一腳了
參數的優先權又大於變數,所以下面會輸出 123
function test(a) {
console.log(a)
var a = 456
}
test(123) // 123
d. 結論:hoisting 的優先順序:fn > argument > variable
function test(a) {
console.log(a)
var a = 456
function a () {
}
}
test(123) // [Function: a]
// 注意執行順序
function test(a) {
console.log(a)
var a = 456
console.log(a)
}
test(123)
// 123
// 456
三、hoisting 的原理為何?從 ECMAScript 下手
1. 首先我們來解這一題:
a. 先看執行 test 這個函式:var a = 7 hoisting,所以 1. undefined
b. 2. 7 沒有問題
c. a ++ => a = 8
d. var a 沒有作用
e. 執行 inner(): 因為 inner fn 裡面沒有宣告 a,所以往上一層找,會找到 a++ => 3. 8
f. b = 200: 會被宣告為全域變數
g. 執行 console.log('4.', a)
: inner fn 先印出 3. 8,接著把 30 給了 a,會是在 fn test() 這一層裡面,所以 4. 30
h. 5. 1 看 global 的變數
i. console.log('6.', a): 6. 70
j. console.log('7.', b): 7. 200
var a = 1;
function test(){
console.log('1.', a);
var a = 7;
console.log('2.', a);
a++;
var a;
inner();
console.log('4.', a);
function inner(){
console.log('3.', a);
a = 30;
b = 200;
}
}
test();
console.log('5.', a);
a = 70;
console.log('6.', a);
console.log('7.', b);
// 1. undefined
// 2. 7
// 3. 8
// 4. 30
// 5. 1
// 6. 70
// 7. 200
2. 從 ECMAScript 了解 hoisting 的原理:
a. 來看看 js 舊版說明書吧
首先從下面這個規格書來看,對應到的是 ES3,內容差不多,只是在 ES6 時,裡面有些名詞不太一樣。
Standard ECMA-262, 3r d Edition - December 1999
b. p37 Ch10
Execution Contexts 執行環境
When control is transferred to ECMAScript executable code, control is entering an execution context. Active execution contexts logically form a stack. The top execution context on this logical stack is the running execution context.
每執行一個函式時,就會產生一個 EC,裡面會放所有跟這個函式有關的東西,有在運作的 EC 會形成一個堆疊(中國稱「棧」),在這個堆疊最上面的 EC,就是正在執行的那一個 EC
一開始會有一個全域的執行環境 global EC,隨著執行一個函式時,就會多一個 EC。一但函式結束執行,就會把該 EC 跟 VO 都刪除以後,才跳到下一個 EC,最後執行到 global EC,然後全部都執行完後,最後也刪除 global EC,東西全部清空。
c. p37 Ch 10.1.3
Variable Instantiation 初始化變數
Every execution context has associated with it a variable object. Variables and functions declared in the source text are added as properties of the variable object. For function code, parameters are added as properties of the variable object.
每一個 EC 都有一個 variable object,在 EC 裡面宣告的函式或是變數,都會被加到這個 variable object 裡面。如果是函式的話,參數也會被加到 variable object 裡面。
可以想成這樣:
VO: {
a: 1
}
function test(){
var a = 1}
節錄重點:
On entering an execution context, the
properties are bound to the variable object in the following order:
當進入一個 EC 時,會按照你宣告的順序,把東西綁在 variable object 裡面。
If the variable object already has a property with this name, replace its value and attributes.
參數也會被放進去,沒有傳的參數,就會被初始化成 undefined。假設裡面已經有同樣名稱的參數了,參數就會被取代。
這裡在講變數:
If there is already a property of the variable object with the name of a declared variable, the value of the property and its attributes are not changed.
如果 variable object 裡面變數已經有值了,那就不會改變任何東西。
四、體驗 JS 引擎的一天,理解 Execution Context 與 Variable Object
進入函式以後,會先初始化 Variable Object
文字版執行流程:
- 先進入 global EC,產生 VO。先找參數,但因為 global 不是 fn 所以沒有參數。
- 找 fn 宣告:宣告 test 初始化為 fn
- 找變數宣告,初始化變數 a -> undefined
- 完成,接著要開始執行程式碼了
- 執行第一行,a 變成 1
- 呼叫 test(),進入新的 test EC 以及 初始化 VO
- 先找 test fn 裡面有沒有參數,沒有
- 再找 fn,把 inner 初始化為一個 fn
- 找變數宣告,初始化變數 a -> undefined
- 開始執行程式碼,第三行 a -> undefined
- 執行第四行,a 變成 7;所以第五行 a -> 7
- 執行第六行 a++,a -> 8
- 執行第七行,VO 已經有 a,所以不變
- 執行第八行,呼叫 inner,進入 inner EC & VO
- 沒有參數、函式、變數,inner VO 是空的
- 執行 11 行,往上找 test VO,a -> 8
- 12 行,inner VO 空的,往上找 a -> 30
- 13 行,都沒有 b,只好把 b 放 global VO
- inner 執行完畢,刪除 inner
- 執行第九行,a -> 30
- test 執行完畢,刪除 test
- 執行 17 行,a -> 1
- 執行 18 行,a -> 70,19 行 a -> 70
- 執行 20 行,b -> 200
- 執行完畢,刪除 global
來個超慢速可以邊看邊思考的動圖吧
五、let 與 const 的詭異行為
let、const 有 hoisting 嗎?
這裡不管用 let or const 都會出現一樣的錯誤訊息:ReferenceError: Cannot access 'a' before initialization
console.log(a)
let a = 10
// ReferenceError: Cannot access 'a' before initialization
小結:let and const 是有 hoisting 的,只是它們的底層運作方式不同
六、TDZ:Temporal Dead Zone
1. let 與 const 一樣會 hoisting,只是在賦值之前不能去存取它。
2. 在進入函式以後開始提升,提升以後到賦值之前,這一段時間叫做 TDZ,如果這段時間存取變數的值的話,就會發生錯誤。
七、閱讀 我知道你懂 hoisting,可是你了解到多深? 的筆記
預計收穫心得:
- 你知道什麼是 hoisting
- 你知道 hoisting 只會提升宣告而非賦值
- 你知道 function 宣告、function 的參數以及一般4. 變數宣告同時出現時的提升優先順序
- 你知道 let 跟 const 沒有 hoisting
- 你知道第四點是錯的,其實有但只是表現形式不一樣
- 你知道有關第五點,有個概念叫做 TDZ(Temporal Dead Zone)
- 你看過 ES3 的規格書,知道裡面是怎麼描述的
- 你看過 ES6 的規格書,知道裡面是怎麼描述的(以後有機會再看吧)
- 你知道 hoisting 背後的原理是什麼
- 你看過 V8 編譯出來的程式碼
1. 自己解釋一下文章裡面的範例:
a. 從結論回推:這裡的錯誤訊息是 "Cannot access 'a' before initialization"
假設1: let 有 hoisting -> 應該會出現 undefined
假設2: let 沒有 hoisting -> 那結果應該會是 10
var a = 10
function test(){
console.log(a)
let a
}
test()
// Cannot access 'a' before initialization
但居然出現的是 "Cannot access 'a' before initialization",表示 let 有 hoisting,只是表現方式跟 var 不一樣。
2. 規格書內容:
a. 第一個處理參數
在 VO 裡面,第一個傳進去的是參數;如果參數沒有值,就會變 undefined。
"For function code: for each formal parameter, as defined in the FormalParameterList, create a property of the variable object whose name is the Identifier and whose attributes are determined by the type of code. The values of the parameters are supplied by the caller as arguments to [[Call]].
If the caller supplies fewer parameter values than there are formal parameters, the extra formal parameters have value undefined."
在函式裡,每一個參數(像是 FormalParameterList 裡所定義的那樣),都會在 VO 裡面新增一個 property。在這個 property 裡面呢,會有 Identifier 以及 attributes。函式裡面的參數,是從呼叫函式時傳的參數而來。
假設呼叫函式時,傳的參數少於函式裡面所定義的參數,函式多餘的參數就會被設成 undefined。
老師舉了一個例子:
函式是這樣:
function test(a, b, c) {}
test(10)
VO 長這樣:
{
a: 10,
b: undefined,
c: undefined
}
b. 第二個處理函式
VO 第二個傳進去的是函式,假設已有同名的 property,則 Attributes 跟 value 會被取代
For each FunctionDeclaration in the code, in source text order, create a property of the variable object whose name is the Identifier in the FunctionDeclaration, whose value is the result returned by creating a Function object as described in 13, and whose attributes are determined by the type of code.
If the variable object already has a property with this name, replace its value and attributes. Semantically, this step must follow the creation of FormalParameterList properties.
對於每個函式的宣告,會在 VO 裡新增一個 property。這個 property 裡一樣會有 Identifier 跟 attributes。Identifier 就是函式名稱,attributes 就是函式。value 則會是函式執行完以後回傳的東西。
如果 VO 已經有同名的 property 了,value 跟 attributes 會被取代。
舉個例子:
function a () {
var a = 10
console.log(a)
}
a() // 10
個人理解,有錯請指正,感謝
VO 長這樣:
a: {
Identifier: a,
Attributes: Function,
Value: 10
}
// 上面整個 a 是一個 property
// 裡面有三個資料
// value 就是程式執行完以後的結果
c. 第三個處理變數
在 VO 裡,第三個傳進去的是變數,值是 undefined,若有同名 property,則不會改變任何事。
For each VariableDeclaration or VariableDeclarationNoIn in the code, create a property of the variable object whose name is the Identifier in the VariableDeclaration or VariableDeclarationNoIn, whose value is undefined and whose attributes are determined by the type of code. If there is already a property of the variable object with the name of a declared variable, the value of the property and its attributes are not changed.
Semantically, this step must follow the creation of the FormalParameterList and FunctionDeclaration properties. In particular, if a declared variable has the same name as a declared function or formal parameter, the variable declaration does not disturb the existing property.
每一個變數宣告會在 VO 裡新增一個 property,裡面一樣有 Identifier、attributes and value。這裡的值會被初始化為 undefined。如果裡面已經有一個同名的 property 了,則 attributes and value 不會被改變。
變數宣告不會影響已經存在的函式宣告或參數宣告。
3. let、const hoisting 時的 TDZ
老師這句話總結得很好,直接引用:
let 與 const 也有 hoisting 但沒有初始化為 undefined,而且在賦值之前試圖取值會發生錯誤。
p37-38 Ch10 全文:
10 Execution Contexts
When control is transferred to ECMAScript executable code, control is entering an execution context. Active execution contexts logically form a stack. The top execution context on this logical stack is the running
execution context.
10.1 Definitions
10.1.1 Function Objects
There are two types of Function objects:
• Program functions are defined in source text by a FunctionDeclaration or created dynamically either by using a FunctionExpression or by using the built-in Function object as a constructor.
• Internal functions are built-in objects of the language, such as parseInt and Math.exp. An implementation may also provide implementation-dependent internal functions that are not described in this specification. These functions do not contain executable code defined by the ECMAScript grammar, so they are excluded from this discussion of execution contexts.
10.1.2 Types of Executable Code
There are three types of ECMAScript executable code:
• Global code is source text that is treated as an ECMAScript Program. The global code of a particular Program does not include any source text that is parsed as part of a FunctionBody.
• Eval code is the source text supplied to the built-in eval function. More precisely, if the parameter to the built-in eval function is a string, it is treated as an ECMAScript Program. The eval code for a particular invocation of eval is the global code portion of the string parameter.
• Function code is source text that is parsed as part of a FunctionBody. The function code of a particular FunctionBody does not include any source text that is parsed as part of a nested FunctionBody. Function code also denotes the source text supplied when using the built-in Function object as a constructor. More precisely, the last parameter provided to the Function
constructor is converted to a string and treated as the FunctionBody. If more than one parameter is provided to the Function constructor, all parameters except the last one are converted to strings and concatenated together, separated by commas. The resulting string is interpreted as the
FormalParameterList for the FunctionBody defined by the last parameter. The function code for a particular instantiation of a Function does not include any source text that is parsed as part of a nested FunctionBody.
10.1.3 Variable Instantiation
Every execution context has associated with it a variable object. Variables and functions declared in the source text are added as properties of the variable object. For function code, parameters are added as
properties of the variable object.
Which object is used as the variable object and what attributes are used for the properties depends on the type of code, but the remainder of the behaviour is generic. On entering an execution context, the
properties are bound to the variable object in the following order:
• For function code: for each formal parameter, as defined in the FormalParameterList, create a property of the variable object whose name is the Identifier and whose attributes are determined by the type of code. The values of the parameters are supplied by the caller as arguments to [[Call]]. If the caller supplies fewer parameter values than there are formal parameters, the extra formal parameters have value undefined. If two or more formal parameters share the same name, hence the same property, the corresponding property is given the value that was supplied for the last parameter with this name. If the value of this last parameter was not supplied by the caller, the value of the corresponding property is undefined.
• For each FunctionDeclaration in the code, in source text order, create a property of the variable object whose name is the Identifier in the FunctionDeclaration, whose value is the result returned by creating a Function object as described in 13, and whose attributes are determined by the type of code. If the variable object already has a property with this name, replace its value and attributes. Semantically, this step must follow the creation of FormalParameterList properties.
• For each VariableDeclaration or VariableDeclarationNoIn in the code, create a property of the variable object whose name is the Identifier in the VariableDeclaration or VariableDeclarationNoIn, whose value is undefined and whose attributes are determined by the type of code. If there is
already a property of the variable object with the name of a declared variable, the value of the property and its attributes are not changed. Semantically, this step must follow the creation of the FormalParameterList and FunctionDeclaration properties. In particular, if a declared variable has the same name as a declared function or formal parameter, the variable declaration does not disturb the existing property.