初學者指南:深入了解 JavaScript 中函式與變數的建立期與執行期差異
更新日期: 2024 年 10 月 17 日
在學習 JavaScript 時,理解程式的執行機制對於寫出穩定、高效的代碼至關重要。
特別是「建立期」(Creation Phase)和「執行期」(Execution Phase),這兩個階段決定了函式和變數在程式中的行為。
本文將聚焦於函式與變數在建立期與執行期的差異,特別是初始化的時機,幫助新手了解 JavaScript 的運作原理,避免常見的錯誤。
建議閱讀本文前,先理解相關概念:初學者指南:深入了解 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 的執行機制:為進一步學習高階主題(如閉包、作用域鏈)打下基礎。
希望這篇文章能夠幫助你掌握這些重要的概念,並在實際開發中靈活運用。