JavaScript 函數參照(Function Reference)完整入門指南

更新日期: 2025 年 3 月 29 日

在 JavaScript 中,函數不僅僅是執行某段程式碼的工具,它們本身也是「值」。

這意味著函數可以被賦值給變數、當作參數傳入其他函數、甚至作為函數的回傳值傳出。

而當我們談到「函數參照(Function Reference)」時,指的正是這些操作的底層概念。

很多初學者在學習函數時,可能只注意到函數是怎麼寫的、怎麼執行,卻忽略了「函數參照」這個關鍵概念。

這可能導致對高階函數、事件處理、回呼函數(callback)等進階主題理解不清。

本篇文章將用最簡單、最實際的方式,帶你一步步了解 JavaScript 中的函數參照到底是怎麼一回事。


什麼是函數參照?

函數是一種「值」:JavaScript 的靈魂特性之一

在 JavaScript 中,函數不只是執行某段程式碼的工具,它也是一種「值」。

這代表你可以像操作其他值(例如數字、字串、物件)一樣,操作函數。這種語言特性被稱為「第一等公民(first-class citizen)」,意思是:函數享有和其他值一樣的地位與待遇。

這些待遇包括:

  • 把函數賦值給變數
  • 把函數作為參數傳給其他函數
  • 從一個函數中回傳另一個函數

換句話說,函數在 JavaScript 中不是只能「寫來執行」,它也可以「被存起來」、「被轉交」、「被產生出來」。這些特性讓 JavaScript 的程式設計變得靈活且強大。

來看一個例子:

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

const sayHello = greet; // 沒有加括號,是參照,不是執行
sayHello(); // 執行 sayHello,其實就是執行 greet,印出 Hello!

這裡發生了幾件事:

  1. greet 是一個函數。
  2. sayHello = greet 的意思是把函數本身(也就是函數這個「值」)指派給 sayHello
  3. 這並不會執行 greet(),只是讓 sayHello 變成這個函數的另一個名稱(參照它)。
  4. 當我們執行 sayHello(),就等於執行 greet()

💡 想像這個過程就像:

  • greet 是原本的遙控器。
  • sayHello = greet 是你又複製了一個遙控器。
  • sayHello() 就好像按下複製的遙控器,效果一樣!

什麼是「參照」,不是「執行」?

這是許多初學者一開始最容易混淆的部分。

你可能會看到兩種寫法:

setTimeout(greet, 1000);  // 傳入函數參照
setTimeout(greet(), 1000); // 傳入函數執行的結果

這兩行的差別在於:

寫法意思何時執行
greet傳入「函數參照」延後執行(由 setTimeout 控制)
greet()執行函數後,把回傳結果傳入立刻執行,並把回傳值(可能是 undefined)傳進去

用生活比喻來幫助你更好理解:

  • 📷 函數的參照(greet):像是你拍了一張朋友的照片,你手上的是「朋友的參照」。
  • 🧍‍♂️ 函數本身(本人):是你朋友真正站在你面前。
  • 📣 函數的執行(greet()):是你叫你朋友「出場表演」!

所以:

  • greet 是「照片」或「身分證」:可以交給別人、收藏起來。
  • greet() 是「叫他現在表演」:執行這個函數的內容。

再看一個實務例子:

function getGreeting() {
  return "Hello!";
}

const greetRef = getGreeting;      // 沒有執行,只是取得函數的參照
const greetResult = getGreeting(); // 執行函數,取得回傳的字串

console.log(greetRef);    // 印出函數本身:ƒ getGreeting() { return "Hello!"; }
console.log(greetRef());  // 印出 Hello!(執行參照,取得結果)
console.log(greetResult); // 印出 Hello!(這是已經執行過的結果)

🔍 解說:

  • getGreeting 是一個函數,可以被參照、傳遞或執行。
  • greetRef = getGreeting:這是「函數參照」,並沒有執行。
  • greetRef():這是「透過參照執行」函數,取得回傳值。
  • greetResult = getGreeting():這是「執行後的回傳值」,不是函數了。

🧠 簡單類比:

