Javascript的執行上下文及作用域
前言
在筆者剛接觸Javascript時,很容易寫出
test();
const test = ()=>{
console.log('demo')
}
然後執行的時候被報出undefined
的錯誤後,才知道要變換聲明和執行的順序;但用function
來聲明函數就沒有這個困擾:
test();
function test(){
console.log('demo')
}
還有就是有關var
、let
、const
變量聲明的關鍵字,都是在踩過一些坑之後,大概記住什麼時候要用什麼會比較好,可能像const 用來聲明不可變的常量
、let 用來聲明可變的變量
、var就隨便聲明,但在for循環時候要小心
之類的碎片經驗。
但在學習瀏覽器、Node、V8的相關知識後,才了解這些碎片經驗背後的基本原理。
變量提升(Hoisting)
所謂變量提升(Hoisting),就是Javascript程式碼執行的過程中,Javascript引擎把變量聲明的部分和函數聲明的部分,放到來Javascript的開頭,默認值為undifined
舉個例子,像是
const data = 1;
實際上在編譯、執行時,順序會像是:
const data = undefined;
data = 1;
Javascript會把變量的聲明和賦值在不同的階段執行。在編譯的階段,Javascript會在該執行上下文中聲明變量,然後給一個undefined
;在執行程式碼的階段,才會把值給賦值過去。
而在用 function
關鍵字來聲明一個函數時,在Javascript編譯階段,會將該函數的內容存到Heap中,然後在該環境對象中新增屬性一個函數名的屬性,其屬性值為指向Heap存放函數內容的位址。
例如,在Chrome console中聲明一個函數 sayHello
後,就可以在window
對象中訪問到該函數:
因此,使用function
來聲明函數,其寫的順序可以先調用再聲明,因為在編譯時Javascript會直接在環境對象中新增該屬性;而用其他關鍵字來把函數內容給賦值過去,就必須先聲明再調用,不然會是undefined
作用域
在ES6之前,Javascript只有全局作用域
和函數作用域
,而聲明變量的時候只有var
關鍵字。在全局作用域
中所聲明的變量、對象,在任何地方都可以被訪問;而在函數作用域
為函數內部定義的變量或函數,只能在函數內部被訪問,而且函數執行完之後會被銷毀。
只有以上這些條件,有一個很大的缺點,因為變量提升(Hoisting)、全局作用域
中的變量在任何地方都可以被訪問,不知道變量什麼時候會被修改、或者被同名的變量給覆蓋,甚至該銷毀的變量沒有被銷毀。
如很常見的面試題:
function foo(){
for (var i = 0; i < 7; i++) {
}
console.log(i);
}
foo()
因為 var i
被變量提升了,所以在for循環結束後,函數內還是可以訪問到變量 i 。
因此,在ES6後,引入let
和const
,而兩者支持塊級作用域
。利用let
或const
聲明變量時,並不會被變量提升,而是會存放在該執行上下文中的詞法環境(lexical environment)
的一塊。
詞法環境會維護一個 stack 結構,當有一個塊作用域用let
/const
聲明變量,如 for(let i=0;i<10;i++){...}
,Javascript引擎會把該變量push進詞法環境中的stack;當該for循環結束後,變量就會被pop出去,達到塊執行結束就銷毀變量的目的
ChangeLog
- 20221103 - 初稿
Ref
- 極客時間:《瀏覽器原理與實踐》、《圖解Google V8》