跳至主要内容

[JS] 箭頭函式(arrow function)和它對 this 的影響

@([Udemy] JavaScript: Understanding the Weird Parts)[JavaScript, udemy]

Arrow Function @ MDN

img

在這篇文章中要來說明一下在 ES6 中相當常見的箭頭函式(arrow function),讓我們來看一下可以怎麼樣使用。

什麼是箭頭函式(arrow function)

首先,我們來看一下過去我們撰寫函式的方法:

function greeting() {
console.log('Hello, JavaScript!!');
}

greeting();

在 ES6 之後,我們可以把它改成箭頭函式的寫法,它會變成下面這樣:

const greeting = () => {
console.log('Hello, JavaScript!!');
};

greeting();

沒有參數的時候要記得加上空括號

要特別留意的地方是,在箭頭函式中如果沒有帶入參數時,一樣要加上空括號:

const greeting = () => console.log('Hello, JavaScript!!');
greeting();

如果只是要回傳某個值,可以省略 return

如果我們的函式本身只是要回傳某個值的話,可以把 return 這個字省略掉:

const greeting = () => 'Hello, JavaScript!!';
console.log(greeting()); // 'Hello, JavaScript!!'

// 等同於這樣寫
const greeting = function () {
return 'Hello, JavaScript!!';
};

箭頭函式帶入參數值

兩個以上的參數,需要使用括號

當我們的函式擁有兩個以上的參數時,我們一樣可以使用括號來帶入參數,寫法像是下面這樣子:

const add = (a, b) => a + b;
console.log(add(3, 5));

// 等同於這樣寫
const add = function (a, b) {
return a + b;
};

當函式只有一個參數時,不需要使用括號

從上面的例子我們可以知道,當函式沒有參數或有兩個以上的參數時,我們都要加上括號 ( ),但是當函式只有一個參數時,可以省略括號不寫,因此,當我們的函式只有一個參數時,我們的函式長得像這樣:

const greeting = (person) => `Hello, ${person}`;
greeting('Aaron'); // Hello, Aaron

// 等同於這樣寫
const greeting = function (person) {
return `Hello, ${person}`;
};

箭頭函式對 this 的影響

箭頭函式當中的 this 綁定的是是定義時的物件,而不是使用時的物件。也就是說,在箭頭函式中,this 指稱的對象在被定義時就固定了,而不會隨著使用時的脈絡而改變。

讓我們來看一下這個例子,在這個範例中,不論我們使用的是原本 function 的寫法或 ES6 中的箭頭函式,都會回傳得到最外層的 Window 物件(如果不清楚 this 的話可以先參考  [筆記] 談談 JavaScript 中的"this"和它的 bug),這樣看起來似乎兩者沒有太大的差別:

// 原本 function 的寫法
const fn = function () {
console.log(this);
};

fn(); // Window Object

// 使用 arrow Function
const arrowFn = () => {
console.log(this);
};
arrowFn(); // Window Object

然而,換個例子的情況就不一樣,讓我們來看看下面兩個不同的例子:

範例一

此範例參考自阮一峰 - ECMAScript 6 入門

我們分別用原本的寫法和箭頭函式的寫法建立了兩個 function:

let id = 21;
let data = {
id: 21,
};

fn.call(data);
arrowFn.call(data);

// 原本的 function
function fn() {
console.log(this.constructor.name); // Object(data)

setTimeout(function () {
console.log(this.constructor.name); // Window
}, 100);
}

// 箭頭函式 Arrow function
function arrowFn() {
console.log(this.constructor.name); // Object(data)

setTimeout(() => {
console.log(this.constructor.name); // Object(data)
}, 100);
}
  • setTimeout:裡面都分別帶入 setTimeout 的函式,我們知道 setTimeout 執行的時間會在整個 JS execution context 都被執行完後才會執行(如果對這概念不清楚的話可參考:[筆記] 談談 JavaScript 中的 asynchronous 和 event queue),因此函式建立的時間和實際執行的時間是不同的,因此這也創造了兩個不同時間點的 this 所指稱的對象。
  • call:另一個要了解的是 .call() 的用法,在 .call() 當中,我們會帶入後面所指定的物件當作所指稱的 this 對象(對這個概念不清楚的話可參考:[筆記] 了解 function borrowing 和 function currying ─ bind(), call(), apply() 的應用)。
  • this.constructor.name:這樣子的寫法只是避免在回傳出物件的時候把整個物件內容給傳出來,而是指示傳出該物件的名稱。

我們可以知道,因為有用 .call(data) 的緣故,因此不論是使用傳統函式寫法 fn 或箭頭函式 arrowFn 時,在一開始函式裡面的 this 都指稱的是 data 這個物件。

然而不同的地方會在執行 setTimeout 中的函式,在使用傳統函式的寫法時,使用  fn.call(data)  時,因為它執行的時間點是在整個 JS execution context 執行完才執行,而當時的環境會變成是 global environment,因此使用傳統函式時,這個 this 指稱的對象會轉變成 Window 物件(預設綁定)。但是,如果使用的是箭頭函式(arrow function),這個 this 所指稱的對象則不會改變,依然是 data 這個 object。

範例:https://jsbin.com/wodegu/edit?js,console

範例二

此範例參考自  Accelerated ES6 JavaScript Training @ Udemy

第二個例子是使用 addEventListener 來達到示範,首先我們在 HTML 中建立一個 <button>,然後利用 JS 來抓取這個 button,接著 JS 部分則如下所示:

var button = document.querySelector('button');
var arrowFn = () => {
// 建立 function 時 this 指 Window
console.log(this.constructor.name); // 執行 function 時 this 指 Window
};
var fn = function () {
// 建立 function 時 this 指 Window
console.log(this.constructor.name); // 執行 function 時 this 指 HTMLButtonElement
};

button.addEventListener('click', arrowFn);

和例子一中的 setTimeout 類似,我們使用的 addEventListener,也會在整個 execution context 執行結束後,在網頁觸發事件時才執行。 因此不論在傳統的函式寫法 fn 或箭頭函式 arrowFn 的寫法,一開始建立 function 的時候 this 所指稱的都是  Window 物件(全域物件)。

然而,如果是使用傳統的寫法,在觸發這個事件時所指稱的對象會從原本的 Window 物件變成 HTMLButtonElement;但若使用的是箭頭函式,則會固定所指稱的對象,因此 this 依然指稱的是 Window 物件。

範例:https://jsbin.com/kotetu/edit?js,output

資料來源