初學者指南:深入了解 JavaScript 中函式與變數的建立期與執行期差異

更新日期: 2024 年 10 月 17 日

在學習 JavaScript 時,理解程式的執行機制對於寫出穩定、高效的代碼至關重要。

特別是「建立期」(Creation Phase)和「執行期」(Execution Phase),這兩個階段決定了函式和變數在程式中的行為。

本文將聚焦於函式與變數在建立期與執行期的差異,特別是初始化的時機,幫助新手了解 JavaScript 的運作原理,避免常見的錯誤。

建議閱讀本文前,先理解相關概念:初學者指南:深入了解 JavaScript 的建立期與執行期


執行上下文(Execution Context)

在深入討論建立期與執行期之前,首先需要了解 執行上下文 的概念。執行上下文是 JavaScript 程式執行時的環境,包含了程式在執行時所需的所有資訊。

執行上下文的類型

  • 全域執行上下文:當程式開始執行時,會創建一個全域執行上下文。
  • 函式執行上下文:每當函式被調用時,都會創建一個新的函式執行上下文。

執行上下文的生命週期

每個執行上下文在創建時,都會經歷兩個階段:

  1. 建立期(Creation Phase)
  2. 執行期(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

解釋:

  1. 建立期var a 被提升並初始化undefined
  2. 執行期,執行 console.log(a);,輸出 undefined
  3. 接著執行 a = 10;,將 a 賦值為 10
  4. 最後執行 console.log(a);,輸出 10

使用 letconst 聲明的變數

建立期

  1. 創建但不初始化:變數被提升,但不進行初始化,處於暫時性死區(Temporal Dead Zone, TDZ)。在此期間,變數存在於作用域內,但在初始化之前無法被訪問。

執行期

  1. 初始化與賦值
  2. let:當程式碼執行到變數宣告時,變數被初始化undefined,或者同時被賦予指定的值。
  3. const:在宣告時必須同時進行初始化,賦予具體的值。

範例:

console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 20;

解釋:

  1. 建立期let b 被創建但未初始化,處於 TDZ。
  2. 執行期,嘗試在宣告前訪問 b,導致 ReferenceError
  3. 當執行到 let b = 20; 時,b初始化20

四、函式在建立期與執行期的行為

函式宣告(Function Declaration)

建立期

  • 提升並初始化:整個函式(包括函式體)被提升,並在建立期就已經完成初始化。因此,函式在程式的任何地方都可調用,即使在宣告之前。

執行期

  • 函式調用與執行:當程式碼執行到函式調用時,函式內的代碼才會被執行。

範例:

foo(); // 輸出:Hello
function foo() {
  console.log("Hello");
}

解釋:

  1. 建立期function foo() 被提升,並已經完成初始化,整個函式可用。
  2. 執行期foo(); 調用函式,輸出 Hello

函式表達式(Function Expression)

建立期

  • 變數提升但未初始化
    • 如果使用 var 聲明,變數被提升並初始化undefined
    • 如果使用 letconst 聲明,變數被提升但不初始化,處於暫時性死區(TDZ)。

執行期

  • 初始化與賦值:當程式碼執行到函式表達式時,才將函式賦值給變數。

範例(使用 var):

bar(); // TypeError: bar is not a function
var bar = function () {
  console.log("Hello");
};

解釋:

  1. 建立期var bar 被提升並初始化undefined
  2. 執行期,執行 bar();,此時 bar 的值為 undefined,無法調用,導致 TypeError
  3. 當執行到 bar = function() { ... } 時,bar 才被賦值為函式。

範例(使用 let):

baz(); // ReferenceError: Cannot access 'baz' before initialization
let baz = function () {
  console.log("Hello");
};

解釋:

  1. 建立期let baz 被提升但未初始化,處於 TDZ。
  2. 執行期,嘗試在初始化之前調用 baz(),導致 ReferenceError

函式與變數提升的差異

提升的內容不同

  • 函式宣告:提升 整個函式,包括函式體,並在建立期就完成初始化
  • 變數宣告(var:只提升變數名稱,並在建立期初始化undefined

初始化的時機不同

  • 函式宣告:在建立期就已經完成初始化,函式可立即使用。
  • 變數宣告
    • var:在建立期初始化undefined,在執行期才賦予實際的值。
    • letconst:在建立期被提升但不初始化,在執行期才進行初始化。

使用時機的差異

  • 函式宣告:可在程式的任何地方調用,即使在宣告之前。
  • 函式表達式與變數:只能在變數初始化(賦值)之後才能使用。

範例:

// 函式宣告
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

解釋:

  • 建立期
    1. var name 被提升並初始化undefined
  • 執行期
    1. 執行 console.log(name);,輸出 undefined
    2. 執行 name = "Alice";,將 name 賦值為 "Alice"
    3. 執行 console.log(name);,輸出 "Alice"

函式與變數同名時的衝突

範例:

var foo = 1;
function foo() {}
console.log(typeof foo); // 輸出:number

解釋:

  • 建立期
    1. 函式宣告 function foo() 被提升,並初始化為函式。
    2. 變數宣告 var foo 被提升,但因為函式提升的優先級高於變數,foo 仍然為函式。
  • 執行期
    1. 執行 foo = 1;,將 foo 賦值為數字 1,覆蓋了之前的函式。
  • 結果typeof foo 輸出 "number"

總結

初始化時機的差異

  • 函式宣告:在建立期就已經完成初始化,整個函式都可用。
  • 變數宣告(var:在建立期被提升並初始化undefined,但實際的賦值在執行期才進行。
  • 變數宣告(letconst:在建立期被提升但不初始化,在執行期執行到宣告時才進行初始化。

可用性的差異

  • 函式宣告:可在宣告之前調用,因為已經在建立期完成初始化。
  • 函式表達式與變數:只能在變數初始化(賦值)之後才能使用。

避免常見錯誤

  • 避免在初始化前使用變數或函式表達式,以免遇到 ReferenceErrorTypeError
  • 注意同名的函式和變數可能導致的衝突,謹慎命名。

最佳實踐

儘量使用 letconst

  • 避免變數提升導致的問題letconst 在建立期未初始化,避免了在初始化前訪問變數的錯誤。
  • 區塊作用域letconst 提供了區塊作用域,限制變數的作用範圍。

避免在同一作用域內聲明同名的變數和函式

  • 防止衝突和意外覆蓋:確保變數和函式名稱的唯一性。

使用函式宣告與函式表達式時的注意事項

  • 需要在宣告前調用時,使用函式宣告
  • 需要動態賦值或條件性賦值時,使用函式表達式

結語

理解 JavaScript 中函式與變數在建立期與執行期的差異,特別是初始化的時機,對於撰寫健壯的程式碼至關重要。

透過深入了解變數提升、函式提升以及它們之間的不同,你可以:

  • 避免常見錯誤:如在變數初始化前訪問導致的 undefinedReferenceErrorTypeError
  • 提高代碼品質:撰寫更可讀、可維護的程式碼。
  • 深入理解 JavaScript 的執行機制:為進一步學習高階主題(如閉包、作用域鏈)打下基礎。

希望這篇文章能夠幫助你掌握這些重要的概念,並在實際開發中靈活運用。


參考資料

Similar Posts

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *