技術成長 · Web 開發 · JavaScript
JavaScript Arrow Function 是什麼?和一般 function 到底差在哪?
上一篇我們談到 JavaScript 的 this。
當時有提到一個很重要的差異:
一般函式的this,通常要看函式怎麼被呼叫;Arrow Function 則沒有自己的this,而是沿用外層環境原本的this。
而且在上一篇介紹 bind()、Partial Application 和 Currying 時,其實我們已經偷跑看過一次 Arrow Function:
const calculatePriceCurry = (discount) => (price) => price * discount;當時為了不要一次塞太多新東西,我們直接把它拆成一般函式來理解:
function calculatePriceCurry(discount) {
return function (price) {
return price * discount;
};
}但看到這裡,可能還是會有一個問題。
Arrow Function 到底是什麼?
它只是把:
function換成:
=>讓程式碼變短而已嗎?
不只是如此。
Arrow Function 除了語法比較精簡以外,在 this、arguments、new 等行為上,也和一般 function 有明顯差異。
所以這一篇就從最基本的 Arrow Function 語法開始,再慢慢比較它和一般 function 到底差在哪裡。
---
什麼是 Arrow Function?
Arrow Function 通常翻譯成「箭頭函式」。
它是 JavaScript ES6 加入的一種函式寫法。
先看一般函式:
function add(a, b) {
return a + b;
}如果改成 Arrow Function,可以寫成:
const add = (a, b) => {
return a + b;
};這裡最明顯的差別,就是:
function變成了:
=>所以才會被叫做 Arrow Function。
因為:
=>看起來就像一個箭頭,沒錯吧,合理吧。
---
Tips:=> 到底代表什麼?
可以先把:
(a, b) => {
return a + b;
}白話理解成:
接收a和b兩個參數,然後執行後面的程式。
例如:
const add = (a, b) => {
return a + b;
};可以理解成:
建立一個函式,收到a和b之後,把兩個數字相加並回傳結果,最後交給add保存。
所以:
add(10, 20);最後就會得到:
30---
Arrow Function 可以寫得更短
Arrow Function 最大的特色之一,就是很多情況可以把程式碼寫得比較精簡。
例如:
const add = (a, b) => {
return a + b;
};如果函式裡面只有一個運算式,而且要直接把結果回傳出去,可以省略 {} 和 return:
const add = (a, b) => a + b;兩種寫法的結果一樣:
console.log(add(10, 20)); // 30---
Tips:什麼是 Implicit Return?
這種沒有寫:
return但 Arrow Function 仍然會自動回傳結果的寫法,通常會稱為:
Implicit Return
中文可以理解成:
隱式回傳。
例如:
const double = number => number * 2;其實就相當於:
const double = number => {
return number * 2;
};白話來說:
因為函式只有一件事情要做,所以不用特別寫 return,JavaScript 會直接把運算結果回傳出去。---
只有一個參數時,可以省略括號
例如:
const double = (number) => {
return number * 2;
};只有一個參數時,可以寫成:
const double = number => {
return number * 2;
};再搭配剛剛的 Implicit Return,可以繼續縮短:
const double = number => number * 2;三種寫法結果都一樣:
console.log(double(5)); // 10不過如果完全沒有參數,括號就不能省略:
const sayHello = () => {
console.log("Hello");
};如果有兩個以上的參數,也需要括號:
const add = (a, b) => a + b;可以先簡單整理成:
// 沒有參數
() => {}
// 一個參數
value => {}
// 多個參數
(a, b) => {}---
直接回傳物件時,要多包一層括號
這裡有一個 Arrow Function 很常見的小陷阱。
假設我們想直接回傳一個物件:
const createUser = () => {
return {
name: "Dennis"
};
};如果要使用 Implicit Return 簡寫,不能直接寫成:
const createUser = () => {
name: "Dennis"
};因為 JavaScript 會把 {} 當成函式本體,而不是要回傳的物件。
正確寫法是:
const createUser = () => ({
name: "Dennis"
});也就是在物件外面再包一層:
()這是在告訴 JavaScript:
這個 {} 是我要回傳的物件,不是函式本體。---
Tips:為什麼要加 ()?
Arrow Function 看到:
=> {時,會把後面的 {} 當成函式區塊。
所以:
() => ({
name: "Dennis"
})外層的 () 可以讓:
{
name: "Dennis"
}被當成一個要直接回傳的物件。
---
回頭看看上一篇的 Currying
現在已經知道 Arrow Function 可以使用 Implicit Return,就可以回頭看看上一篇出現過的這一行:
const calculatePriceCurry = (discount) => (price) => price * discount;上一篇我們直接把它拆成:
function calculatePriceCurry(discount) {
return function (price) {
return price * discount;
};
}現在改用 Arrow Function 一層一層拆,就會變成:
const calculatePriceCurry = (discount) => {
return (price) => {
return price * discount;
};
};先看外面這層:
(discount) => {它接收一個 discount。
接著:
return (price) => {
return price * discount;
};回傳另一個 Arrow Function。
所以我們可以寫:
const memberPrice = calculatePriceCurry(0.9);這時候 memberPrice 接到的是:
(price) => {
return price * discount;
}接著再執行:
memberPrice(1000);就會得到:
900而因為兩層函式都只有一個要直接回傳的運算式,所以才可以一路縮成:
const calculatePriceCurry = (discount) => (price) => price * discount;所以這一行並不是什麼特殊的 Arrow Function 魔法。
只是把:
function calculatePriceCurry(discount) {
return function (price) {
return price * discount;
};
}利用 Arrow Function 和 Implicit Return 濃縮起來而已。
Tips:這裡其實又看到 Closure 了
內層的:
(price) => price * discount仍然可以取得外層的:
discount這就是前面介紹過的 Closure。
所以這個例子其實一次串起了三個前面學過的概念:
Closure、Currying、Arrow Function。
---
Arrow Function 真的只是比較短嗎?
到目前為止,看起來 Arrow Function 好像只是:
一般函式的簡寫版本。
但真正重要的地方從這裡才開始。
Arrow Function 和一般 function 不只是寫法不同。
它們在行為上也有差異。
其中最常被問到的就是:
this---
第一個差異:this
上一篇已經談過,一般函式中的 this 通常要看函式是怎麼被呼叫的。
例如:
const user = {
name: "Dennis",
showName() {
console.log(this.name);
}
};
user.showName(); // Dennis呼叫方式是:
user.showName();所以這次 showName() 裡面的 this 指向 user。
但是 Arrow Function 不一樣。
Arrow Function 沒有自己的 this。它會沿用外層環境原本的 this。
這部分上一篇已經介紹過,所以這裡主要把它放進 Arrow Function 和一般 function 的比較裡。
---
Arrow Function 放在物件方法裡會怎樣?
例如:
const user = {
name: "Dennis",
showName: () => {
console.log(this.name);
}
};
user.showName();很多人第一次看到會認為:
user.showName();點號前面是 user,所以 this 應該指向 user。
但這裡的 showName 是 Arrow Function。
Arrow Function 不會因為:
user.showName();這種呼叫方式,就產生自己的 this。
它會沿用外層環境原本的 this。
所以這裡通常無法取得:
Dennis---
Callback 裡面反而很適合使用 Arrow Function
Arrow Function 沒有自己的 this,有時候反而非常方便。
例如:
const user = {
name: "Dennis",
showNameLater() {
setTimeout(() => {
console.log(this.name);
}, 1000);
}
};
user.showNameLater();先看這一段:
user.showNameLater();showNameLater() 是一般方法,因此這次執行時:
this指向 user。
接著裡面的 Arrow Function:
() => {
console.log(this.name);
}沒有自己的 this。
所以它會沿用外層 showNameLater() 執行時的 this。
最後就能取得:
user.name一秒後輸出:
Dennis這也是 Arrow Function 很常出現在 Callback 裡的原因之一。
---
Tips:這和 Closure 一樣嗎?
有點像,但不要混在一起。
Closure 處理的是:
函式能不能繼續存取建立位置的外部變數。
Arrow Function 的 this 則是:
它自己不建立this,所以直接沿用外層環境原本的this。
兩者都和外層環境有關,但不是同一件事情。
---
第二個差異:arguments
一般 function 裡有一個特殊的:
arguments例如:
function showArguments() {
console.log(arguments);
}
showArguments("Dennis", 20, "巴黎");雖然函式宣告時完全沒有寫參數:
function showArguments() {但是 arguments 還是可以取得呼叫時傳進來的參數。
---
Tips:arguments 是什麼?
arguments 是一般函式裡的一個特殊物件。
它會保存這次呼叫函式時傳入的參數。
例如:
function showArguments() {
console.log(arguments[0]);
console.log(arguments[1]);
}
showArguments("Dennis", 20);會輸出:
Dennis
20可以先把它理解成:
JavaScript 幫一般函式準備的一個「這次到底收到哪些參數」的紀錄。
不過 arguments 看起來很像陣列,但它不是一般的 Array。
---
Arrow Function 沒有自己的 arguments
如果改成:
const showArguments = () => {
console.log(arguments);
};Arrow Function 不會建立自己的 arguments。
所以不能直接把一般函式使用 arguments 的方式照搬過來。
如果真的需要接收不固定數量的參數,可以使用:
...args例如:
const showArguments = (...args) => {
console.log(args);
};
showArguments("Dennis", 20, "桃園");這時 args 會是一個真正的陣列:
["Dennis", 20, "桃園"]---
Tips:...args 是什麼?
這裡的:
...args稱為 Rest Parameter(剩餘參數)。
白話來說:
不管後面還傳多少參數,都先幫我收進 args 這個陣列裡。例如:
const showData = (...args) => {
console.log(args);
};
showData(1, 2, 3, 4, 5);會得到:
[1, 2, 3, 4, 5]所以現代 JavaScript 中,如果只是想接收不固定數量的參數,很多時候會直接使用 Rest Parameter。
---
第三個差異:Arrow Function 不能搭配 new
一般函式可以作為建構函式:
function User(name) {
this.name = name;
}
const dennis = new User("Dennis");
console.log(dennis.name); // Dennis上一篇有提到,當我們使用:
new User("Dennis")JavaScript 會建立一個新的物件,並讓 User() 裡面的 this 指向這個新物件。
但 Arrow Function 不行。
例如:
const User = (name) => {
this.name = name;
};
const dennis = new User("Dennis");會發生:
TypeError因為 Arrow Function 不能當作建構函式使用。
---
Tips:為什麼 Arrow Function 不能搭配 new?
Arrow Function 本身沒有建構函式需要的內部建構能力。
因此不能使用:
new User()來呼叫 Arrow Function。
另外,Arrow Function 也沒有自己的 this。
所以如果函式預計要搭配:
new建立物件,就應該使用一般 function 或 class。
---
第四個差異:Arrow Function 沒有自己的 prototype
一般函式建立後,通常會有自己的:
prototype例如:
function User() {}
console.log(User.prototype);但 Arrow Function:
const User = () => {};沒有一般函式那樣的 prototype 屬性:
console.log(User.prototype); // undefined這也和前面提到它不能作為建構函式搭配 new 使用有關。
---
Tips:Prototype 是什麼?
Prototype 中文通常翻譯成「原型」。
這是一個比較大的 JavaScript 主題。
目前可以先簡單理解:
JavaScript 物件之間可以透過 Prototype 建立繼承與共享功能的關係。
這一篇先知道 Arrow Function 沒有一般建構函式使用的 prototype 就可以。
之後談到 Prototype 時再完整拆解。
---
一般 function 和 Arrow Function 都可以當 Callback
看到這裡不要誤會成:
Callback 就一定要使用 Arrow Function。
不是。
兩種都可以。
例如:
const numbers = [1, 2, 3];
const doubled = numbers.map(function (number) {
return number * 2;
});
console.log(doubled);也可以寫成:
const numbers = [1, 2, 3];
const doubled = numbers.map(number => number * 2);
console.log(doubled);兩個結果都是:
[2, 4, 6]只是在這種簡單的 Callback 中,Arrow Function 通常比較簡潔。
所以很常看到:
numbers.map(number => number * 2);而不是:
numbers.map(function (number) {
return number * 2;
});---
什麼時候適合使用 Arrow Function?
Arrow Function 很適合:
- 簡單的 Callback
map()、filter()、reduce()這類陣列操作- 不需要自己
this的函式 - 希望沿用外層
this的 Callback - 很短、單純做資料轉換的函式
例如:
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(number => number % 2 === 0);
console.log(evenNumbers);
// [2, 4]這種情況 Arrow Function 就非常適合。
因為函式的功能很單純:
收到一個 number,判斷是不是偶數。---
什麼時候比較適合一般 function?
如果函式:
- 需要根據呼叫方式取得自己的
this - 要當物件方法並使用
this - 要搭配
new - 要當建構函式
- 需要自己的
arguments
通常就比較適合使用一般 function。
例如物件方法:
const user = {
name: "Dennis",
showName() {
console.log(this.name);
}
};因為這裡需要:
this根據:
user.showName();這次呼叫指向 user。
所以使用一般方法會比較合適。
---
Arrow Function 和一般 function 比較
最後整理成一張表:
| 特性 | 一般 function | Arrow Function |
|---|---|---|
| 基本語法 | 較完整 | 較精簡 |
| Implicit Return | 不行 | 可以 |
自己的 this | 有 | 沒有 |
this 判斷 | 通常看呼叫方式 | 沿用外層 this |
自己的 arguments | 有 | 沒有 |
...args | 可以 | 可以 |
可以搭配 new | 可以 | 不可以 |
| 可當建構函式 | 可以 | 不可以 |
一般函式的 prototype 屬性 | 有 | 沒有 |
| 適合簡短 Callback | 可以 | 很常見 |
適合需要動態 this 的物件方法 | 適合 | 通常不太適合 |
---
常見誤解
誤解一:Arrow Function 只是一般 function 的簡寫
不完全是。
它確實可以讓程式碼變短,例如:
const double = number => number * 2;但它和一般 function 在:
this
arguments
new
prototype等行為上都有差異。
所以不能單純把它理解成:
少寫幾個字的 function。
---
誤解二:Arrow Function 比較新,所以任何地方都應該用它
不是。
Arrow Function 和一般 function 沒有誰一定比較好。
重點是:
這個函式需要什麼行為?
如果你需要自己的 this:
const user = {
name: "Dennis",
showName() {
console.log(this.name);
}
};一般方法比較適合。
如果只是做一個簡單 Callback:
numbers.map(number => number * 2);Arrow Function 通常更方便。
---
誤解三:Arrow Function 寫在物件裡,this 就會是那個物件
不是。
例如:
const user = {
name: "Dennis",
showName: () => {
console.log(this.name);
}
};Arrow Function 不會因為它被寫在 user 裡面,就自動取得 user 作為 this。
因為物件本身不會替 Arrow Function 建立一個新的 this。
---
誤解四:Arrow Function 不能當 Callback
當然可以。
而且它其實非常常被拿來當 Callback。
例如:
setTimeout(() => {
console.log("時間到了");
}, 1000);或:
numbers.map(number => number * 2);真正需要注意的不是「能不能當 Callback」,而是:
這個 Callback 是否需要自己的 this。---
簡單整理
Arrow Function 最基本的寫法是:
const add = (a, b) => {
return a + b;
};如果只有一個運算式,可以簡寫成:
const add = (a, b) => a + b;但 Arrow Function 不只是比較短。
它和一般 function 的重要差異包括:
- Arrow Function 沒有自己的
this。 - Arrow Function 會沿用外層環境的
this。 - Arrow Function 沒有自己的
arguments。 - 可以使用 Rest Parameter
...args接收不固定參數。 - Arrow Function 不能搭配
new。 - Arrow Function 不能作為一般建構函式。
- Arrow Function 沒有一般函式的
prototype屬性。 - Arrow Function 很適合簡單 Callback 與資料轉換。
- 需要自己的
this時,通常比較適合一般 function。
最後濃縮成一句話:
Arrow Function 不只是把function換成=>,而是一種在this、arguments、new等行為上都和一般 function 不同的函式。
所以下次看到:
function () {}和:
() => {}不要只把它們想成:
長版和短版。
更重要的是先問:
這個函式需不需要自己的this?需不需要arguments?會不會拿來搭配new?
搞清楚這幾件事之後,就比較知道什麼時候該用哪一種函式。