JavaScript 函式的兩種寫法:函式陳述句 vs 函式表示式

更新日期: 2025 年 5 月 3 日

在 JavaScript 中,函式(function)是語言的基石之一。不管你是處理事件、建立 API 邏輯,還是封裝程式邏輯模組,幾乎都會用到函式。

然而,你知道嗎?

JavaScript 提供了兩種定義函式的主要方式:

  • 函式陳述句(Function Declaration)
  • 函式表示式(Function Expression)

雖然看起來只是一種寫法上的差異,但它們其實會影響到程式執行的順序、作用域管理、以及錯誤排查的行為

本篇文章將帶你:

  • 分辨兩者的語法與差異
  • 了解它們在執行階段的行為不同
  • 學會如何選擇使用時機

什麼是函式陳述句(Function Declaration)?

語法形式

在 JavaScript 中,函式陳述句(Function Declaration)是一種最基本也最常見的函式定義方式。

語法形式如下:

function greet() {
  console.log("Hello!");
}

這裡的 function 是關鍵字,greet 是函式名稱,而 {} 裡面則是這個函式執行時的程式內容(也稱為「函式主體」)。

這段程式碼的意思是:定義一個名為 greet 的函式,當它被呼叫時會印出 “Hello!” 到主控台。

你可以這樣使用它:

greet(); // 輸出:Hello!

函式陳述句的特性與行為

特性說明
✅ 有名稱函式名稱 greet 是顯示的、有辨識度的,有助於錯誤追蹤與開發工具的除錯堆疊分析(stack trace)
✅ 提升(Hoisting)函式在宣告之前也能被呼叫,因為瀏覽器會在執行前先將函式載入記憶體(這稱為「提升」)
✅ 可重複定義如果你用同樣名稱定義多個函式,後定義的會覆蓋先定義的,類似變數被重新指定值

為什麼「提升」這麼重要?

JavaScript 在執行程式前會先做一件事情:建立執行環境(Execution Context)並做 Hoisting(提升)處理

在這個階段,所有的函式陳述句都會被提前載入到記憶體中,這表示你可以在定義之前就呼叫它們:

sayHi(); // ✅ 可以呼叫,輸出 "Hi!"

function sayHi() {
  console.log("Hi!");
}

對比一下函式表示式(稍後會介紹),這種提升特性是函式陳述句特有的優勢,在撰寫工具函式或模組初始化程式時非常有幫助。

⚠️ 注意:重複定義會被覆蓋

函式陳述句可以用同樣名稱宣告多次,但最後一次宣告會覆蓋前面的版本

function greet() {
  console.log("Hi!");
}

function greet() {
  console.log("Hello!");
}

greet(); // 輸出:Hello!

這在大型專案中可能會造成意外的錯誤,因此不建議在同一作用域內重複使用同樣的函式名稱。

🧠 初學者常見迷思

迷思事實說明
我以為函式一定要先定義才能用?只要是函式陳述句,JavaScript 會在執行前就把它加入記憶體,所以你可以先呼叫後定義。
我定義了兩次同名函式,為什麼舊的被蓋掉了?因為後定義的函式會覆蓋掉前面的定義,類似變數重新賦值。

什麼是函式表示式(Function Expression)?

在 JavaScript 中,除了使用「函式陳述句」來定義函式之外,還可以使用「函式表示式」的方式,將函式當成值賦給變數

這是一種更靈活、更動態的函式定義方式,也是在現代 JavaScript 開發中非常常見的寫法。

語法形式

const greet = function () {
  console.log("Hello!");
};

這段程式碼的意思是:

  • 使用 const 宣告一個變數 greet
  • 並把一個匿名函式(anonymous function)賦值給這個變數

此時,greet 就變成一個可以執行的函式:

greet(); // 輸出:Hello!

你也可以給這個函式一個名稱,稱為具名函式表示式(Named Function Expression)

const greet = function greetInternal() {
  console.log("Hello!");
};

但注意:greetInternal 這個名稱只能在函式本身內部使用,在函式外是無法呼叫它的。

為什麼 greetInternal 在函式外無法使用?

當你使用具名函式表示式(Named Function Expression)時,雖然你在函式內給它了一個名稱,但這個名稱的作用域(scope)只存在於函式的內部

來看一個範例:

const greet = function greetInternal() {
  console.log("Hello!");
};

