iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 21
1
Modern Web

JavaScript基本功修煉系列 第 21

JavaScript基本功修練:Day21 - 箭頭函式

ES6新增了箭頭函式的語法,方便我們用簡短直接的方式寫出函式,開發時不用再重複寫function、return這些英文字。然而,雖然簡潔方便,但在運作上與傳統函式並不一樣,特別是在this的運用方面,兩者的概念完全不同。這篇文章會整理有關箭頭函式的知識,包括以下內容:

  • 箭頭函式的語法
  • 比較箭頭函式和傳統函式的分別
  • 不可用箭頭函式的情況

箭頭函式的語法

最簡寫的語法結構,用白話來解釋就是:

參數 => 表達式(意思是會回傳一個值)

全寫:

//全寫
const num = (a) => {
    return a 
}

(){}return,在某些情況下可以刪去:

//--------------括號()--------------

//一個參數,不用()
const num = a => a

//沒有參數,要()
const num = () => 'hello world'

//多於一個參數,要()
const num = (a,b) => a + b

//--------------花括號{}、return--------------

//多於一行陳述,要用{}、return
const num = (a,b) => {
    let sum = a + b;
    return sum * 10
}

//一行陳述,不用 {}、return 
const num = (a,b) => a + b

// !!!! 有{}就有return !!!!

注意,{}就要有return,否則會回傳undefined

const num = (a,b) => {
    a + b
}
num(10,20) //undefined

比較傳統函式與箭頭函式的分別

箭頭函式沒有arguments物件

箭頭函式並沒有arguments這個物件:

function func1(){
    console.log(arguments)
}
func1(1,2,3,4) //類陣列 [1,2,3,4]

const func2 = () => console.log(arguments)
func2(1,2,3,4) 
//Uncaught ReferenceError: arguments is not defined

傳進去函式的參數會被放在一個類陣列裏,這就是arguments。傳統函式會內置有argument這個物件,但箭頭函式不會。

雖然如此,我們仍然可以用其餘參數的方式,把參數取出來:

const func2 = (...x) => console.log(x) 
func2(1,2,3,4) //[1, 2, 3, 4]

this的運用

  • 傳統函式:它會自動建立this,預設是指向window物件
  • 箭頭函式:它沒有自己的this,它是繼承上一層作用域(即是函式)的this

在比較兩者的this之前,到底什麼是作用域?

重溫一下作用域的意思:

MDN說明,函式會產生一個作用域(scope)。我們知道函式裹可以再包一個函式,形成一層層的函式,即是一層層的作用域。內部函式可以取得外部函式的值,但外部就不能取得內部的值。

A function serves as a closure in JavaScript, and thus creates a scope

If a variable or other expression is not "in the current scope," then it is unavailable for use. Scopes can also be layered in a hierarchy, so that child scopes have access to parent scopes, but not vice versa.

例如以下情況,外部的func1是不會取到func2y。在執行func1()之前,在全域查詢xx不會取到內部的x = 123

var x = 100

function func1(){
// func1作用域
    x = 123

    function func2(){
    // func2作用域
        var y = 456
        console.log(x) //123
    }
    func2()
    console.log(y) //y is not defined
}

console.log(x) //全域的100,不會吃到func1作用域的123
func1()

另外要注意,物件實字(object literal)不會產生作用域(scope),函式才會產生作用域。

回到重點,比較一下傳統函式和箭頭函式的this。先看看一個簡單例子:

const person = {
    name: 'Mary',
    age: 18,
    show1: () => console.log(this.age, this),
    show2: function(){
        //正式建立一個作用域
        setTimeout( () => {
            console.log(this.age, this)},1000)        
    },
    show3: function(){
        return console.log(this.age, this)
    }
}

person.show1() //undefined, window物件
person.show2() //18, person物件
person.show3() //18, person物件

show1
show1是箭頭函式,裏面的this會繼承上一層作用域的this(即是window物件),window物件沒有age變數,所以回傳undefined

show2
show2有兩層函式,外層是傳統函式,內層是箭頭函式。當內層被執行時,這裏的this會尋找外層作用域的this(即是person物件),所以會回傳18

另外,setTimeout是一個Callback function(回傳函式),當我們用傳統函式去寫這裏的回傳函式時,它的this會自動指向window物件。然而,在箭頭函式裏,因為它不會有自己的this,所以它只會像之前提及一樣,尋找自己的外層作用域的this。

show3
show3是一個傳統函式,傳統函式裏的this,取決於我們如何呼叫這個傳統函式。因為我們是透過person物件來呼叫show3,所以show3的函式裏的this會指向person物件,繼而回傳18

接下來我們看一些比較複雜的例子。

例子參考至這裏,範例如下:

const person = function(){
    const num1 = function(){
        setTimeout( () => {
            console.log(this.age, this)
        },500);
    };
    const num2 = {
        num3: num1,
        age: 30
    }
    num1(); //undefined, window物件
    num2.num3() //30, num2物件
}