行為類比
getGreeting像是拿到食譜(可以傳給別人)
getGreeting()煮出一道料理(使用食譜)
greetRef = getGreeting拿到一份食譜的影本
greetRef()用影本也能煮出一樣的料理
greetResult保存好的料理成品(不能再改變內容)

為什麼需要理解函數參照?

很多初學者一開始學 JavaScript 時,對函數的理解大多停留在:

「我寫了一個函數,我呼叫它,它執行完就沒事了。」

但事實上,理解「函數參照(Function Reference)」是邁向進階 JavaScript 的基礎知識,它幾乎涵蓋你未來在開發中會接觸到的所有關鍵主題,例如:

  • 非同步操作(例如 setTimeout、AJAX 請求、Promise)
  • 事件處理(例如按鈕點擊、鍵盤輸入)
  • 陣列方法(例如 mapforEachfilter
  • 函數式編程技巧(例如高階函數、柯里化)
  • React、Vue 等框架中的元件事件與狀態管理

而這一切的根本,就是 你必須知道「何時在傳參照」、「何時在執行函數」

回呼函數(Callback Function)

所謂「回呼函數」,簡單來說就是:

「我把這個函數的參照交給別人(另一個函數),等時機到了再由他來執行。」

你可能覺得這聽起來抽象,我們來看一個大家都學過的範例:setTimeout

function greet() {
  return "Hello!";
}

setTimeout(greet, 1000);     // ✅ 正確:1 秒後執行 greet 函數
setTimeout(greet(), 1000);   // ❌ 錯誤:馬上執行 greet 並把結果傳入 setTimeout

✅ 正確:傳入「函數參照」

setTimeout(greet, 1000);
  • greet 是函數的參照(也就是那把「鑰匙」),我們把它交給 setTimeout
  • JavaScript 引擎會在 1 秒之後執行這個函數,也就是 greet()
  • 重點是:我們沒有在這裡執行它,而是讓 setTimeout 控制執行時機。

這樣的做法允許我們做「非同步操作」,例如延遲執行、事件響應、API 請求完成後再處理等。

這種「把函數交出去,稍後執行」的設計,就是回呼函數的核心。

❌ 錯誤:傳入「函數執行結果」

setTimeout(greet(), 1000);
  • greet() 是函數的執行結果,會立刻執行,回傳 "Hello!"
  • 所以實際等於:setTimeout("Hello!", 1000)
  • setTimeout 需要的是一個函數參照,不是一個字串,所以這會導致意料之外的錯誤(什麼都不會發生)。

🧠 生活比喻:鑰匙 vs 房間

  • greet 就像一把「房間鑰匙」,你交給別人,他可以選擇何時開門
  • greet() 就是你現在立刻打開門,然後把裡面的東西拿給別人(但他其實只想要鑰匙)。

這種誤用是初學者最常見的錯誤之一,在非同步操作中尤其容易踩雷。

🧪 延伸範例:map、forEach 等內建方法

Array.prototype.map()forEach()filter() 等陣列方法,也都需要「傳入函數參照」來對每個元素進行操作:

const numbers = [1, 2, 3];

function double(num) {
  return num * 2;
}

const result = numbers.map(double); // 傳入函數參照,不是 double()
console.log(result); // [2, 4, 6]

事件監聽(Event Listener)

另一個非常常見的場景,就是事件監聽(例如使用者點擊按鈕、輸入文字)。

const button = document.querySelector("button");

function handleClick() {
  console.log("Button clicked!");
}

✅ 正確寫法:傳入函數參照

button.addEventListener("click", handleClick);
  • 這裡我們把 handleClick 這個函數的參照交給 addEventListener
  • 它會在使用者點擊按鈕時,自動呼叫這個函數

❌ 錯誤寫法:傳入函數執行結果

button.addEventListener("click", handleClick());
  • handleClick()在程式執行當下馬上被執行一次,輸出 Button clicked!
  • 然後它的回傳值(undefined)被交給 addEventListener
  • 結果:點擊按鈕時什麼都不會發生,因為你綁定的不是函數,而是 undefined

🧠 小提醒:事件還沒發生,怎麼可以先執行?

就像你告訴朋友:「按下這個按鈕時,請叫我一聲」。

你是先把「任務(函數參照)」交給他,而不是現在就叫你。這樣才能在事件發生的時候再執行對應的函數

小結:理解函數參照的必要性

使用場景為什麼要傳「函數參照」?
setTimeout(fn)延遲執行,等時間到才要執行函數
addEventListener等使用者操作後才執行處理函數
array.map(fn)對每一項元素重複執行某函數
Promise.then(fn)非同步操作結束後才執行下一步的函數
React onClick={fn}事件觸發時執行,不該立即執行函數

這些例子共同的核心觀念就是:

我們不是「現在就要執行某個函數」,而是「把它交出去,等時機到了再執行」。


如何創造與傳遞函數參照

將函數指派給變數

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

const greet = sayHi;
greet(); // 印出 Hi!

你也可以用匿名函數或箭頭函數創造新的參照:

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

const greetArrow = () => {
  console.log("Hi!");
};

函數當作參數傳遞

function callTwice(fn) {
  fn();
  fn();
}

callTwice(sayHi); // 傳遞參照,並在函式內執行兩次

函數作為回傳值

function multiplier(factor) {
  return function(num) {
    return num * factor;
  };
}

const double = multiplier(2);
console.log(double(5)); // 印出 10

這裡的 multiplier(2) 回傳一個新的函數,並將它指派給 double。這種設計在「閉包」與「函數工廠」中很常見。


函數參照常見錯誤與陷阱

雖然「函數參照」的概念聽起來很簡單:把函數當作值傳來傳去。

但實際上,初學者在開發過程中最常踩雷的地方,正是搞錯了「參照」與「執行」的時機,或是在需要傳遞參數時不知道該怎麼正確包裝。

這章會介紹兩個最常見的錯誤情境:

錯誤一:把「執行」當作「參照」來傳

這是最經典、也最容易出現的錯誤。初學者常會不小心在傳遞函數時,加上了 () 括號,導致函數立刻被執行,而不是等到預期的時間或事件才執行。

🔧 錯誤示範:

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

setTimeout(showMessage(), 1000); // ❌ 錯誤:馬上執行 showMessage

這段程式碼的實際流程是:

  1. showMessage() 立即被執行,印出 "Hello!"
  2. setTimeout 收到的是 undefined(因為 showMessage() 沒有 return
  3. 所以,1 秒後什麼事也不會發生

✅ 正確示範:

setTimeout(showMessage, 1000); // ✅ 傳遞函數參照,1 秒後執行

這裡我們傳遞的是函數本身(參照),不是它執行後的結果。這樣 setTimeout 才能正確在 1 秒後執行它。

🧠 延伸理解:

語法意義結果
showMessage傳遞函數參照可由其他人決定何時執行
showMessage()執行函數,取得回傳值馬上執行,傳遞的是結果

你可以把它想像成:

  • showMessage 是一顆尚未爆炸的煙火,交給別人控制什麼時候點燃
  • showMessage() 是你現在就點燃它,立刻炸完,遞出去的只是一團煙

錯誤二:要傳參數卻直接執行函數

如果你有一個需要參數的函數,卻直接傳入 setTimeoutaddEventListener,很可能會誤用括號造成立即執行。

這時候的正確解法,是使用匿名函數(或箭頭函數)包起來

⚠️ 錯誤示範:

function say(message) {
  console.log(message);
}

setTimeout(say("Hi"), 1000); // ❌ 錯誤:立即執行 say("Hi")

這裡 say("Hi") 會在程式執行時立刻執行一次,並把回傳值(undefined)傳進 setTimeout,1 秒後什麼都不會發生。

✅ 正確寫法:用匿名函數包裝

setTimeout(() => say("Hi"), 1000); // ✅ 傳遞匿名函數,延遲執行 say("Hi")

這裡的箭頭函數 () => say("Hi") 是一個匿名函數,它本身不會立即執行,而是被當作參照傳進去。等 1 秒之後,setTimeout 才會執行這個匿名函數,從而觸發 say("Hi")

🧠 延伸說明:這其實是兩段式操作

setTimeout(() => say("Hi"), 1000);

等同於:

const wrapper = () => say("Hi");
setTimeout(wrapper, 1000);

這樣的寫法在實務中非常重要,尤其是在:

  • 傳遞需要參數的回呼函數
  • 對應不同按鈕時需要傳入不同資料
  • 搭配陣列操作、迴圈綁定事件等情境

小結:遇到參照陷阱時怎麼辦?

錯誤行為結果解法
fn() 傳入 setTimeout立即執行,傳入回傳值(通常是 undefined)改成 fn
fn(args) 傳入事件監聽器立即執行,沒綁到正確的事件處理函數改成 () => fn(args)
忘記包裝需要參數的函數參照函數立即執行,效果錯誤或失效用匿名函數包起來 () => fn(args)

補充:當參照搭配參數時的實用範例

const buttons = document.querySelectorAll("button");

function handleClick(message) {
  console.log(message);
}

buttons.forEach((btn, index) => {
  btn.addEventListener("click", () => handleClick(`你按了第 ${index + 1} 個按鈕`));
});

這裡每個匿名函數內部都傳遞了不同參數,讓每個按鈕點擊時可以顯示不同的訊息。這樣的用法在真實開發中非常常見。


太棒了!你畫的這張圖非常清晰,完整呈現了 JavaScript 中「函數參照」從宣告、儲存、傳遞、調用,到垃圾回收的整個生命週期。

我依照這張圖,撰寫一段「函數參照的運作邏輯」章節,以文字形式呈現流程圖中的每一步,並補充實際程式碼與說明,讓讀者能夠在沒有圖的情況下,也能想像整個流程是如何運作的。


函數參照的運作邏輯

我們常說「函數參照是一種值」,但這其實背後牽涉一整套運作機制。

這一節,我們來從記憶體配置的角度,一步一步理解函數參照到底是怎麼誕生、怎麼傳遞、怎麼被執行,最後又是如何被回收的。

函數是怎麼被建立的?

📦 函數宣告

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

當 JavaScript 解譯這段程式碼時,它會做以下幾件事:

🧠 在記憶體中分配空間

  • JavaScript 引擎會在記憶體中為這個函數 greeting 分配空間,儲存它的程式邏輯(也就是函數體)與一些額外的資訊(像是作用域、執行環境等)。

🔗 建立函數參照

  • 變數 greeting 本身並不「包含」這整段邏輯,它只是持有一個參照(reference),指向記憶體中那塊儲存函數邏輯的區域。

你可以把這想像成這樣:

函數就像是一個機器人,放在倉庫裡等待被派上用場,而變數 greeting 則是一個遙控器,你只要手上有這個遙控器,就可以隨時遙控那個機器人幫你完成任務。

函數參照可以怎麼使用?

一旦你有了這個函數參照,你就可以把它:

✅ 1. 賦值給另一個變數:

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


const sayHello = greeting;
  • sayHello 不是複製了一個新函數,而是取得了同一份函數的參照
  • 所以你可以用 sayHello() 來執行原本的 greeting() 函數。

✅ 2. 傳給其他函數當作參數(回呼函數):

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

function doSomething(callback) {
  callback();
}

doSomething(greeting); // greeting 被當作參照傳入

這種設計方式,就是「高階函數」的核心邏輯:函數可以接收別的函數作為參數,並在裡面呼叫它

✅ 3. 作為函數的回傳值(函數工廠 / 閉包):

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

function getGreeter() {
  return greeting;
}

const greeter = getGreeter();
greeter(); // 執行原本的 greeting 函數
  • getGreeter() 執行後會回傳一個函數參照,這就是「函數回傳函數」的模式。
  • 若內部函數使用了外部變數,那麼它還會形成一個閉包(closure),保有當時的詞法環境。

函數參照何時會被執行?

到目前為止,我們談的「函數參照」其實都還沒發生「執行」這件事。

函數參照就像一支尚未按下的遙控器,或一個指向任務的連結,只有在你加上 () 呼叫它時,JavaScript 才會真正去執行該函數的內容。

舉個例子:

function greet() {
  return "Hello!";
}

const sayHello = greet; // 這裡只是建立參照,尚未執行
const result = sayHello(); // 這裡才是執行:sayHello() → 執行 greet → 回傳值

當你呼叫 sayHello() 時,其實就是透過這個參照去觸發真正的 greet 函數執行。

🧠 呼叫函數背後發生什麼事?

當函數參照被呼叫,JavaScript 引擎會:

  1. 建立一個執行環境(Execution Context)
  2. 將這個環境推進「呼叫堆疊(Call Stack)」
  3. 執行函數體裡的程式碼
  4. 將執行結果(如果有)回傳給呼叫端
  5. 將該執行環境從堆疊中移除

這些細節屬於「函數執行流程」的底層原理,與「函數參照」本身是兩個不同層面的概念。

不過,認識到這一點可以幫助你建立一個重要觀念:

參照只是一種「指向」,而執行是另一件事,發生在你明確呼叫(加上 ())時。

延伸閱讀: JS 底層運作邏輯系列文

當然可以!以下是《5.4 函數參照何時會被清除?》的詳細擴寫版本,包含背景知識、範例、與常見誤解澄清,讓初學者能更全面理解「函數何時從記憶體中被釋放」這件事的判斷邏輯與實際應用。

函數參照何時會被清除?

JavaScript 是一種具有自動垃圾回收(Garbage Collection)制的語言,也就是說,你不需要自己手動釋放不再使用的記憶體空間,JavaScript 引擎會在適當時機幫你完成這件事。

🧠 什麼是「垃圾回收」?

當一段記憶體中的資料,已經無法從任何地方存取,也就是「沒有任何參照指向它」,這段資料就會被視為垃圾(garbage),可以被安全地從記憶體中移除,釋放空間供其他變數或物件使用。

這個邏輯同樣適用於函數,因為函數在 JavaScript 中本質上也是一種值(value)。

📌 當函數參照被清除的條件

一個函數會被垃圾回收的時機是:

當這個函數的參照不再被任何變數、參數、物件屬性或閉包持有時,這個函數就成為「可回收」的對象。

來看個簡單的例子:

let f = function () {
  return 42;
};

f = null; // ❌ f 不再參照原本的函數

在這段程式碼中:

  • 一開始 f 是一個指向匿名函數的參照
  • 當你寫 f = null 時,原本那個函數的參照被斷開
  • 此時如果沒有其他任何變數還指向那個函數
  • JavaScript 引擎會在之後的某個時機將那個匿名函數標記為垃圾,並釋放其佔用的記憶體

❗ 常見誤解:設為 null ≠ 立即回收

值得注意的是,將變數設為 null 只是解除參照,不代表那段記憶體「立刻」被清除。

垃圾回收是由引擎在背景進行的,它的執行時機是不可預測的,依賴:

  • 當前的記憶體使用情況
  • 執行環境(如瀏覽器或 Node.js)
  • 垃圾回收器的策略(如標記-清除、引用計數等)

小結:整個函數參照的生命歷程

階段說明
宣告建立函數並分配記憶體
建立參照將參照儲存在變數、參數、或作為回傳值
傳遞把參照傳給其他函數或元件
執行呼叫參照(加上 ())建立執行環境並執行函數體
銷毀所有參照都解除時,函數成為垃圾回收對象

結語:掌握函數參照,打好進階 JavaScript 的基礎

理解「函數參照」這個概念,不只是幫你搞懂 setTimeoutaddEventListener,更是往高階 JavaScript 開發之路的第一步。

許多現代框架(如 React)在設計元件時,也大量使用函數參照來控制元件的行為與生命週期。

你可以把函數參照想像成「指向函數的遙控器」,這個遙控器可以交給別人使用,但真正的「執行」行為仍由函數本身控制。

未來當你學習到像是閉包(closure)、柯里化(currying)、高階函數(higher-order function)時,對「函數參照」的理解將會是你非常重要的底層基礎。

Similar Posts