在 JavaScript 中,函數是物件,可以像變量一樣被賦值、傳遞、返回。理解「函數引用」與「函數調用」的區別,是掌握回調函數、事件綁定、定時器等核心機制的關鍵。
// 正確:傳遞函數本身給 setTimeout
setTimeout(myFunction, 1000);
// 正確:事件綁定傳遞處理函數引用
element.addEventListener('click', handleClick);
// 正確:陣列方法傳遞回調函數
[1,2,3].forEach(processItem);
語義:「請在未來某個時機調用這個函數」
// 錯誤:立即執行 myFunction(),將其返回值 (undefined) 傳給 setTimeout
setTimeout(myFunction(), 1000);
// 錯誤:立即執行 handleClick(),綁定其返回值
element.addEventListener('click', handleClick());
語義:「現在立刻執行這個函數,並將執行結果傳遞出去」
| 場景 | 正確寫法 | 錯誤寫法 | 後果 |
|---|---|---|---|
| setTimeout | setTimeout(fn, 1000) | setTimeout(fn(), 1000) | 立即執行,1秒後嘗試調用 undefined 報錯 |
| 事件綁定 | el.onclick = fn | el.onclick = fn() | 綁定返回值而非函數,點擊無反應或報錯 |
| 陣列遍歷 | arr.map(fn) | arr.map(fn()) | 立即執行一次,map 收到非函數值報錯 |
| Promise | .then(fn) | .then(fn()) | 立即執行,破壞鏈式調用 |
若回調函數需要參數,不能直接寫 setTimeout(fn(arg), 1000),需使用匿名函數包裹或bind:
// 方法一:箭頭函數包裹 (推薦)
setTimeout(() => myFunction(param1, param2), 1000);
// 方法二:匿名函數包裹
setTimeout(function() { myFunction(param1, param2); }, 1000);
// 方法三:Function.prototype.bind
setTimeout(myFunction.bind(null, param1, param2), 1000);
// ❌ 錯誤:全部綁定到同一個 i (最終值)
for (var i = 0; i < 3; i++) {
btns[i].onclick = function() { alert(i); }; // 全彈出 3
}
// ✅ 正確:使用 let 區塊作用域
for (let i = 0; i < 3; i++) {
btns[i].onclick = function() { alert(i); }; // 彈出 0,1,2
}
// ✅ 正確:IIFE 建立閉包 (ES5 寫法)
for (var i = 0; i < 3; i++) {
(function(index) {
btns[index].onclick = function() { alert(index); };
})(i);
}
const obj = {
value: 42,
getValue: function() { return this.value; }
};
setTimeout(obj.getValue, 100); // ❌ this 指向 window,返回 undefined
// 修正方案
setTimeout(obj.getValue.bind(obj), 100); // ✅ bind 綁定 this
setTimeout(() => obj.getValue(), 100); // ✅ 箭頭函數保留外層 this
setTimeout(function() { obj.getValue(); }, 100); // ✅ 匿名函數間接調用
「要傳函數就別加括號,加了括號是執行結果。」
「回調、綁定、定時器,傳的是『做事的配方』不是『做好的菜』。」
徹底理解這一點,將解決 80% 的 JavaScript 非同步與事件處理 Bug。