JavaScript中的箭頭函數

2022-11-09 06:00:42

前言

本文可以讓你瞭解所有有關JavaScript箭頭函數的資訊。我們將告訴你如何使用ES6的箭頭語法,以及在程式碼中使用箭頭函數時需要注意的一些常見錯誤。你會看到很多例子來說明它們是如何工作的。

JavaScript的箭頭函數隨著ECMAScript 2015的釋出而到來,也被稱為ES6。由於其簡潔的語法和對this關鍵字的處理,箭頭函數迅速成為開發者們最喜愛的功能。

箭頭函數語法

函數就像食譜一樣,你在其中儲存有用的指令,以完成你需要在程式中發生的事情,比如執行一個動作或返回一個值。通過呼叫函數,來執行食譜中包含的步驟。你可以在每次呼叫該函數時都這樣做,而不需要一次又一次地重寫菜譜。

下面是在JavaScript中宣告函數並呼叫它的標準方法:

// function declaration
function sayHiStranger() {
  return 'Hi, stranger!'
}

// call the function
sayHiStranger()

你也可以編寫同樣的函數作為函數表示式,就行這樣:

const sayHiStranger = function () {
  return 'Hi, stranger!'
}

JavaScript箭頭函數始終是表示式。下面是如何使用箭頭符號重寫上面的函數:

const sayHiStranger = () => 'Hi, stranger'

這樣做的好處包括:

  • 程式碼只有一行
  • 沒有function關鍵字
  • 沒有return關鍵字
  • 沒有大括號{}

在JavaScript中,函數是一等公民。你可以把函數儲存在變數中,把它們作為引數傳遞給其他函數,並從其他函數中把它們作為值返回。你可以使用JavaScript箭頭函數來做所有這些事情。

無圓括號語法

在上述範例中,函數是沒有引數的。在本例中,你必須在胖箭頭符號(=>)之前新增一對空的圓括號()。當有多個引數時同理:

const getNetflixSeries = (seriesName, releaseDate) => `The ${seriesName} series was released in ${releaseDate}`
// call the function
console.log(getNetflixSeries('Bridgerton', '2020') )
// output: The Bridgerton series was released in 2020

如果只有一個引數,你可以省略圓括號(你不必如此,但你可以這麼做):

const favoriteSeries = seriesName => seriesName === "Bridgerton" ? "Let's watch it" : "Let's go out"
// call the function
console.log(favoriteSeries("Bridgerton"))
// output: "Let's watch it"

當你這麼做的時候要小心一點。比如說,你決定使用預設引數,你必須將其包裹在圓括號中:

// with parentheses: correct
const bestNetflixSeries = (seriesName = "Bridgerton") => `${seriesName} is the best`
// outputs: "Bridgerton is the best"
console.log(bestNetflixSeries())

// no parentheses: error
const bestNetflixSeries = seriesName = "Bridgerton" => `${seriesName} is the best`
// Uncaught SyntaxError: invalid arrow-function arguments (parentheses around the arrow-function may help)

隱式返回

在函數體內只有一個表示式時,你可以讓ES6的箭頭語法更加簡潔。你可以把所有內容放在一行,去掉大括號,並移除return關鍵字。

你已經在上面的範例中看到了這些漂亮的一行程式碼是如何工作的。下面的orderByLikes()函數返回奈飛劇集物件的陣列,按照最高點贊數排序:

// using the JS sort() function to sort the titles in descending order 
// according to the number of likes (more likes at the top, fewer at the bottom
const orderByLikes = netflixSeries.sort((a, b) => b.likes - a.likes)

// call the function 
// output:the titles and the n. of likes in descending order
console.log(orderByLikes)

這種寫法很酷,但是要注意程式碼的可讀性。特別是在使用單行和無括號的ES6箭頭語法對一堆箭頭函數進行排序時。就像這個例子:

const greeter = greeting => name => `${greeting}, ${name}!`

那裡發生了什麼?嘗試使用常規的函數語法:

function greeter(greeting) {
  return function(name) {
    return `${greeting}, ${name}!` 
  }
}

