課程:JavaScript 與 React 底層原理 第 6 堂:Topic 3 收尾與複習
17 This 與箭頭函數
想像一下,你正在寫一個計時器程式。你在物件中定義了一個方法來啟動倒數,一切看起來都很完美:你使用了 this.seconds 來存取資料。但當你把這段邏輯放入 setTimeout 回調函數後,程式突然崩潰了,報錯說 this 是 undefined 或者指向了全域物件。
為什麼同樣的程式碼,換個地方執行,它的「身份(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 指向誰,而不理會它是怎麼被呼叫的,你可以使用 call、apply 或 bind。
call與apply:立即執行函數,並將第一個參數綁定為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 會背後執行以下步驟:
- 建立一個全新的空物件。
- 將該函數的
**this**綁定到這個新物件上。 - 執行函數內容。
- 回傳該物件。
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 中,箭頭函數在定義的那一刻,就已經「看準」了外層 startArrow 的 this(即 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 汪汪叫"
因為箭頭函數無法透過 call 或 apply 改變其 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)。這是一個典型的「獨立呼叫」。
- 如果是 寫法 A,
this會遺失變成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 時,可以按照以下優先順序進行判斷:
- 它是箭頭函數嗎?
- 是:它的
this等於定義時外層環境的this。
- 它是被
**new**呼叫的嗎?
- 是:
this指向那個新建立的物件。
- 它是被
**call**/**apply**/**bind**強制綁定的嗎?
- 是:
this指向指定的對象。
- 它是作為物件的方法被呼叫的嗎?(如
**obj.func()**)
- 是:
this指向該物件。
- 如果都不是:
- 它是獨立呼叫,
this是undefined(嚴格模式)或全域物件。
掌握了這個流程,你就擁有了在複雜 JavaScript 專案中導航的能力。
從物件導向到函數式思維
我們在 Topic 3 深入探討了原型繼承、物件組合、Mixin 以及 this 的綁定規則。這些知識點共同構成了 JavaScript 的物件系統版圖。雖然現代 React 開發傾向於函數式編程(Functional Programming),但底層物件系統的運作方式——尤其是記憶體節省的委派機制與詞法作用域的 this 捕獲——依然是理解 React 行為的基石。
理解 this 如何在回調函數中「迷失方向」,能讓你更深刻地體會到閉包(Closure)與箭頭函數在穩定資料流中的價值。這也正是我們課程的核心哲學:唯有看清底層的變動,才能寫出穩健的高層邏輯。
下一部分,我們將對整個 Topic 3 進行統整複習,確保你已經準備好將這些物件系統的知識轉化為實戰能力,並迎接下一個大主題:非同步 JavaScript 與 Event Loop。