[Frontend筆記]Javascript的執行上下文及作用域

了解JS中的執行上下文和作用域,能更了解其執行原理

Posted by 李定宇 on Thursday, November 3, 2022

Javascript的執行上下文及作用域

前言

在筆者剛接觸Javascript時,很容易寫出

test();

const test = ()=>{
	console.log('demo')
}

然後執行的時候被報出undefined的錯誤後,才知道要變換聲明和執行的順序;但用function來聲明函數就沒有這個困擾:

test();

function test(){
	console.log('demo')
}

還有就是有關varletconst變量聲明的關鍵字,都是在踩過一些坑之後,大概記住什麼時候要用什麼會比較好,可能像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對象中訪問到該函數:

image

因此,使用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後,引入letconst,而兩者支持塊級作用域。利用letconst聲明變量時,並不會被變量提升,而是會存放在該執行上下文中的詞法環境(lexical environment)的一塊。

詞法環境會維護一個 stack 結構,當有一個塊作用域用let/const聲明變量,如 for(let i=0;i<10;i++){...},Javascript引擎會把該變量push進詞法環境中的stack;當該for循環結束後,變量就會被pop出去,達到塊執行結束就銷毀變量的目的

ChangeLog

  • 20221103 - 初稿

Ref