現在,你可以快速看到外部函數greeter如何具有引數greeting,並返回一個匿名函數。這個內部函數又有一個叫做name的引數,並使用greetingname的值返回一個字串。下面是呼叫函數的方式:

const myGreet = greeter('Good morning')
console.log( myGreet('Mary') )   

// output: 
"Good morning, Mary!"

注意隱式返回錯誤

當你的JavaScript箭頭函數包含不止一個語句,你需要在大括號內包裹所有語句,並使用return關鍵字。

在下面的程式碼中,該函數建立了一個包含幾個Netflix劇集的標題和摘要的物件:

const seriesList = netflixSeries.map( series => {
  const container = {}
  container.title = series.name 
  container.summary = series.summary

  // explicit return
  return container
} )

.map()函數中的箭頭函數在一系列的語句中展開,在語句的最後返回一個物件。這使得在函數主體周圍使用大括號是不可避免的。

另外,由於正在使用花括號,隱式返回便不是一個選項。你必須顯式使用return關鍵字。

如果你的函數使用隱式返回來返回一個物件字面量,你需要使用圓括號來包裹該物件字面量。不這樣做將導致錯誤,因為JavaScript引擎將物件字面量的大括號錯誤地解析為函數的大括號。正如你剛才注意到的,當你在一個箭頭函數中使用大括號時,你不能省略return關鍵字。

前面程式碼的較短版本演示了這種語法:

// Uncaught SyntaxError: unexpected token: ':'
const seriesList = netflixSeries.map(series => { title: series.name });

// Works fine
const seriesList = netflixSeries.map(series => ({ title: series.name }));

無法命名箭頭函數

function關鍵字和參數列之間沒有名稱標識的函數被稱為匿名函數。下面是常規匿名函數表示式的樣子:

const anonymous = function() {
  return 'You can\'t identify me!' 
}

箭頭函數都是匿名函數:

const anonymousArrowFunc = () => 'You can\'t identify me!'

從ES6開始,變數和方法可以通過匿名函數的語法位置,使用name屬性來推斷其名稱。這使得在檢查函數值或報告錯誤時有可能識別該函數。

使用anonymousArrowFunc檢查一下:

console.log(anonymousArrowFunc.name)
// output: "anonymousArrowFunc"

需要注意的是,只有當匿名函數被分配給一個變數時,這個可以推斷的name屬性才會存在,正如上面的例子。如果你使用匿名函數作為回撥函數,你就會失去這個有用的功能。在下面的演示中,.setInterval()方法中的匿名函數無法利用name屬性:

let counter = 5
let countDown = setInterval(() => {
  console.log(counter)
  counter--
  if (counter === 0) {
    console.log("I have no name!!")
    clearInterval(countDown)
  }
}, 1000)

這還不是全部。這個推斷的name屬性仍然不能作為一個適當的識別符號,你可以用它來指代函數本身--比如遞迴、解除繫結事件等。

如何處理this關鍵字

關於箭頭函數,最重要的一點是它們處理this關鍵字的方式。特別是,箭頭函數內的this關鍵字不會重新系結。

為了說明這意味著什麼,請檢視下面的演示。

這裡有一個按鈕。點選按鈕會觸發一個從5到1的反向計數器,它顯示在按鈕本身。

<button class="start-btn">Start Counter</button>

...

const startBtn = document.querySelector(".start-btn");

startBtn.addEventListener('click', function() {
  this.classList.add('counting')
  let counter = 5;
  const timer = setInterval(() => {
    this.textContent = counter 
    counter -- 
    if(counter < 0) {
      this.textContent = 'THE END!'
      this.classList.remove('counting')
      clearInterval(timer)
    }
  }, 1000) 
})

注意到.addEventListener()方法裡面的事件處理器是一個常規的匿名函數表示式,而不是一個箭頭函數。為什麼呢?如果在函數內部列印this的值,你會看到它參照了監聽器所連線的按鈕元素,這正是我們所期望的,也是程式按計劃工作所需要的:

