夜行手記

技術成長 · Web 開發 · JavaScript

JavaScript Arrow Function 是什麼?和一般 function 到底差在哪?

Arrow Function(箭頭函式)不只是把 function 換成 => 的簡寫方式,它和一般 function 在行為上也有不少差異。本篇會從 Arrow Function 的基本語法、Implicit Return、參數簡寫開始,接著比較它和一般 function 在 this、arguments、new、prototype 等方面的不同,也會回頭拆解上一篇出現過的 Currying 範例,幫助理解 Arrow Function 到底適合用在哪些情境。
9 分鐘閱讀

上一篇我們談到 JavaScript 的 this

當時有提到一個很重要的差異:

一般函式的 this,通常要看函式怎麼被呼叫;Arrow Function 則沒有自己的 this,而是沿用外層環境原本的 this

而且在上一篇介紹 bind()、Partial Application 和 Currying 時,其實我們已經偷跑看過一次 Arrow Function:

JavaScript
const calculatePriceCurry = (discount) => (price) => price * discount;

當時為了不要一次塞太多新東西,我們直接把它拆成一般函式來理解:

JavaScript
function calculatePriceCurry(discount) {
    return function (price) {
        return price * discount;
    };
}

但看到這裡,可能還是會有一個問題。

Arrow Function 到底是什麼?

它只是把:

JavaScript
function

換成:

JavaScript
=>

讓程式碼變短而已嗎?

不只是如此。

Arrow Function 除了語法比較精簡以外,在 thisargumentsnew 等行為上,也和一般 function 有明顯差異。

所以這一篇就從最基本的 Arrow Function 語法開始,再慢慢比較它和一般 function 到底差在哪裡。

---

什麼是 Arrow Function?

Arrow Function 通常翻譯成「箭頭函式」。

它是 JavaScript ES6 加入的一種函式寫法。

先看一般函式:

JavaScript
function add(a, b) {
    return a + b;
}

如果改成 Arrow Function,可以寫成:

JavaScript
const add = (a, b) => {
    return a + b;
};

這裡最明顯的差別,就是:

JavaScript
function

變成了:

JavaScript
=>

所以才會被叫做 Arrow Function。

因為:

text
=>

看起來就像一個箭頭,沒錯吧,合理吧。

---

Tips:=> 到底代表什麼?

可以先把:

JavaScript
(a, b) => {
    return a + b;
}

白話理解成:

接收 ab 兩個參數,然後執行後面的程式。

例如:

JavaScript
const add = (a, b) => {
    return a + b;
};

可以理解成:

建立一個函式,收到 ab 之後,把兩個數字相加並回傳結果,最後交給 add 保存。

所以:

JavaScript
add(10, 20);

最後就會得到:

text
30

---

Arrow Function 可以寫得更短

Arrow Function 最大的特色之一,就是很多情況可以把程式碼寫得比較精簡。

例如:

JavaScript
const add = (a, b) => {
    return a + b;
};

如果函式裡面只有一個運算式,而且要直接把結果回傳出去,可以省略 {}return

JavaScript
const add = (a, b) => a + b;

兩種寫法的結果一樣:

JavaScript
console.log(add(10, 20)); // 30

---

Tips:什麼是 Implicit Return?

這種沒有寫:

JavaScript
return

但 Arrow Function 仍然會自動回傳結果的寫法,通常會稱為:

Implicit Return

中文可以理解成:

隱式回傳。

例如:

JavaScript
const double = number => number * 2;

其實就相當於:

JavaScript
const double = number => {
    return number * 2;
};

白話來說:

因為函式只有一件事情要做,所以不用特別寫 return,JavaScript 會直接把運算結果回傳出去。

---

只有一個參數時,可以省略括號

例如:

JavaScript
const double = (number) => {
    return number * 2;
};

只有一個參數時,可以寫成:

JavaScript
const double = number => {
    return number * 2;
};

再搭配剛剛的 Implicit Return,可以繼續縮短:

JavaScript
const double = number => number * 2;

三種寫法結果都一樣:

JavaScript
console.log(double(5)); // 10

不過如果完全沒有參數,括號就不能省略:

JavaScript
const sayHello = () => {
    console.log("Hello");
};

如果有兩個以上的參數,也需要括號:

JavaScript
const add = (a, b) => a + b;

可以先簡單整理成:

JavaScript
// 沒有參數
() => {}

// 一個參數
value => {}

