停止濫用箭頭函數:這五個場景請務必使用 function
自 ES6 問世以來,箭頭函數(Arrow Functions)以其簡潔的語法和對 this 的詞法綁定,迅速成為了 JavaScript 開發者的“新寵”。我們似乎傾向于在任何可以使用函數的地方都換上 () => {}。
然而,箭頭函數并非“銀彈”,它并不能完全替代傳統的 function 關鍵字。過度濫用箭頭函數,尤其是在不理解其工作原理的情況下,會導致難以追蹤的 bug 和意外行為。this 的指向是 JavaScript 中最核心也最容易混淆的概念之一,而箭頭函數和傳統 function 在 this 的處理上有著本質區別。
核心區別速記:
- function: this 的值是在函數被調用時動態決定的,取決于誰調用了它。
- => (箭頭函數): 它沒有自己的 this。它會捕獲其定義時所在上下文的 this 值,這個綁定是固定的,不會改變。
理解了這一點,我們就會明白為什么在以下 5 個場景中,堅持使用 function 不僅是最佳實踐,甚至是唯一的正確選擇。
場景一:對象的方法 (Object Methods)
這是最經典、最常見的場景。當我們為一個對象定義方法時,通常希望 this 指向該對象本身,以便訪問其屬性。
? 錯誤示范 (使用箭頭函數):
const person = {
name: '老王',
age: 30,
sayHi: () => {
// 這里的 this 繼承自全局作用域 (在瀏覽器中是 window),而不是 person 對象
console.log(`大家好,我是 ${this.name}`);
}
};
person.sayHi(); // 輸出: "大家好,我是 " (或者 "大家好,我是 undefined")
在這個例子中,箭頭函數 sayHi 在 person 對象中定義,但它的 this 捕獲的是定義 person 對象時的上下文,即全局作用域。全局作用域下沒有 name 屬性,所以結果不是我們想要的。
? 正確姿勢 (使用 function):
const person = {
name: '老王',
age: 30,
sayHi: function() {
// 這里的 this 在調用時被動態綁定為 person 對象
console.log(`大家好,我是 ${this.name}`);
},
// ES6 對象方法簡寫形式,本質上也是一個 function
sayHiShorthand() {
console.log(`大家好,我是 ${this.name}`);
}
};
person.sayHi(); // 輸出: "大家好,我是 老王"
person.sayHiShorthand(); // 輸出: "大家好,我是 老王"
結論: 當我們為對象定義一個需要引用該對象自身屬性的方法時,請使用 function 或 ES6 方法簡寫。
場景二:DOM 事件監聽器 (Event Listeners)
在使用 addEventListener 為 DOM 元素綁定事件時,我們常常需要訪問觸發該事件的元素本身(例如,修改它的樣式、內容等)。傳統 function 會自動將 this 綁定到該 DOM 元素上。
? 錯誤示范 (使用箭頭函數):
const button = document.getElementById('myButton');
button.addEventListener('click', () => {
// 這里的 this 依然是 window 或 undefined,而不是 button 元素
this.classList.toggle('active'); // TypeError: Cannot read properties of undefined (reading 'classList')
});
箭頭函數再次從外部作用域捕獲 this,導致我們無法直接操作點擊的按鈕。
? 正確姿勢 (使用 function):
const button = document.getElementById('myButton');
button.addEventListener('click', function() {
// 在這里,this 被正確地綁定為觸發事件的 button 元素
console.log(this); // <button id="myButton">...</button>
this.classList.toggle('active'); // 正常工作
});
結論: 在 DOM 事件監聽回調中,如果我們需要用 this 來引用觸發事件的元素,請使用 function。
場景三:構造函數 (Constructor Functions)
箭頭函數在設計上就不能作為構造函數使用。如果我們嘗試用 new 關鍵字來調用一個箭頭函數,JavaScript 會直接拋出錯誤。這是因為構造函數需要有自己的 this 來指向新創建的實例,并且需要一個 prototype 屬性,而箭頭函數兩者都不具備。
? 錯誤示范 (使用箭頭函數):
? 正確姿勢 (使用 function 或 class):
結論: 永遠不要用箭頭函數作為構造函數。請使用 function 或 class。
場景四:原型方法 (Prototype Methods)
與對象方法類似,當我們為構造函數的原型 prototype 添加方法時,我們也希望 this 指向調用該方法的實例。
? 錯誤示范 (使用箭頭函數):
? 正確姿舍 (使用 function):
結論: 在 prototype 上定義方法時,請使用 function,以確保 this 指向類的實例。
場景五:需要 arguments 對象的函數
箭頭函數沒有自己的 arguments 對象。arguments 是一個類數組對象,包含了函數被調用時傳入的所有參數。如果我們在箭頭函數內部訪問 arguments,它只會訪問到外層(如果存在)傳統函數的 arguments 對象。
? 錯誤示范 (使用箭頭函數):
? 正確姿勢 (使用 function):
注意: 在現代 JavaScript 中,更推薦使用剩余參數 (...args) 來處理不確定數量的參數。剩余參數是真正的數組,并且它在箭頭函數和傳統函數中都能正常工作。但如果我們需要維護舊代碼,或者有特殊理由需要使用 arguments 對象,那么 function 是我們唯一的選擇。
那么,什么時候應該用箭頭函數?
箭頭函數依然非常優秀和極為有用,它的主要優勢在于其詞法 this 綁定,完美解決了過去 var self = this 或 .bind(this) 的冗長寫法。
最佳使用場景:
回調函數:尤其是在 map, filter, forEach 等數組方法中,或者在 setTimeout, Promise.then 內部,當我們需要保持外部 this 上下文時。
const timer = {
seconds: 0,
start() {
setInterval(() => {
// 這里的 this 正確地指向 timer 對象,因為箭頭函數捕獲了 start 方法的 this
this.seconds++;
console.log(this.seconds);
}, 1000);
}
};
timer.start();