startBtn.addEventListener('click', function() {
  console.log(this)
  ...
})

下面是它在Firefox開發人員工具控制檯中的樣子:

然後,嘗試使用箭頭函數來替代常規函數,就像這樣:

startBtn.addEventListener('click', () => {
  console.log(this)
  ...
})

現在,this不再參照按鈕元素。相反,它參照Window物件:

這意味著,如果你想要在按鈕被點選之後,使用this來為按鈕新增class,你的程式碼就無法正常工作:

// change button's border's appearance
this.classList.add('counting')

下面是控制檯中的錯誤資訊:

當你在JavaScript中使用箭頭函數,this關鍵字的值不會被重新系結。它繼承自父作用域(也稱為詞法作用域)。在這種特殊情況下,箭頭函數被作為引數傳遞給startBtn.addEventListener()方法,該方法位於全域性作用域中。因此,函數處理器中的this也被繫結到全域性作用域中--也就是Window物件。

因此,如果你想讓this參照程式中的開始按鈕,正確的做法是使用一個常規函數,而不是一個箭頭函數。

匿名箭頭函數

在上面的演示中,接下來要注意的是.setInterval()方法中的程式碼。在這裡,你也會發現一個匿名函數,但這次是一個箭頭函數。為什麼?

請注意,如果你使用常規函數,this值會是多少:

const timer = setInterval(function() {
  console.log(this)
  ...
}, 1000)

button元素嗎?並不是。這個值將會是Window物件!

事實上,上下文已經發生了變化,因為現在this在一個非繫結的或全域性的函數中,它被作為引數傳遞給.setInterval() 。因此,this關鍵字的值也發生了變化,因為它現在被繫結到全域性作用域。

在這種情況下,一個常見的hack手段是包括另一個變數來儲存this關鍵字的值,這樣它就會一直指向預期的元素--在這種情況下,就是button元素:

const that = this
const timer = setInterval(function() {
  console.log(that)
  ...
}, 1000)

你也可以使用.bind()來解決這個問題:

const timer = setInterval(function() {
  console.log(this)
  ...
}.bind(this), 1000)

有了箭頭函數,問題就徹底消失了。下面是使用箭頭函數時this的值:

const timer = setInterval( () => { 
  console.log(this)
  ...
}, 1000)

這次,控制檯列印了button,這就是我們想要的。事實上,程式要改變按鈕的文字,所以它需要this來指代button元素:

const timer = setInterval( () => { 
  console.log(this)
 // the button's text displays the timer value
  this.textContent = counter
}, 1000)

箭頭函數沒有自己的this上下文。它們從父級繼承this的值,正是因為這個特點,在上面這種情況下就是很好的選擇。

不正常工作的情況

箭頭函數並不只是在JavaScript中編寫函數的一種花裡胡哨的新方法。它們有自己的侷限性,這意味著在有些情況下你不想使用箭頭函數。讓我們看看更多的例子。

箭頭函數作為物件方法

箭頭函數作為物件上的方法不能很好地工作。

考慮這個netflixSeries物件,上面有一些屬性和一系列方法。呼叫console.log(netflixSeries.getLikes()) 應該會列印一條資訊,說明當前喜歡的人數。console.log(netflixSeries.addLike())應該會增加一個喜歡的人數,然後在控制檯上列印新值:

const netflixSeries = {
  title: 'After Life', 
  firstRealease: 2019,
  likes: 5,
  getLikes: () => `${this.title} has ${this.likes} likes`,
  addLike: () => {  
    this.likes++
    return `Thank you for liking ${this.title}, which now has ${this.likes} likes`
  } 
}

相反,呼叫.getLikes()方法返回'undefined has NaN likes',呼叫.addLike()方法返回'Thank you for liking undefined, which now has NaN likes'。因此,this.titlethis.likes未能分別參照物件的屬性titlelikes

這次,問題出在箭頭函數的詞法作用域上。物件方法中的this參照的是父物件的範圍,在本例中是Window物件,而不是父物件本身--也就是說,不是netflixSeries物件。