這裡發生了兩件事:

  1. 宣告了一個變數 greet
  2. 賦值給它一個名為 greetInternal 的函式

但重點在於:

greetInternal 只在函式本身的範圍內可見,在函式外部你只能用 greet 呼叫這個函式。

在函式內可以使用 greetInternal(遞迴或自我參考)

const greet = function greetInternal(times = 1) {
  if (times > 1) {
    console.log("Hello!");
    greetInternal(times - 1); // ✅ OK:函式內部可以自己呼叫自己
  } else {
    console.log("Bye!");
  }
};

greet(3);
// 輸出:
// Hello!
// Hello!
// Bye!

這個模式常用於遞迴(recursive)邏輯,讓函式即使透過匿名變數傳遞,也能在內部知道自己的名稱。

在函式外使用 greetInternal 會報錯

console.log(greetInternal); // ❌ ReferenceError: greetInternal is not defined

這是因為 greetInternal 僅被限制在函式自己的作用範圍內,不會被提升或洩漏到外部作用域。

小技巧:你還是可以用變數呼叫它

greet(); // ✅ 正確使用方式,因為 greet 是變數名稱

如果你需要讓函式在作用域外也能有個名字,應該使用函式陳述句(Function Declaration)

function greetInternal() {
  console.log("Hello!");
}

greetInternal(); // ✅ OK:函式名稱在整個作用域內可見

不會提升(Hoisting 行為)

不同於函式陳述句,函式表示式不會被提升。雖然變數本身會被提升(因為是 varlet / const 宣告),但值尚未指派前無法使用

sayHi(); // ❌ TypeError: sayHi is not a function

const sayHi = function () {
  console.log("Hi!");
};

這段程式碼在執行 sayHi() 時,sayHi 是存在的變數,但它的值(也就是那個函式)還沒被指派進去,因此會報錯。

可作為值傳遞、回傳、儲存

函式表示式最大的特色就是它是一個值,也就是說它可以像一般變數一樣被操作:

當作參數傳遞

function run(callback) {
  callback(); // 執行傳入的函式
}

run(function () {
  console.log("I'm passed in as a value!");
});

作為函式的回傳值(高階函式)

function createGreeter() {
  return function () {
    console.log("Hi from returned function!");
  };
}

const greeter = createGreeter();
greeter(); // 輸出:Hi from returned function!

儲存在物件或陣列中

const tools = {
  greet: function () {
    console.log("Hello from object!");
  }
};

tools.greet(); // 輸出:Hello from object!

搭配現代語法:箭頭函式、閉包等

由於函式表示式是一種「可被當作值」的函式定義方式,這讓它具備極高的靈活性。

也因此,JavaScript 許多現代語法設計,都是建立在函式表示式的概念之上。以下介紹兩個最常見也最實用的應用情境:

箭頭函式(Arrow Function):簡潔又直覺的現代函式語法

箭頭函式是 ES6 引入的新語法,最適合與函式表示式一起使用。

它的基本語法如下:

const greet = () => {
  console.log("Hello from arrow function!");
};

這裡的 greet 就是一個函式變數,這整段寫法是函式表示式的簡化版本。

範例:搭配陣列操作

const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6]

注意:

箭頭函式不適合用在需要 thisarguments 的情境中,例如物件方法或建構函式。

閉包(Closure):保留內部狀態的技巧

閉包是 JavaScript 中非常核心的概念,而閉包最常見的寫法,就是透過函式表示式建立的。

先來看個經典範例:

function makeCounter() {
  let count = 0;

  return function () {
    count++;
    console.log(count);
  };
}

const counter = makeCounter();
counter(); // 1
counter(); // 2

發生了什麼事?

  • 外層的 makeCounter() 函式在執行時建立了一個區域變數 count
  • 回傳的是一個函式表示式
  • 這個被回傳的函式「記住」了它所處的外層環境(也就是 count 的記憶體位置)
  • 每次執行 counter(),它都還能操作原本那個 count

這就是閉包的威力:函式保留了外部環境的變數,即使外部函式早已結束執行。

為什麼用函式表示式?

因為閉包通常是作為一個函式的回傳值或參數傳遞,這些都只能透過函式表示式(可以作為值)來實現。

Hoisting(提升)行為差異:為什麼有些函式可以先呼叫、有些不行?

