跳至主要内容

課程:JavaScript 與 React 底層原理 第 6 堂:Topic 3 收尾與複習

17 This 與箭頭函數

想像一下,你正在寫一個計時器程式。你在物件中定義了一個方法來啟動倒數,一切看起來都很完美:你使用了 this.seconds 來存取資料。但當你把這段邏輯放入 setTimeout 回調函數後,程式突然崩潰了,報錯說 thisundefined 或者指向了全域物件。

為什麼同樣的程式碼,換個地方執行,它的「身份(this)」就變了?

在 JavaScript 中,this 就像是一個代名詞。在中文裡,如果我說「這是我家」,「這」指的是我站的地方;如果你說「這是我家」,「這」指的是你站的地方。this 的含義並非固定不變,它取決於誰在什麼情況下呼叫了這個函數。理解 this 的動態綁定規律,是掌握 JavaScript 物件導向行為與 React 事件處理的關鍵。

this 的四種動態綁定規則

在 ES6 箭頭函數出現之前,JavaScript 的 this 只有一種核心行為:它是在「呼叫時」被決定的,而不是在「定義時」被決定的。 根據呼叫方式的不同,我們通常將其歸納為四種規則。

預設綁定 (Default Binding)

這是最簡單也最容易出錯的情況。當一個函數被當作「獨立函數」直接呼叫時(不屬於任何物件),this 會指向全域物件(在瀏覽器中是 window)。

function greet() {
console.log(this); // 在嚴格模式下是 undefined,非嚴格模式是 window
}

greet();

為什麼這很重要? 在現代的 React 或模組化開發中,我們幾乎總是在「嚴格模式(Strict Mode)」下運行。在嚴格模式下,為了安全起見,禁止 this 指向全域物件,因此它會是 undefined。如果你在一個獨立函數中試圖存取 this.props,就會立刻觸發錯誤。這也是為什麼我們需要接下來的綁定規則。

隱式綁定 (Implicit Binding)

當函數作為某個物件的方法被呼叫時,this 會指向「呼叫該函數的那個物件」。你可以把它想像成:誰點出了這個函數,this 就指向誰。

const user = {
name: "小明",
sayHi: function() {
console.log(`你好,我是 ${this.name}`);
}
};

user.sayHi(); // 「小明」點出了 sayHi,所以 this 是 user

然而,隱式綁定有一個非常著名的「遺失(Lost)」問題。如果你把 user.sayHi 賦值給一個變數,然後呼叫那個變數,this 就會消失:

const talk = user.sayHi;
talk(); // 出現錯誤!此時是獨立呼叫,套用預設綁定,this 變成了 undefined

這在 React 中非常常見。當你把一個元件方法傳給 onClick={this.handleClick} 時,你其實是在傳遞一個函數參考,當 React 真正觸發點擊事件時,它是「獨立呼叫」這個函數的,這就是為什麼你的 handleClick 內部會找不到 this

顯式綁定 (Explicit Binding)

如果你想強行指定 this 指向誰,而不理會它是怎麼被呼叫的,你可以使用 callapplybind

  • callapply:立即執行函數,並將第一個參數綁定為 this
  • **bind**:最為特殊,它不會立即執行,而是會回傳一個全新的函數,這個新函數的 this 已經被永久「鎖死」在你指定的物件上。
function intro(age) {
console.log(`我是 ${this.name},今年 ${age}`);
}

const person = { name: "阿強" };

// 鎖定 intro 的 this 為 person
const boundIntro = intro.bind(person);
boundIntro(25); // "我是 阿強,今年 25 歲"

在早期的 React Class Component 中,我們必須在 constructor 裡寫大量的 this.handleClick = this.handleClick.bind(this),其目的就是為了防止隱式綁定遺失。

new 綁定

當我們使用 new 關鍵字呼叫建構函數時,JavaScript 會背後執行以下步驟:

  1. 建立一個全新的空物件。
  2. 將該函數的 **this** 綁定到這個新物件上。
  3. 執行函數內容。
  4. 回傳該物件。
function Hero(name) {
this.name = name;
}

const ironMan = new Hero("東尼");
console.log(ironMan.name); // "東尼"

這四種規則涵蓋了傳統 JavaScript 函數的所有行為。但是,這些規則太過瑣碎,且容易因為非同步操作(如 setTimeout)而產生意料之外的 this 漂移。


箭頭函數:打破規則的 Lexical This

ES6 引入的箭頭函數(Arrow Functions)徹底改變了遊戲規則。它不適用於上述的四種綁定規則,因為:箭頭函數本身根本沒有自己的 **this**

什麼是 Lexical This(詞法 this)?

當你在箭頭函數內部使用 this 時,JavaScript 會像尋找普通變數一樣,透過 Scope Chain(作用域鏈) 向上層層查找。它會「捕獲」定義該箭頭函數時,當前所在環境的 this

換句話說:

  • 普通函數: this 是在執行時決定的(動態)。
  • 箭頭函數: this 是在定義時決定的(靜態)。

讓我們看一個經典的對比:

const timer = {
seconds: 0,
// 情況 A:普通函數
startRegular: function() {
setTimeout(function() {
// 这里的 this 是 setTimeout 呼叫的,套用預設綁定
console.log(this.seconds);
}, 1000);
},
// 情況 B:箭頭函數
startArrow: function() {
setTimeout(() => {
// 這裡沒有 this,所以向上找 startArrow 的執行環境
// startArrow 是被 timer 點出來的(隱式綁定),其 this 是 timer
console.log(this.seconds);
}, 1000);
}
};