當然,解決辦法是使用常規函數:

const netflixSeries = {
  title: 'After Life', 
  firstRealease: 2019,
  likes: 5,
  getLikes() {
    return `${this.title} has ${this.likes} likes`
  },
  addLike() { 
    this.likes++
    return `Thank you for liking ${this.title}, which now has ${this.likes} likes`
  } 
}

// call the methods 
console.log(netflixSeries.getLikes())
console.log(netflixSeries.addLike())

// output: 
After Life has 5 likes
Thank you for liking After Life, which now has 6 likes

箭頭函數與第三方庫

另一個需要注意的問題是,第三方庫通常會繫結方法呼叫,因此this值會指向一些有用的東西。

比如說,在Jquery事件處理器內部,this將使你能夠存取處理器所繫結的DOM元素:

$('body').on('click', function() {
  console.log(this)
})
// <body>

但是如果我們使用箭頭函數,正如我們所看到的,它沒有自己的this上下文,我們會得到意想不到的結果:

$('body').on('click', () =>{
  console.log(this)
})
// Window

下面是使用Vue的其他例子:

new Vue({
  el: app,
  data: {
    message: 'Hello, World!'
  },
  created: function() {
    console.log(this.message);
  }
})
// Hello, World!

created勾點內部,this被繫結到Vue範例上,因此會顯示'Hello, World!'資訊。

然而如果我們使用箭頭函數,this將會指向父作用域,上面沒有message屬性:

new Vue({
  el: app,
  data: {
    message: 'Hello, World!'
  },
  created: () => {
    console.log(this.message);
  }
})
// undefined

箭頭函數沒有arguments物件

有時,你可能需要建立一個具有無限引數個數的函數。比如,假設你想建立一個函數,列出你最喜歡的奈飛劇集,並按照偏好排序。然而,你還不知道你要包括多少個劇集。JavaScript提供了arguments物件。這是一個類陣列物件(不是完整的陣列),在呼叫時儲存傳遞給函數的值。

嘗試使用箭頭函數實現此功能:

const listYourFavNetflixSeries = () => {
  // we need to turn the arguments into a real array 
  // so we can use .map()
  const favSeries = Array.from(arguments) 
  return favSeries.map( (series, i) => {
    return `${series} is my #${i +1} favorite Netflix series`  
  } )
  console.log(arguments)
}

console.log(listYourFavNetflixSeries('Bridgerton', 'Ozark', 'After Life'))

當你呼叫該函數時,你會得到以下錯誤:Uncaught ReferenceError: arguments is not defined。這意味著arguments物件在箭頭函數中是不可用的。事實上,將箭頭函數替換成常規函數就可以了:

const listYourFavNetflixSeries = function() {
   const favSeries = Array.from(arguments) 
   return favSeries.map( (series, i) => {
     return `${series} is my #${i +1} favorite Netflix series`  
   } )
   console.log(arguments)
 }
console.log(listYourFavNetflixSeries('Bridgerton', 'Ozark', 'After Life'))

// output: 
["Bridgerton is my #1 favorite Netflix series",  "Ozark is my #2 favorite Netflix series",  "After Life is my #3 favorite Netflix series"]

因此,如果你需要arguments物件,你不能使用箭頭函數。

但如果你真的想用一個箭頭函數來複制同樣的功能呢?你可以使用ES6剩餘引數(...)。下面是你該如何重寫你的函數:

const listYourFavNetflixSeries = (...seriesList) => {
   return seriesList.map( (series, i) => {
     return `${series} is my #${i +1} favorite Netflix series`
   } )
 }

總結

通過使用箭頭函數,你可以編寫帶有隱式返回的單行程式碼,以解決JavaScript中this關鍵字的繫結問題。箭頭函數在陣列方法中也很好用,如.map().sort().forEach().filter()、和.reduce()。但請記住:箭頭函數並不能取代常規的JavaScript函數。記住,只有當箭形函數是正確的工具時,才能使用它。

以上就是本文的所有內容,如果對你有所幫助,歡迎點贊收藏轉發~