person()

我們呼叫person變數裏的函式,繼而在person函式裏呼叫num1num3函式。

num1
setTimeout裏面的this會尋找外層匿名函式所指向的this物件,即是window,所以setTimeout裏面的this也會是指向windowwindow全域物件並沒有變數age,所以this.age結果是undefined

num2
num2物件裏的num3num1,即是上面那個包著setTimeout箭頭函式的匿名函式。然而,這次是在num2這個物件實字之下執行。這時候setTimout對上一層的匿名函式的this就是指向num2物件,由於setTimeoutthis等於匿名函式的this,即是num2物件,所以會指向num2物件裏的age屬性,回傳30。

嚴格模式下的this不同

如果我們在全域直接呼叫函式,就是簡易呼叫(simple call)的做法。在嚴格模式下,如果簡易呼叫的方式調用this,傳統函式裏的this會成為undefined,但箭頭函式的this仍然會是window物件。

//在全域直接呼叫函式,並回傳this

//箭頭函式會得出window
const f = () => { 
    'use strict'; 
    return console.log(this); //window物件
};
f()

//傳統函式會得出undefined
const f1 = function(){
    'use strict'; 
    return console.log(this) //undefined 
}
f1()

不可用箭頭函式的情況

不可用箭頭函式的情況包括:

  • 物件實字裏的方法
  • call,bind,apply方法
  • 建構函式
  • 設定物件原型
  • DOM事件監聽

物件實字裏的方法

這一點在上面比較傳統函式和箭頭函式時已經提過,如果一個物件實字的方法是用箭頭函式,它不會指向那件物件,因為箭頭函式要做的事,就是找它上層的函式(作用域),並繼承這個函式的this(即是指向它所指向的物件)。所以它會指向的是全域物件window,而非那個物件實字。

不厭其煩地重溫一下:

const obj = {
    price: 30,
    addPrice: () => console.log(this.price,this) //undefined, window物件
}

obj.addPrice()

call,bind,apply方法

callbindapply方法是傳入物件,並替換掉函式裏this所指向的物件。但因為箭頭函式已綁定了它會繼承的物件,所以即使我們傳入物件,函式也會直接無視這些方法所傳入的物件:

const person1 = {
    name: 'Jane'
}

//this已綁定window物件
const showName1 = () => {
    console.log(this);
}

const showName2 = function(){
    console.log(this)
}

showName1.call(person1) //window物件
showName2.call(person1) //person1物件

建構函式

在原型的章節中曾提及,我們可以用建構函式來new出一個實體物件。在建構函式中,我們會用到this來定義由該建構函式產生出來的實體物件裏的屬性,所以這裏的this會指向該新產生出來的實體物件。但是,因為剛才提及過,箭頭函式裏的this已經綁定了它需要繼承的物件(在此例是window),所以它不能指向新的實體物件。

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

const person = new User('David',20)
//Uncaught TypeError: User is not a constructor

設定物件的原型

我們不能用箭頭函式設定物件的原型,原因跟上面一樣,範例如下:

const User = function(name,age){
    this.name = name;
    this.age = age;
}


User.prototype.age1 = () => console.log(this.age,this)
User.prototype.age2 = function(){
    console.log(this.age,this)
}

const person1 = new User('Mary',30)

person1.age1(); //undefined, window物件
person1.age2(); //30, person1物件

DOM事件監聽

在設定DOM元素時,我們可以直接在元素上綁定監聽事件。以下例子中的this直接綁定button這個元素:

<button onclick="console.log(this)">button</button>

我們也可以在JS寫上監聽事件,例如按下後會變成紅色:

const btn = document.querySelector('button');
btn.addEventListener('click',function(){
    this.style.background = 'red'
})

addEventListener入面,我們需要用傳統函式,不能用箭頭函式。原因跟上面提及一樣,因為箭頭函式的this已經是綁定了,它不能再指向另一個物件,所以不可用箭頭函式去處理DOM元素的事件監聽。

總結

  • 語法:寫{}要就一併寫return

  • 作用域:函式會產生一個作用域,物件實字不會

  • 箭頭函式和傳統函式的分別:

    • 箭頭函式沒有arguments物件
    • this的運用不同,傳統函式預設this指向window,箭頭函式繼承上層作用域(即是函式)所指向的物件
  • 不可用箭頭函式的情況:

    • 物件實字裏的方法
    • call,bind,apply方法
    • 建構函式
    • 設定物件原型
    • DOM事件監聽

参考資料

鐵人賽:箭頭函式 (Arrow functions)
從ES6開始的JavaScript學習生活
JS核心篇(六角學院)


上一篇
JavaScript基本功修練:Day20 - this的運作
下一篇
JavaScript基本功修練:Day22 - 回傳函式與立即函式(IIFE)
系列文
JavaScript基本功修煉31

尚未有邦友留言

立即登入留言