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!
這裡發生了幾件事:
greet
是一個函數。sayHello = greet
的意思是把函數本身(也就是函數這個「值」)指派給sayHello
。- 這並不會執行
greet()
,只是讓sayHello
變成這個函數的另一個名稱(參照它)。 - 當我們執行
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) - 事件處理(例如按鈕點擊、鍵盤輸入)
- 陣列方法(例如
map
、forEach
、filter
) - 函數式編程技巧(例如高階函數、柯里化)
- 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
這段程式碼的實際流程是:
showMessage()
立即被執行,印出"Hello!"
setTimeout
收到的是undefined
(因為showMessage()
沒有return
)- 所以,1 秒後什麼事也不會發生
✅ 正確示範:
setTimeout(showMessage, 1000); // ✅ 傳遞函數參照,1 秒後執行
這裡我們傳遞的是函數本身(參照),不是它執行後的結果。這樣 setTimeout
才能正確在 1 秒後執行它。
🧠 延伸理解:
語法 | 意義 | 結果 |
---|---|---|
showMessage | 傳遞函數參照 | 可由其他人決定何時執行 |
showMessage() | 執行函數,取得回傳值 | 馬上執行,傳遞的是結果 |
你可以把它想像成:
showMessage
是一顆尚未爆炸的煙火,交給別人控制什麼時候點燃showMessage()
是你現在就點燃它,立刻炸完,遞出去的只是一團煙
錯誤二:要傳參數卻直接執行函數
如果你有一個需要參數的函數,卻直接傳入 setTimeout
或 addEventListener
,很可能會誤用括號造成立即執行。
這時候的正確解法,是使用匿名函數(或箭頭函數)包起來。
⚠️ 錯誤示範:
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 引擎會:
- 建立一個執行環境(Execution Context)
- 將這個環境推進「呼叫堆疊(Call Stack)」
- 執行函數體裡的程式碼
- 將執行結果(如果有)回傳給呼叫端
- 將該執行環境從堆疊中移除
這些細節屬於「函數執行流程」的底層原理,與「函數參照」本身是兩個不同層面的概念。
不過,認識到這一點可以幫助你建立一個重要觀念:
✅ 參照只是一種「指向」,而執行是另一件事,發生在你明確呼叫(加上
()
)時。
延伸閱讀: 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 的基礎
理解「函數參照」這個概念,不只是幫你搞懂 setTimeout
或 addEventListener
,更是往高階 JavaScript 開發之路的第一步。
許多現代框架(如 React)在設計元件時,也大量使用函數參照來控制元件的行為與生命週期。
你可以把函數參照想像成「指向函數的遙控器」,這個遙控器可以交給別人使用,但真正的「執行」行為仍由函數本身控制。
未來當你學習到像是閉包(closure)、柯里化(currying)、高階函數(higher-order function)時,對「函數參照」的理解將會是你非常重要的底層基礎。