在 JavaScript 中,當程式碼被執行前,引擎會先建立一個「執行階段上下文(Execution Context)」。這個過程中,會自動進行一個重要的動作——Hoisting(提升)

所謂 Hoisting,意思是:

在執行前,變數與函式宣告會被「提前移動」到作用域的最上方,即使它們在實際程式碼中出現在較後面。

但不同類型的宣告,提升行為也不同!

函式陳述句(Function Declaration):整個函式定義會被提升

sayHi(); // ✅ 可以呼叫,輸出 "Hi!"

function sayHi() {
  console.log("Hi!");
}

在這段程式中,雖然 sayHi() 是寫在定義之前,但 JavaScript 執行前已經幫你做了這件事:

// 引擎在內部會像這樣處理(邏輯順序上):
function sayHi() {
  console.log("Hi!");
}

sayHi(); // 呼叫成功

也就是說,整個函式(名稱 + 主體)都被提升到記憶體中了,所以你可以放心地在它定義之前使用它。

函式表示式(Function Expression):只有變數被提升,函式本體不會

sayHi(); // ❌ TypeError: sayHi is not a function

const sayHi = function () {
  console.log("Hi!");
};

這一段的錯誤通常會讓初學者困惑:為什麼明明有宣告,卻還是不能呼叫?

來看引擎內部實際的處理方式:

// 執行前,只有變數名稱被提升,但沒有賦值
// const sayHi; ← 這是提升的效果(其實不能重複賦值,這裡是邏輯解釋)

sayHi(); // 嘗試呼叫,但 sayHi 還沒有被賦值,是 undefined → 報錯

// 這行直到執行到這裡時才會被賦值為函式
sayHi = function () {
  console.log("Hi!");
};

而因為 constlet 在變數提升後會進入暫時性死區(TDZ, Temporal Dead Zone),這表示你在賦值前不能對它做任何操作,否則會出錯。

為什麼是 TypeError 而不是 ReferenceError?

  • sayHi 在記憶體中是存在的變數(變數名已被提升),但它的值是 undefined 或尚未初始化
  • 因此執行 sayHi() 是對 undefined 呼叫函式,這會造成:TypeError: sayHi is not a function

記憶體模型比喻:兩種函式的「先準備」差異

類型記憶體行為(Hoisting)呼叫時機
函式陳述句函式名稱 + 整個內容在執行前就被「裝入記憶體」定義之前就可以呼叫
函式表示式變數名稱提升,但直到執行賦值行為時才知道是函式只能在賦值之後呼叫

實戰建議

情境建議用法原因
想先定義邏輯、再呼叫使用函式陳述句可提升,程式順序較自由
要把函式當成值、傳遞、封裝使用函式表示式彈性高、結合現代語法佳
嚴格控制執行順序,避免提前使用使用函式表示式 + const保障執行時機一致性

使用時機建議與最佳實務

在 JavaScript 的日常開發中,我們常常會用到函式來封裝邏輯、分離功能或建立模組。

然而,函式有兩種定義方式:函式陳述句(Function Declaration)函式表示式(Function Expression)

這並不是單純的語法偏好問題,而是關乎程式碼的可讀性、執行順序、安全性與維護彈性

請記住:

「選擇適當的函式寫法,是一種寫出穩定、可預測程式的工程習慣。」

以下是常見開發情境中,該如何選擇的具體建議與背後的思維:

情境一:定義工具函式或模組工具 → 使用「函式陳述句」

// utils.js
function formatDate(date) {
  return date.toISOString().split("T")[0];
}

適合用途:

  • 工具函式(如資料格式轉換、驗證器)
  • 常駐模組功能(如初始化環境變數)
  • 重複使用、與其他模組整合的邏輯單元

為什麼選擇函式陳述句?

  • 支援 Hoisting:你可以在檔案上方直接使用函式,不受撰寫順序限制,邏輯更自然。
  • 具名函式可讀性高:在除錯時,錯誤堆疊(stack trace)會明確指出是哪一個函式錯誤。
  • 撰寫習慣清楚直觀:讀者可以一眼就看到這是「主要功能段落」,適合被呼叫多次。

延伸補充:

在多人協作時,將公共工具寫成函式陳述句還能幫助建立統一程式風格。

情境二:建立動態邏輯、傳遞函式、使用閉包 → 使用「函式表示式」

