本文為 JS 底層運作邏輯系列文,第五篇
- 初學者指南:深入了解 JavaScript 中的 Event Loop(事件循環)
- 初學者指南:深入了解 JavaScript 的 Call Stack(呼叫堆疊)
- 初學者指南:深入了解 JavaScript 的執行環境(Execution Context)
- 初學者指南:深入了解 JavaScript 的建立期與執行期
- 初學者指南:深入了解 JavaScript 中函式與變數的建立期與執行期差異 👈 所在位置
在學習 JavaScript 時,理解程式的執行機制對於寫出穩定、高效的代碼至關重要。
特別是「建立期」(Creation Phase)和「執行期」(Execution Phase),這兩個階段決定了函式和變數在程式中的行為。
本文將聚焦於函式與變數在建立期與執行期的差異,特別是初始化的時機,幫助新手了解 JavaScript 的運作原理,避免常見的錯誤。
執行上下文(Execution Context)
在深入討論建立期與執行期之前,首先需要了解 執行上下文 的概念。執行上下文是 JavaScript 程式執行時的環境,包含了程式在執行時所需的所有資訊。
執行上下文的類型
- 全域執行上下文:當程式開始執行時,會創建一個全域執行上下文。
- 函式執行上下文:每當函式被調用時,都會創建一個新的函式執行上下文。
執行上下文的生命週期
每個執行上下文在創建時,都會經歷兩個階段:
- 建立期(Creation Phase)
- 執行期(Execution Phase)
建立期與執行期的詳細解釋
建立期(Creation Phase)
在建立期,JavaScript 引擎會為執行上下文做以下事情:
- 建立作用域鏈(Scope Chain):確定當前上下文的作用域,建立對外部作用域的引用。
- 創建變數物件(Variable Object,VO):處理變數和函式的宣告。
- 變數提升(Hoisting):
- 變數宣告:使用
var聲明的變數會被提升,並在建立期初始化為undefined。 - 函式宣告:使用
function聲明的函式會被提升,並在建立期就已經完成初始化,整個函式(包括函式體)都可用。
- 變數宣告:使用
- 確定
this的指向:設定當前上下文中this的值。
執行期(Execution Phase)
在執行期,JavaScript 引擎開始按順序執行程式碼,為變數賦值,調用函式等。
變數在建立期與執行期的行為
使用 var 聲明的變數
建立期:
- 提升與初始化:變數被提升到作用域的頂部,並在建立期初始化為
undefined。這意味著變數在建立期就已經存在,且擁有初始值undefined。
執行期:
- 賦值:當程式碼執行到賦值語句時,變數被賦予實際的值。
範例:
console.log(a); // 輸出:undefined
var a = 10;
console.log(a); // 輸出:10解釋:
- 在建立期,
var a被提升並初始化為undefined。 - 在執行期,執行
console.log(a);,輸出undefined。 - 接著執行
a = 10;,將a賦值為10。 - 最後執行
console.log(a);,輸出10。
使用 let 和 const 聲明的變數
建立期:
- 創建但不初始化:變數被提升,但不進行初始化,處於暫時性死區(Temporal Dead Zone, TDZ)。在此期間,變數存在於作用域內,但在初始化之前無法被訪問。
執行期:
- 初始化與賦值:
let:當程式碼執行到變數宣告時,變數被初始化為undefined,或者同時被賦予指定的值。const:在宣告時必須同時進行初始化,賦予具體的值。
範例:
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 20;解釋:
- 在建立期,
let b被創建但未初始化,處於 TDZ。 - 在執行期,嘗試在宣告前訪問
b,導致ReferenceError。 - 當執行到
let b = 20;時,b被初始化為20。
四、函式在建立期與執行期的行為
函式宣告(Function Declaration)
建立期:
- 提升並初始化:整個函式(包括函式體)被提升,並在建立期就已經完成初始化。因此,函式在程式的任何地方都可調用,即使在宣告之前。
執行期:
- 函式調用與執行:當程式碼執行到函式調用時,函式內的代碼才會被執行。
範例:
foo(); // 輸出:Hello
function foo() {
console.log("Hello");
}解釋:
- 在建立期,
function foo()被提升,並已經完成初始化,整個函式可用。 - 在執行期,
foo();調用函式,輸出Hello。
函式表達式(Function Expression)
建立期:
- 變數提升但未初始化:
- 如果使用
var聲明,變數被提升並初始化為undefined。 - 如果使用
let或const聲明,變數被提升但不初始化,處於暫時性死區(TDZ)。
- 如果使用
執行期:
- 初始化與賦值:當程式碼執行到函式表達式時,才將函式賦值給變數。
範例(使用 var):
bar(); // TypeError: bar is not a function
var bar = function () {
console.log("Hello");
};解釋:
- 在建立期,
var bar被提升並初始化為undefined。 - 在執行期,執行
bar();,此時bar的值為undefined,無法調用,導致TypeError。 - 當執行到
bar = function() { ... }時,bar才被賦值為函式。
範例(使用 let):
baz(); // ReferenceError: Cannot access 'baz' before initialization
let baz = function () {
console.log("Hello");
};解釋:
- 在建立期,
let baz被提升但未初始化,處於 TDZ。 - 在執行期,嘗試在初始化之前調用
baz(),導致ReferenceError。
函式與變數提升的差異
提升的內容不同
- 函式宣告:提升 整個函式,包括函式體,並在建立期就完成初始化。
- 變數宣告(
var):只提升變數名稱,並在建立期初始化為undefined。
初始化的時機不同
- 函式宣告:在建立期就已經完成初始化,函式可立即使用。
- 變數宣告:
var:在建立期初始化為undefined,在執行期才賦予實際的值。let、const:在建立期被提升但不初始化,在執行期才進行初始化。
使用時機的差異
- 函式宣告:可在程式的任何地方調用,即使在宣告之前。
- 函式表達式與變數:只能在變數初始化(賦值)之後才能使用。
範例:
// 函式宣告
hoistedFunction(); // 輸出:This function has been hoisted
function hoistedFunction() {
console.log("This function has been hoisted");
}
// 函式表達式(使用 var)
nonHoistedFunction(); // TypeError: nonHoistedFunction is not a function
var nonHoistedFunction = function () {
console.log("This function has not been hoisted");
};
// 函式表達式(使用 let)
anotherFunction(); // ReferenceError: Cannot access 'anotherFunction' before initialization
let anotherFunction = function () {
console.log("This function is in TDZ");
};實際應用與常見問題
變數提升導致的 undefined
範例:
console.log(name); // 輸出:undefined
var name = "Alice";
console.log(name); // 輸出:Alice解釋:
- 建立期:
var name被提升並初始化為undefined。
- 執行期:
- 執行
console.log(name);,輸出undefined。 - 執行
name = "Alice";,將name賦值為"Alice"。 - 執行
console.log(name);,輸出"Alice"。
- 執行
函式與變數同名時的衝突
範例:
var foo = 1;
function foo() {}
console.log(typeof foo); // 輸出:number解釋:
- 建立期:
- 函式宣告
function foo()被提升,並初始化為函式。 - 變數宣告
var foo被提升,但因為函式提升的優先級高於變數,foo仍然為函式。
- 函式宣告
- 執行期:
- 執行
foo = 1;,將foo賦值為數字1,覆蓋了之前的函式。
- 執行
- 結果:
typeof foo輸出"number"。
總結
初始化時機的差異
- 函式宣告:在建立期就已經完成初始化,整個函式都可用。
- 變數宣告(
var):在建立期被提升並初始化為undefined,但實際的賦值在執行期才進行。 - 變數宣告(
let、const):在建立期被提升但不初始化,在執行期執行到宣告時才進行初始化。
可用性的差異
- 函式宣告:可在宣告之前調用,因為已經在建立期完成初始化。
- 函式表達式與變數:只能在變數初始化(賦值)之後才能使用。
避免常見錯誤
- 避免在初始化前使用變數或函式表達式,以免遇到
ReferenceError或TypeError。 - 注意同名的函式和變數可能導致的衝突,謹慎命名。
最佳實踐
儘量使用 let 和 const
- 避免變數提升導致的問題:
let和const在建立期未初始化,避免了在初始化前訪問變數的錯誤。 - 區塊作用域:
let和const提供了區塊作用域,限制變數的作用範圍。
避免在同一作用域內聲明同名的變數和函式
- 防止衝突和意外覆蓋:確保變數和函式名稱的唯一性。
使用函式宣告與函式表達式時的注意事項
- 需要在宣告前調用時,使用函式宣告。
- 需要動態賦值或條件性賦值時,使用函式表達式。
結語
理解 JavaScript 中函式與變數在建立期與執行期的差異,特別是初始化的時機,對於撰寫健壯的程式碼至關重要。
透過深入了解變數提升、函式提升以及它們之間的不同,你可以:
- 避免常見錯誤:如在變數初始化前訪問導致的
undefined、ReferenceError或TypeError。 - 提高代碼品質:撰寫更可讀、可維護的程式碼。
- 深入理解 JavaScript 的執行機制:為進一步學習高階主題(如閉包、作用域鏈)打下基礎。
希望這篇文章能夠幫助你掌握這些重要的概念,並在實際開發中靈活運用。