跳至主要内容

課程:JavaScript 與 React 底層原理 第 9 堂:ES6+ 語法收尾

26 箭頭函數語意

在上一部分中,我們掌握了 Spread 與 Rest 運算子,這讓函數參數的處理變得極其靈活。如果你觀察現代 React 專案的程式碼,你會發現傳統的 function 關鍵字幾乎消失了,取而代之的是遍地開花的箭頭函數(Arrow Functions)。

但箭頭函數真的只是 function 的「簡寫」嗎?如果只是為了少打幾個字,JavaScript 引擎不需要特地引入一個全新的函數類型。事實上,箭頭函數在底層執行機制上與一般函數有著天壤之別。今天,我們要拆解箭頭函數的設計動機,看它如何優雅地解決了 JavaScript 歷史悠久的 this 難題,並成為 React 開發的標準配備。

語法靈活性:隱式回傳與寫法的藝術

箭頭函數最直觀的改變是語法。它提供了多種縮寫形式,這在處理高階函數(如 map, filter)時非常強大。

隱式回傳(Implicit Return)的規則

在一般函數中,如果你想回傳一個值,必須明確寫出 return。但在箭頭函數中,如果你省略了大括號 {},它就會進入「表達式模式」,自動回傳該表達式的結果。

// 一般函數:必須明確 return
const add = function(a, b) {
return a + b;
};

// 箭頭函數:隱式回傳
const addArrow = (a, b) => a + b;

這看起來很美好,但這裡潛藏著一個初學者必踩的陷阱:當你想隱式回傳一個「物件字面值」時。

請預測以下程式碼的執行結果:

const getPoint = (x, y) => { x: x, y: y };
console.log(getPoint(1, 2));