timer.startRegular(); // undefined (或報錯)
timer.startArrow(); // 0 (成功存取!)

為什麼箭頭函數是開發首選?

startRegular 中,setTimeout 的回調函數是一個普通函數。當一秒鐘過去,JS 引擎執行該回調時,它是「獨立呼叫」的。所以內部的 this 變成了全域物件,找不到 seconds

但在 startArrow 中,箭頭函數在定義的那一刻,就已經「看準」了外層 startArrowthis(即 timer 物件),並把這個參考鎖進了自己的「環境背包(Closure)」裡。無論之後是誰呼叫這個回調,它永遠記得它的家在哪裡。

這就是為什麼箭頭函數不需要 **.bind(this)**。它天生具備維持上下文連貫性的能力。


常見陷阱:箭頭函數不是萬靈丹

雖然箭頭函數很好用,但因為它「沒有自己的 this」,在某些場景下反而會弄巧成拙。

陷阱一:作為物件的方法

如果你希望方法內部的 this 指向物件本身,千萬不要用箭頭函數定義該方法。

const user = {
name: "小美",
// 錯誤示範
sayHi: () => {
console.log(this.name);
}
};

user.sayHi(); // undefined

預測一下發生了什麼? 當你定義 user 物件時,sayHi 箭頭函數被建立了。此時它的外層環境是「全域作用域」(因為物件大括號 {} 不會產生新的作用域)。因此,這個箭頭函數捕獲的是全域的 this。當你執行 user.sayHi() 時,它依然指向全域,而不是 user

陷阱二:原型鏈上的方法

在上一節我們討論過 Prototype。如果你想在原型上增加方法,也必須使用普通函數。

function Dog(name) {
this.name = name;
}

// 錯誤:這會導致 this 指向全域
Dog.prototype.bark = () => {
console.log(`${this.name} 汪汪叫`);
};

const myDog = new Dog("來福");
myDog.bark(); // "undefined 汪汪叫"

因為箭頭函數無法透過 callapply 改變其 this,它一旦定義,其 this 就已經在詞法階層上固定了。


與 React 的深度連結

為什麼我們要在這門課程中強調 this 的規則?即使我們現在大都使用 Functional Components (Hooks),this 的邏輯依然深深影響著 React 的設計。

事件處理器(Event Handlers)的抉擇

在 React 元件中,我們經常定義事件處理函數。考慮以下兩種寫法:

// 寫法 A:普通函數(在舊版 Class 元件或某些 JS 框架中常見)
function handleClick() {
console.log(this.state);
}

// 寫法 B:箭頭函數
const handleClick = () => {
console.log(this.state);
}

在 React 的底層運作中,當你點擊一個按鈕,React 會像這樣呼叫你的處理器:callback(event)。這是一個典型的「獨立呼叫」。

  • 如果是 寫法 Athis 會遺失變成 undefined。為了修復它,你必須手動綁定:onClick={this.handleClick.bind(this)}
  • 如果是 寫法 B,因為箭頭函數是定義在元件內部的,它會捕獲元件執行時的 this。這使得程式碼更乾淨,也避免了許多初學者難以排查的 bug。

Hooks 為什麼不需要 this

這正是 Hooks 偉大的地方。React 團隊發現管理 this 的複雜度(如 bind 的性能開銷、混淆的綁定規則)對開發者來說太痛苦了。

Hooks(如 useState)透過 Closure(閉包) 解決了狀態存取問題。在 Functional Component 中,每次渲染都是一個新的函數執行環境(EC),所有的變數(包括 state)都直接存在該作用域內。我們不再需要透過 this 去「尋找」資料,因為資料就在我們的詞法環境(Lexical Environment)中。

這也是為什麼我們說 「Closure 是 React 的靈魂,而 **this** 是 React 試圖擺脫的舊包袱」


總結 this 的判定流程

當你看到 this 時,可以按照以下優先順序進行判斷:

  1. 它是箭頭函數嗎?
  • 是:它的 this 等於定義時外層環境的 this
  1. 它是被 **new** 呼叫的嗎?
  • 是:this 指向那個新建立的物件。
  1. 它是被 **call** / **apply** / **bind** 強制綁定的嗎?
  • 是:this 指向指定的對象。
  1. 它是作為物件的方法被呼叫的嗎?(如 **obj.func()**
  • 是:this 指向該物件。
  1. 如果都不是:
  • 它是獨立呼叫,thisundefined(嚴格模式)或全域物件。

掌握了這個流程,你就擁有了在複雜 JavaScript 專案中導航的能力。

從物件導向到函數式思維

我們在 Topic 3 深入探討了原型繼承、物件組合、Mixin 以及 this 的綁定規則。這些知識點共同構成了 JavaScript 的物件系統版圖。雖然現代 React 開發傾向於函數式編程(Functional Programming),但底層物件系統的運作方式——尤其是記憶體節省的委派機制與詞法作用域的 this 捕獲——依然是理解 React 行為的基石。

理解 this 如何在回調函數中「迷失方向」,能讓你更深刻地體會到閉包(Closure)與箭頭函數在穩定資料流中的價值。這也正是我們課程的核心哲學:唯有看清底層的變動,才能寫出穩健的高層邏輯。

下一部分,我們將對整個 Topic 3 進行統整複習,確保你已經準備好將這些物件系統的知識轉化為實戰能力,並迎接下一個大主題:非同步 JavaScript 與 Event Loop。