前言
年味兒漸散,收拾下心情,繼續敲程式碼吧。
對於即將到來金三銀四的求職季,相信不少同學都在默默地做著準備。本系列旨在梳理前端龐雜的知識點,並儘可能通俗易懂地表述出來,也希望能幫到有需要的同學。
這是前端面試題系列的第 5 篇,你可能錯過了前面的篇章,可以在這裡找到:
面試中,我經常會問及 ES6 的知識點,因為平時工作中用得很多。當問到箭頭函式時,不少候選人都會讚歎地說:箭頭函式很好用,而且再也不用操心 this 的指向了。
我接著問:箭頭函式是挺好用的,但是你有沒有遇到過,不適合使用箭頭函式的場景呢?
這時,能回答得上來的候選人就很少了。箭頭函式在大多數情況下,是很好用的,但是為什麼在有些場景,使用箭頭函式後會產生問題?是不是箭頭函式還不夠完善?又有哪些場景會發生問題?該如何解決呢?這,正是本文想要一起探討的。
箭頭函式的寫法
為什麼叫箭頭函式( Arrow Function )?因為它的寫法,看上去就是一個箭頭:
const multiply = num => num * num;
複製程式碼
它等價於:
const multiply = function (num) {
return num * num;
};
複製程式碼
此外,還可以傳多個引數,以及可變引數。
// 多引數
const multiply = (num1, num2) => num1 * num2;
// 可變引數
const sum = (num1, num2, ...rest) => {
let result = num1 + num2;
for (let i = 0; i < rest.length; i++) {
result += rest[i];
}
return result;
};
複製程式碼
當有多條語句時,需要配上 {...}
和 return
。
另外,如果返回的結果是物件,則需要配上 ()
,像下面這樣:
const func = val => ({ value: val });
複製程式碼
從上述的寫法來看,相較普通函式而言,箭頭函式的確簡便了很多,提升了我們程式碼的易用性。但它並非在任何場景下都適用,接下來,將會介紹幾種不適合箭頭函式的場景,並會提出可行的解決方案。
不適合的場景
1、物件的方法
看下面這個例子:
const obj = {
x: 1,
print: () => {
console.log(this === window); // => true
console.log(this.x); // undefined
}
};
obj.print();
複製程式碼
this.x 列印出來是 undefined。為什麼?然後,我在上面加了一行,發現 this 指向了 window。
解析:print 方法用了箭頭函式,其內部的 this 指向的還是上下文 window,上下文中並沒有定義 x,所以 this.x 輸出為 undefined。
解決辦法:用 ES6 的短語法,或者傳統的函式表示式都可以。所以,print 要這樣寫:
print () {
console.log(this === test); // => true
console.log(this.x); // 1
}
複製程式碼
2、原型方法
同樣的規則也適用於原型方法的定義,使用箭頭函式會導致執行時的執行上下文錯誤。
function Cat (name) {
this.name = name;
}
Cat.prototype.sayCatName = () => {
console.log(this === window); // => true
return this.name;
};
const cat = new Cat('Miao');
cat.sayCatName(); // => undefined
複製程式碼
解決辦法是:用回傳統的函式表示式,像下面這樣:
Cat.prototype.sayCatName = function () {
console.log(this === cat); // => true
return this.name;
};
複製程式碼
sayCatName 變回傳統的函式表示式之後,被呼叫時的執行上下文就會指向新建立的 cat 例項。
3、事件的回撥
看下面這個例子:
const btn = document.getElementById('myButton');
btn.addEventListener('click', () => {
console.log(this === window); // => true
this.innerHTML = 'Clicked button';
});
複製程式碼
這裡會有問題,因為 this 指向了 window。
解析:當為一個 DOM 事件繫結回撥函式後,觸發回撥函式時的 this,需要指向當前發生事件的 DOM 節點,也就是這裡的 btn。當回撥發生時,瀏覽器會用 btn 的上下文去呼叫處理函式。所以最後的 this.innerHTML 等價於 window.innerHTML,問題就在這裡。
解決辦法:用函式表示式代替箭頭函式。像這樣:
btn.addEventListener('click', function() {
console.log(this === btn); // => true
this.innerHTML = 'Clicked button';
});
複製程式碼
另外,在 react 中的事件回撥,也經常會遇到類似的問題。
// jsx render
<Button onClick={this.handleClickButton.bind(this)}>
...
</Button>
// callback
handleClickButton () {
...
}
複製程式碼
注意:這裡 onClick 的回撥函式,並非字串,而是一個實實在在的函式。可以將 onClick 理解為一箇中間變數,所以 react 在處理函式時的 this 指向就會丟失。
為了解決這個問題,我們需要為回撥函式繫結 this,使得事件處理函式無論如何傳遞,this 都指向我們例項化的那個物件。
在這裡,如果用箭頭函式,可以這樣改寫:
<Button onClick={ event => this.handleClickButton(event) }>
...
</Button>
複製程式碼
箭頭函式並沒有自己的 this,所以事件處理函式的呼叫者並不受影響。
4、建構函式
箭頭函式不能通過 new 關鍵字呼叫。
const Message = (text) => {
this.text = text;
};
var helloMessage = new Message('Hello World!');
// Uncaught TypeError: Message is not a constructor
複製程式碼
解析:從報錯資訊可以看出,箭頭函式沒有 constructor 方法,所以不能用作建構函式。 JavaScript 會通過丟擲異常的方式,進行隱式地預防。
解決方法:用函式表示式代替箭頭函式。
總結
回顧 MDN 給出的解釋:箭頭函式表示式的語法比函式表示式更短,並且沒有自己的this,arguments,super或 new.target。這些函式表示式更適用於那些本來需要匿名函式的地方,並且它們不能用作建構函式。
所以說,箭頭函式無疑是 ES6 帶來的重大改進,在正確的場合使用箭頭函式,能讓程式碼變得更加簡潔短小。但箭頭函式也不是萬能的,不能用的時候,千萬別硬往上套。比如,在需要動態上下文的場景中,使用箭頭函式需要格外地小心,這些場景包括:物件的方法、原型方法、事件的回撥、建構函式。並非一定要用箭頭函式,才能解決問題。
PS:歡迎關注我的公眾號 “超哥前端小棧”,交流更多的想法與技術。