揭曉答案: 它會回傳 undefined。 為什麼?因為 JavaScript 引擎看到箭頭後面的 { 時,會優先將其解析為「函數體的區塊(Block)」,而不是物件的開括號。因此,引擎會認為你在執行兩行標籤語句(Label Statements),且沒有任何 return,最後自然得到 undefined

正確做法: 用小括號 () 包裹物件,明確告訴引擎「這是一個表達式」。

const getPoint = (x, y) => ({ x: x, y: y }); // 成功回傳物件

在 React 中,當你在 map 列表並回傳一個包含多個屬性的物件給下一層元件時,這個括號至關重要。


詞法 This(Lexical This):箭頭函數的靈魂

這是箭頭函數最核心的特性。在 Topic 3 中,我們學到了一般函數的 this 是「動態綁定」的,取決於呼叫方式。這導致了一個經典的災難:在非同步回調或事件處理器中,this 經常會「遺失」。

預測:誰拿到了 this?

思考以下情境:我們有一個計時器物件,希望每秒增加一次分數。

const game = {
score: 0,
start: function() {
setTimeout(function() {
this.score++;
console.log("Current score:", this.score);
}, 1000);
}
};

game.start();

執行後你會發現,this.score 變成了 NaN。這是因為 setTimeout 的回調函數是在全域環境下被獨立呼叫的(預設綁定),此時的 this 指向全域物件(或為 undefined),而非 game 物件。

在 ES6 之前,開發者必須使用 var self = this.bind(this) 來硬生生地把 this 鎖定住。

揭曉:箭頭函數如何解決問題

箭頭函數採用的是 Lexical This(詞法 This)。這意味著:箭頭函數本身完全沒有自己的 **this** 綁定。

回到 Topic 1 的「執行環境(Execution Context, EC)」概念。當一個函數被呼叫時,引擎會建立一個 EC。一般函數的 EC 會包含一個 this 綁定;但箭頭函數的 EC 則跳過這個步驟。

當你在箭頭函數內部存取 this 時,它會遵循 Scope Chain(作用域鏈),向外層環境查找,直到找到一個擁有 this 的環境為止。這就像是箭頭函數背了一個「環境背包」,裡面裝的是定義它時外層環境的 this

我們用箭頭函數重寫上面的例子:

const game = {
score: 0,
start: function() {
// 箭頭函數定義在此處,外層是 start 函數的環境
// 此時 start 的 this 是指向 game 物件
setTimeout(() => {
this.score++;
console.log("Current score:", this.score);
}, 1000);
}
};

game.start(); // 成功印出 "Current score: 1"

因為箭頭函數沒有自己的 this,它直接「捕獲」了 start 函數執行時的 this(即 game 物件)。這使得非同步程式碼的撰寫變得直覺且安全。

深度筆記: 因為箭頭函數沒有自己的 this 綁定,所以對它使用 call(), apply(), 或 bind()無效的。你無法強行改變一個本來就不存在的東西。


與一般函數的四大本質差異

為了保持輕量與特定的用途,箭頭函數被剝離了許多一般函數具備的能力。理解這些限制,才能避免在不該使用它的地方出錯。

1. 沒有自己的 this (Lexical This)

如前所述,它透過作用域鏈查找外層的 this。這讓它成為回調函數(Callbacks)的完美選擇,但卻不適合定義物件的方法。

錯誤範例:

const user = {
name: 'Aria',
greet: () => console.log(`Hi, I'm ${this.name}`)
};
user.greet(); // Hi, I'm undefined (因為 this 向上查找到全域)

2. 沒有 arguments 物件

在一般函數中,你可以存取隱含的 arguments 物件來獲取所有傳入的參數。箭頭函數則沒有這個物件。如果你嘗試存取它,同樣會觸發作用域鏈查找,拿到外層函數的 arguments

現代解法: 永遠使用我們在 5.2 學過的 Rest 運算子

const sum = (...args) => args.reduce((a, b) => a + b, 0);

這比 arguments 更強大,因為 args 是一個真正的陣列,可以直接使用 reduce 等方法。

3. 沒有 prototype 屬性

一般函數在建立時,會自動獲得一個 prototype 屬性,用來支援原型繼承。然而,箭頭函數的設計初衷是作為「純粹的邏輯片段」,而不是物件的藍圖,因此它完全沒有這個屬性。

const MyFunc = () => {};
console.log(MyFunc.prototype); // undefined

4. 不能作為建構函數 (Constructor)

這點與第 3 點息息相關。因為沒有 prototype,箭頭函數無法配合 new 關鍵字使用。如果你嘗試執行 new () => {},JavaScript 會直接拋出 TypeError: ... is not a constructor


React 元件與事件處理器選用箭頭函數的設計邏輯

在 React 的發展史中,箭頭函數扮演了關鍵角色。

為什麼 React 元件喜歡用箭頭函數?

雖然 React 同時支援 function MyComponent()const MyComponent = () => ...,但後者在現代開發中更受歡迎。

  1. 宣告式風格: const 表明了元件是一個穩定的值,不會被重新賦值。
  2. 配合 Hooks: Hooks(如 useState)的核心理念是函數式編程。箭頭函數簡潔的語法與 useEffect 中的匿名回調完美契合。
  3. 強制定義後使用: 根據我們在 Topic 1.4 學過的 Hoisting 規則,const 定義的箭頭函數存在 TDZ(暫時性死區),必須先定義才能使用。這能避免在複雜檔案中發生「還沒定義元件就先呼叫」的潛在混亂。

事件處理器的標準寫法

在 React 處理點擊事件時,我們經常寫成:

const handleClick = () => {
console.log("Clicked!");
};

return <button onClick={handleClick}>Click Me</button>;

即便在舊版的 Class Component 中,我們也傾向將方法定義為箭頭函數(稱為 Class Properties 語法),目的是為了自動綁定 this,避免在傳遞給 onClick 時導致 this 變成 undefined

雖然在 Hooks 的 Functional Component 中,我們已經不再頻繁使用 this,但箭頭函數帶來的 Closure(閉包) 特性,正是 Hooks 能夠「記住」狀態的底層基石(回憶 Topic 2.5)。


總結與銜接

核心觀念回顧

  • 隱式回傳: 只有在單行表達式且不使用大括號時生效;回傳物件時必須加小括號 ({ ... })
  • 詞法 This: 箭頭函數沒有自己的 this,它是透過作用域鏈捕獲定義時外層環境的 this。這解決了回調函數中 this 遺失的經典問題。
  • 輕量化設計: 箭頭函數沒有 arguments、沒有 prototype、不能 new。這使它在記憶體使用上比一般函數更為高效。

掌握了箭頭函數的語意後,你已經具備了閱讀與撰寫現代化 JavaScript 代碼的核心能力。但隨著專案規模擴大,我們不可能把所有函數都寫在同一個檔案裡。在下一部分中,我們將進入 ES 模組系統(ESM),學習如何利用 importexport 組織這些函數,並理解為何這種靜態結構對於現代前端工具的效能優化(如 Tree Shaking)至關重要。