[譯]JS箭頭函式三連問:為何用、怎麼用、何時用

kitety劉小生發表於2018-11-09

在現代JS中最讓人期待的特性就是關於箭頭函式,用=>來標識。箭頭函式有兩個主要的優點:其一是非常簡明的語法,另外就是直觀的作用域和this的繫結。

因為這些優點,箭頭函式比起其他形式的函式宣告更加受歡迎。比如,受歡迎的airbnb eslint configuration庫會強制使用JavaScript箭頭函式建立匿名函式。

然而,就像世間萬物一樣,箭頭函式有一些優點也有一些“缺點”,這就需要在使用的時候做一些權衡了。

學習如何權衡是使用好箭頭函式的關鍵。在這篇文章中我們將回顧箭頭函式是怎樣工作的,然後深入探討,實際程式碼中箭頭函式是如何改進我們程式碼的,以及一些箭頭函式不推薦的情況。

什麼才是箭頭函式

JS的箭頭函式大概就像python中的lambda(python定義匿名函式的關鍵字)和ruby中的blocks(類似於閉包)一樣。 這些匿名函式都有他們特殊的語法:首先接收一定數目的引數,然後在定義它們的函式的作用域或就近作用域中執行。

接下來我們將詳細探討這些。

箭頭函式的語法

箭頭函式有一個大體的結構,同時也有很多的特殊情況可以簡化。 核心的結構如下:

(argument1, argument2, ... argumentN) => {
    // function body
}
複製程式碼

在括號裡面有一系列的引數,接著跟著一個箭頭符號=>,最後是函式體。這跟傳統的函式很相像,只是我們省略了function關鍵字,並且新增了一個=>在引數後面。

並且,這裡也有很多種情況,讓箭頭函式結構變得更加的簡潔。

首先,如果函式體裡面是一個單獨的表示式,你可以省略大括號直接將表示式寫在一行,並且表示式的結果也將會被函式直接返回。比如:

const add = (a, b) => a + b;
複製程式碼

其次,如果這傳入的是一個單獨的引數,你也可省略引數部分的括號。比如:

const getFirst = array => array[0];
複製程式碼

如你所見,這樣就看起來更加的簡潔了,我們也將在後面說明更多的特性。

高階語法

如果你瞭解這些高階語法之後將十分受用。

首先,如果你嘗試在一行書寫函式,但是返回的值卻是一個物件內容,你原想這樣寫:

(name, description) => {name: name, description: description};
複製程式碼

而問題就是這樣的語法會引起歧義,會誤以為你在寫一個函式的函式體。 如果想返回的是單個的物件,請用括號包裝該物件:

(name, description) => ({name: name, description: description});
複製程式碼

封閉的上下文作用域

不像其他形式的函式,箭頭函式並沒有他們自己的執行上下文。實際上,這就意味著程式碼中的thisarguments都是繼承自他們的父函式。

比如,比較下面箭頭函式和傳統函式的區別:

const test = {
  name: 'test object',
  createAnonFunction: function() {
    return function() {
      console.log(this.name);
      console.log(arguments);
    };
  },
  createArrowFunction: function() {
    return () => {
      console.log(this.name);
      console.log(arguments);
    };
  }
};
複製程式碼

我們有一個有兩個方法的物件,每個方法都返回了一個匿名函式。區別在於第一個方法裡面用了傳統的函式表示式,後面的用了箭頭函式表示式。如果我們在傳入同樣的引數執行,我們得到了兩個不同的結果。

const anon = test.createAnonFunction('hello', 'world');
//返回匿名函式
const arrow = test.createArrowFunction('hello', 'world');
anon();
//undefined
//{}
// this->window
arrow();
//test object
//object { '0': 'hello', '1': 'world' }
//this->test
複製程式碼

第一個匿名函式有自己的上下文(指向並非test物件),當你呼叫的時候沒有參考的this.name的屬性,(注意:現在this指向window),也沒有建立它時呼叫的引數。另一個,箭頭函式與建立它的函式有相同的上下文,讓其可以訪問引數arguments和物件。

箭頭函式改進您的程式碼

傳統lambda函式的主要用例之一,就是將函式用於陣列的遍歷,現在用JavaScript箭頭函式實現。 比如你有一個有值的陣列,你想去map遍歷每一項,這時箭頭函式是非常推薦的:

const words = ['hello', 'WORLD', 'Whatever'];
const downcasedWords = words.map(word => word.toLowerCase());
複製程式碼

一個及其常見的例子就是返回一個物件的某個值:

const names = objects.map(object => object.name);
複製程式碼

類似的,當用forEach來替換傳統for迴圈的時候,實際上箭頭函式會直觀的保持this來自於父一級

this.examples.forEach(example => {
  this.runExample(example);
});
複製程式碼

Promise和Promise鏈

當在編寫非同步程式的時候,箭頭函式也會讓程式碼更加直觀和簡潔。

Promise可以更簡單的編寫非同步程式。雖然你樂意去使用async/await,你也需要好好理解promise,因為這是他們的基礎。

使用promise,仍然需要定義你的程式碼執行完成之後的回撥函式。 這是箭頭函式的理想位置,特別是如果您生成的函式是有狀態的,同時想引用物件中的某些內容。

this.doSomethingAsync().then((result) => {
  this.storeResult(result);
});
複製程式碼

物件轉換

箭頭函式的另一個常見而且十分有用的地方就是用於封裝的物件轉換。 例如在Vue.js中,有一種通用模式,就是使用mapState將Vuex儲存的各個部分,直接包含到Vue元件中。 這涉及到定義一套mappers,用於從原物件到完整的轉換輸出,這在元件問題中實十分有必要的。 這一系列簡單的轉換,使用箭頭函式是最合適不過的。比如:

export default {
  computed: {
    ...mapState({
      results: state => state.results,
      users: state => state.users,
    });
  }
}
複製程式碼

你不應該使用箭頭函式的情景

這裡有許多箭頭函式不推薦的場景,這種情況之下不僅沒有幫助,而且還會造成不必要的麻煩。

首先就是物件中的方法。這裡有一個函式上下文的例子,對於我們理解很有幫助。 曾經流行一種趨勢,用class類的語法和箭頭函式,為其自動繫結方法。比如:事件方法可以使用,但是仍然繫結在class類中。 看起來就像下面的例子:

class Counter {
  counter = 0;
  handleClick = () => {
    this.counter++;
  }
}
複製程式碼

在這種方法中,如果被一個點選事件函式呼叫了,它雖然不是Counter的上下文中,它仍舊可以訪問例項的資料,這種方式的缺點不言而喻。

用這種方式的確提供了一種繫結函式的快捷方式,但是函式的表達形式多種多樣,相當不直觀。如果你嘗試在原型使用這種物件,這將不利於測試,同時也會產生很多問題。 相反,推薦用一種常規的繫結方式,如有必要可以繫結在例項的建構函式中:

class Counter {
  counter = 0;
  handleClick() {
    this.counter++;
  }
  constructor() {
    this.handleClick = this.handleClick.bind(this);
  }
}
複製程式碼

深層呼叫

另一種使用箭頭函式會讓你頭疼的地方,就是你去用很多函式的組合呼叫,尤其是函式的深層呼叫。 簡單的理由跟匿名函式一樣,堆疊的追蹤很複雜。

如果你的函式僅僅在一層之下,而不是深層的迭代,這倒不是什麼問題。但是如果你將函式定義為箭頭函式,並且在他們之間來回撥用,當你除錯bug的時候你將被程式碼困惑,甚至得到如下的錯誤資訊:

{anonymous}()
{anonymous}()
{anonymous}()
{anonymous}()
{anonymous}()
//anonymous 匿名
複製程式碼

有動態上下文的函式

還有最有一種箭頭函式會讓你困惑的情形,就是this是動態繫結的時候。 如果你在以下情形使用箭頭函式,那麼this的動態繫結不會如期工作,並且你也會困惑這些程式碼為什麼不像預期那樣工作,也會給你之後工作的人造成麻煩。 一些典型的例子:

  • 事件的呼叫函式,this指向當前的目標屬性
  • 在jquery中,大多數時候this指向的是當前被選擇的元素
  • 在vue中,methodscomputed中的this指向的是vue的元件。

當然你也可以在上面的情形之下謹慎的使用箭頭函式。但特別是在jqueryvue的情況下, 這通常會干擾正常功能, 並使您感到困惑:為什麼看起來跟別人程式碼一樣的程式碼就是不工作。

總結

箭頭函式是JS語言中十分特別的屬性,並且使很多情形中程式碼更加的變化莫測。儘管如此,就像其他的語言特性,他們有各自的優缺點。因此我們使用它應該僅僅是作為一種工具,而不是無腦的簡單的全部替換為箭頭函式。

本文只是個人興趣翻譯,如有錯誤之處還望各位斧正。文章版權屬於原文作者。

原文地址:codeburst.io/javascript-…

相關文章