const makeCounter = function () {
  let count = 0;
  return function () {
    return ++count;
  };
};

適合用途:

  • 建立可封裝狀態的函式(如:makeCounter、工廠函式)
  • 回傳或傳遞函式(如:map(fn)setTimeout(fn)
  • 利用閉包記住外部變數

為什麼選擇函式表示式?

  • 具有「值」的特性:你可以將它指定給變數、陣列、物件屬性,甚至作為參數或回傳值。
  • 作用範圍更可控:它不會被提升,因此可避免不小心在賦值前執行錯誤。
  • 支援現代語法搭配:如箭頭函式、匿名函式、條件式函式、動態產生等。

延伸補充:

搭配 constlet 可建立更具可預測性的區塊級作用範圍,有助於撰寫安全的封裝邏輯。

情境三:建立 IIFE(立即執行函式) → 必須使用「函式表示式」

(function () {
  console.log("Init config...");
})();

適合用途:

  • 初始化邏輯(設定變數、設定環境)
  • 建立封閉作用域、避免變數污染全域
  • 寫模組模式(如:早期的 Module Pattern

為什麼必須使用函式表示式?

  • 語法要求:IIFE 必須是一個「值」才能包在括號裡,因此只能使用表示式。
  • 立即執行特性:函式陳述句無法立即執行,表示式才可以馬上產生並執行結果。
  • 安全隔離作用域:可避免定義變數時干擾到其他模組的變數空間。

延伸補充:

在沒有模組系統的環境(如老舊瀏覽器)中,IIFE 是建立私有變數與作用域的重要技巧。

情境四:需要嚴格控制函式執行順序 → 使用「函式表示式」

const stepOne = function () {
  console.log("Step 1");
};

const stepTwo = function () {
  stepOne();
  console.log("Step 2");
};

stepTwo();

適合用途:

  • 多段執行需依序組合邏輯(如組合式資料處理)
  • 嚴格控制某段邏輯不能被過早執行
  • 自定義步驟流程、函式工廠、Hook 系統等

為什麼選擇函式表示式?

  • 不會被提升:避免「尚未初始化」卻被提前呼叫,強化安全性。
  • 撰寫順序 = 執行順序:行為更可預測,減少執行時錯誤。
  • 適合高模組化與動態邏輯:搭配物件導向、函數式組合時特別清晰。

延伸補充:

在使用自動化測試或開發框架(如 Jest、React)時,函式表示式可以讓邏輯結構與測試流程保持一致。

補充小表:何時該選誰?

使用需求/情境適合使用的寫法備註說明
公用工具函式、模組函式函式陳述句提升支援,結構明確
需要封裝狀態、回傳或傳遞函式函式表示式更具彈性
立即執行的模組初始化邏輯函式表示式(IIFE)陳述句不支援括號包裝
執行順序需一致、避免提前使用函式表示式更可預測
搭配箭頭函式、閉包、callback 的寫法函式表示式現代語法友好

如果你希望,我也可以幫你補上「常見錯誤與誤用情境對照表」或「選擇指南圖解」,讓這段內容更具視覺化與參考性,要我幫你整理嗎?

總結:掌握兩種函式寫法,寫出更穩定的程式碼

在 JavaScript 中,函式既是程式邏輯的核心,也是資料的一種形式。

學會靈活使用兩種函式寫法,能幫助你寫出更穩定、可維護、擴充性高的程式碼。

🔁 函式陳述句(Function Declaration)

  • ✅ 適合用來定義常駐工具函式或結構穩定的邏輯模組
  • ✅ 支援提升(Hoisting),撰寫流程更自由
  • ✅ 有明確函式名稱,錯誤追蹤更容易

🧩 函式表示式(Function Expression)

  • ✅ 可作為值,靈活傳遞、賦值、封裝邏輯
  • ✅ 適合搭配閉包、箭頭函式與 callback 等現代語法
  • ✅ 嚴格遵守定義順序,不受 Hoisting 影響,行為可預測

總體建議

如果你想要…建議選擇
快速定義可重用的公用函式函式陳述句
封裝狀態、建立模組、傳遞函式邏輯函式表示式
搭配箭頭函式、閉包、IIFE 等進階技巧函式表示式更適合

掌握這些概念,將為你未來進入如 ReactNode.jsTypeScript 等進階 JavaScript 生態打下穩固基礎。

Similar Posts

發佈留言

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