// 多個參數
(a, b) => {}

---

直接回傳物件時,要多包一層括號

這裡有一個 Arrow Function 很常見的小陷阱。

假設我們想直接回傳一個物件:

JavaScript
const createUser = () => {
    return {
        name: "Dennis"
    };
};

如果要使用 Implicit Return 簡寫,不能直接寫成:

JavaScript
const createUser = () => {
    name: "Dennis"
};

因為 JavaScript 會把 {} 當成函式本體,而不是要回傳的物件。

正確寫法是:

JavaScript
const createUser = () => ({
    name: "Dennis"
});

也就是在物件外面再包一層:

JavaScript
()

這是在告訴 JavaScript:

這個 {} 是我要回傳的物件,不是函式本體。

---

Tips:為什麼要加 ()

Arrow Function 看到:

JavaScript
=> {

時,會把後面的 {} 當成函式區塊。

所以:

JavaScript
() => ({
    name: "Dennis"
})

外層的 () 可以讓:

JavaScript
{
    name: "Dennis"
}

被當成一個要直接回傳的物件。

---

回頭看看上一篇的 Currying

現在已經知道 Arrow Function 可以使用 Implicit Return,就可以回頭看看上一篇出現過的這一行:

JavaScript
const calculatePriceCurry = (discount) => (price) => price * discount;

上一篇我們直接把它拆成:

JavaScript
function calculatePriceCurry(discount) {
    return function (price) {
        return price * discount;
    };
}

現在改用 Arrow Function 一層一層拆,就會變成:

JavaScript
const calculatePriceCurry = (discount) => {
    return (price) => {
        return price * discount;
    };
};

先看外面這層:

JavaScript
(discount) => {

它接收一個 discount

接著:

JavaScript
return (price) => {
    return price * discount;
};

回傳另一個 Arrow Function。

所以我們可以寫:

JavaScript
const memberPrice = calculatePriceCurry(0.9);

這時候 memberPrice 接到的是:

JavaScript
(price) => {
    return price * discount;
}

接著再執行:

JavaScript
memberPrice(1000);

就會得到:

text
900

而因為兩層函式都只有一個要直接回傳的運算式,所以才可以一路縮成:

JavaScript
const calculatePriceCurry = (discount) => (price) => price * discount;

所以這一行並不是什麼特殊的 Arrow Function 魔法。

只是把:

JavaScript
function calculatePriceCurry(discount) {
    return function (price) {
        return price * discount;
    };
}

利用 Arrow Function 和 Implicit Return 濃縮起來而已。

Tips:這裡其實又看到 Closure 了

內層的:

JavaScript
(price) => price * discount

仍然可以取得外層的:

JavaScript
discount

這就是前面介紹過的 Closure。

所以這個例子其實一次串起了三個前面學過的概念:

Closure、Currying、Arrow Function。

---

Arrow Function 真的只是比較短嗎?

到目前為止,看起來 Arrow Function 好像只是:

一般函式的簡寫版本。

但真正重要的地方從這裡才開始。

Arrow Function 和一般 function 不只是寫法不同。

它們在行為上也有差異。

其中最常被問到的就是:

JavaScript
this

---

第一個差異:this

上一篇已經談過,一般函式中的 this 通常要看函式是怎麼被呼叫的。

例如:

JavaScript
const user = {
    name: "Dennis",

    showName() {
        console.log(this.name);
    }
};

user.showName(); // Dennis

呼叫方式是:

JavaScript
user.showName();

所以這次 showName() 裡面的 this 指向 user

但是 Arrow Function 不一樣。

Arrow Function 沒有自己的 this

它會沿用外層環境原本的 this

這部分上一篇已經介紹過,所以這裡主要把它放進 Arrow Function 和一般 function 的比較裡。

---

Arrow Function 放在物件方法裡會怎樣?

例如:

JavaScript
const user = {
    name: "Dennis",

    showName: () => {
        console.log(this.name);
    }
};

user.showName();

很多人第一次看到會認為:

JavaScript
user.showName();

點號前面是 user,所以 this 應該指向 user

但這裡的 showName 是 Arrow Function。

Arrow Function 不會因為:

JavaScript
user.showName();

這種呼叫方式,就產生自己的 this

它會沿用外層環境原本的 this

所以這裡通常無法取得:

text
Dennis

---

Callback 裡面反而很適合使用 Arrow Function

Arrow Function 沒有自己的 this,有時候反而非常方便。

例如:

JavaScript
const user = {
    name: "Dennis",

    showNameLater() {
        setTimeout(() => {
            console.log(this.name);
        }, 1000);
    }
};

user.showNameLater();

先看這一段:

JavaScript
user.showNameLater();

showNameLater() 是一般方法,因此這次執行時:

JavaScript
this

指向 user

接著裡面的 Arrow Function:

JavaScript
() => {
    console.log(this.name);
}

沒有自己的 this

所以它會沿用外層 showNameLater() 執行時的 this

最後就能取得:

JavaScript
user.name

一秒後輸出:

text
Dennis

這也是 Arrow Function 很常出現在 Callback 裡的原因之一。

---

Tips:這和 Closure 一樣嗎?

有點像,但不要混在一起。

Closure 處理的是:

函式能不能繼續存取建立位置的外部變數。

Arrow Function 的 this 則是:

它自己不建立 this,所以直接沿用外層環境原本的 this

兩者都和外層環境有關,但不是同一件事情。

---

第二個差異:arguments

一般 function 裡有一個特殊的:

JavaScript
arguments

例如:

JavaScript
function showArguments() {
    console.log(arguments);
}

showArguments("Dennis", 20, "巴黎");

雖然函式宣告時完全沒有寫參數:

JavaScript
function showArguments() {

但是 arguments 還是可以取得呼叫時傳進來的參數。

---

Tips:arguments 是什麼?

arguments 是一般函式裡的一個特殊物件。

它會保存這次呼叫函式時傳入的參數。

例如:

JavaScript
function showArguments() {
    console.log(arguments[0]);
    console.log(arguments[1]);
}

showArguments("Dennis", 20);

會輸出:

text
Dennis
20

可以先把它理解成:

JavaScript 幫一般函式準備的一個「這次到底收到哪些參數」的紀錄。

不過 arguments 看起來很像陣列,但它不是一般的 Array。

---

Arrow Function 沒有自己的 arguments

如果改成:

JavaScript
const showArguments = () => {
    console.log(arguments);
};

Arrow Function 不會建立自己的 arguments

所以不能直接把一般函式使用 arguments 的方式照搬過來。

如果真的需要接收不固定數量的參數,可以使用:

JavaScript
...args

例如:

JavaScript
const showArguments = (...args) => {
    console.log(args);
};

showArguments("Dennis", 20, "桃園");

這時 args 會是一個真正的陣列:

JavaScript
["Dennis", 20, "桃園"]

---

Tips:...args 是什麼?

這裡的:

JavaScript
...args

稱為 Rest Parameter(剩餘參數)

白話來說:

不管後面還傳多少參數,都先幫我收進 args 這個陣列裡。

例如:

JavaScript
const showData = (...args) => {
    console.log(args);
};

showData(1, 2, 3, 4, 5);

會得到:

JavaScript
[1, 2, 3, 4, 5]

所以現代 JavaScript 中,如果只是想接收不固定數量的參數,很多時候會直接使用 Rest Parameter。

---

第三個差異:Arrow Function 不能搭配 new

一般函式可以作為建構函式:

JavaScript
function User(name) {
    this.name = name;
}

const dennis = new User("Dennis");

console.log(dennis.name); // Dennis

上一篇有提到,當我們使用:

JavaScript
new User("Dennis")

JavaScript 會建立一個新的物件,並讓 User() 裡面的 this 指向這個新物件。

但 Arrow Function 不行。

例如:

JavaScript
const User = (name) => {
    this.name = name;
};

const dennis = new User("Dennis");

會發生:

text
TypeError

因為 Arrow Function 不能當作建構函式使用。

---

Tips:為什麼 Arrow Function 不能搭配 new

Arrow Function 本身沒有建構函式需要的內部建構能力。

因此不能使用:

JavaScript
new User()

來呼叫 Arrow Function。

另外,Arrow Function 也沒有自己的 this

所以如果函式預計要搭配:

JavaScript
new

建立物件,就應該使用一般 function 或 class

---

第四個差異:Arrow Function 沒有自己的 prototype

一般函式建立後,通常會有自己的:

JavaScript
prototype

例如:

JavaScript
function User() {}

console.log(User.prototype);

但 Arrow Function:

JavaScript
const User = () => {};

沒有一般函式那樣的 prototype 屬性:

JavaScript
console.log(User.prototype); // undefined

這也和前面提到它不能作為建構函式搭配 new 使用有關。

---

Tips:Prototype 是什麼?

Prototype 中文通常翻譯成「原型」。

這是一個比較大的 JavaScript 主題。

目前可以先簡單理解:

JavaScript 物件之間可以透過 Prototype 建立繼承與共享功能的關係。

這一篇先知道 Arrow Function 沒有一般建構函式使用的 prototype 就可以。

之後談到 Prototype 時再完整拆解。

---

一般 function 和 Arrow Function 都可以當 Callback

看到這裡不要誤會成:

Callback 就一定要使用 Arrow Function。

不是。

兩種都可以。

例如:

JavaScript
const numbers = [1, 2, 3];

const doubled = numbers.map(function (number) {
    return number * 2;
});

console.log(doubled);

也可以寫成:

JavaScript
const numbers = [1, 2, 3];

const doubled = numbers.map(number => number * 2);

console.log(doubled);

兩個結果都是:

JavaScript
[2, 4, 6]

只是在這種簡單的 Callback 中,Arrow Function 通常比較簡潔。

所以很常看到:

JavaScript
numbers.map(number => number * 2);

而不是:

JavaScript
numbers.map(function (number) {
    return number * 2;
});

---

什麼時候適合使用 Arrow Function?

Arrow Function 很適合:

  • 簡單的 Callback
  • map()filter()reduce() 這類陣列操作
  • 不需要自己 this 的函式
  • 希望沿用外層 this 的 Callback
  • 很短、單純做資料轉換的函式

例如:

JavaScript
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。

例如物件方法:

JavaScript
const user = {
    name: "Dennis",

    showName() {
        console.log(this.name);
    }
};

因為這裡需要:

JavaScript
this

根據:

JavaScript
user.showName();

這次呼叫指向 user

所以使用一般方法會比較合適。

---

Arrow Function 和一般 function 比較

最後整理成一張表:

特性一般 functionArrow Function
基本語法較完整較精簡
Implicit Return不行可以
自己的 this沒有
this 判斷通常看呼叫方式沿用外層 this
自己的 arguments沒有
...args可以可以
可以搭配 new可以不可以
可當建構函式可以不可以
一般函式的 prototype 屬性沒有
適合簡短 Callback可以很常見
適合需要動態 this 的物件方法適合通常不太適合

---

常見誤解

誤解一:Arrow Function 只是一般 function 的簡寫

不完全是。

它確實可以讓程式碼變短,例如:

JavaScript
const double = number => number * 2;

但它和一般 function 在:

text
this
arguments
new
prototype

等行為上都有差異。

所以不能單純把它理解成:

少寫幾個字的 function。

---

誤解二:Arrow Function 比較新,所以任何地方都應該用它

不是。

Arrow Function 和一般 function 沒有誰一定比較好。

重點是:

這個函式需要什麼行為?

如果你需要自己的 this

JavaScript
const user = {
    name: "Dennis",

    showName() {
        console.log(this.name);
    }
};

一般方法比較適合。

如果只是做一個簡單 Callback:

JavaScript
numbers.map(number => number * 2);

Arrow Function 通常更方便。

---

誤解三:Arrow Function 寫在物件裡,this 就會是那個物件

不是。

例如:

JavaScript
const user = {
    name: "Dennis",

    showName: () => {
        console.log(this.name);
    }
};

Arrow Function 不會因為它被寫在 user 裡面,就自動取得 user 作為 this

因為物件本身不會替 Arrow Function 建立一個新的 this

---

誤解四:Arrow Function 不能當 Callback

當然可以。

而且它其實非常常被拿來當 Callback。

例如:

JavaScript
setTimeout(() => {
    console.log("時間到了");
}, 1000);

或:

JavaScript
numbers.map(number => number * 2);

真正需要注意的不是「能不能當 Callback」,而是:

這個 Callback 是否需要自己的 this

---

簡單整理

Arrow Function 最基本的寫法是:

JavaScript
const add = (a, b) => {
    return a + b;
};

如果只有一個運算式,可以簡寫成:

JavaScript
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 換成 =>,而是一種在 thisargumentsnew 等行為上都和一般 function 不同的函式。

所以下次看到:

JavaScript
function () {}

和:

JavaScript
() => {}

不要只把它們想成:

長版和短版。

更重要的是先問:

這個函式需不需要自己的 this?需不需要 arguments?會不會拿來搭配 new

搞清楚這幾件事之後,就比較知道什麼時候該用哪一種函式。