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
這個物件:
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
,預設是指向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
是不會取到func2
的y
。在執行func1()
之前,在全域查詢x
,x
不會取到內部的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
函式裏呼叫num1
和num3
函式。
num1
:setTimeout
裏面的this
會尋找外層匿名函式所指向的this
物件,即是window
,所以setTimeout
裏面的this
也會是指向window
,window
全域物件並沒有變數age
,所以this.age
結果是undefined
。
num2
:num2
物件裏的num3
是num1
,即是上面那個包著setTimeout
箭頭函式的匿名函式。然而,這次是在num2
這個物件實字之下執行。這時候setTimout
對上一層的匿名函式的this
就是指向num2
物件,由於setTimeout
的this
等於匿名函式的this
,即是num2
物件,所以會指向num2
物件裏的age
屬性,回傳30。
如果我們在全域直接呼叫函式,就是簡易呼叫(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()
不可用箭頭函式的情況包括:
這一點在上面比較傳統函式和箭頭函式時已經提過,如果一個物件實字的方法是用箭頭函式,它不會指向那件物件,因為箭頭函式要做的事,就是找它上層的函式(作用域),並繼承這個函式的this
(即是指向它所指向的物件)。所以它會指向的是全域物件window,而非那個物件實字。
不厭其煩地重溫一下:
const obj = {
price: 30,
addPrice: () => console.log(this.price,this) //undefined, window物件
}
obj.addPrice()
call
、bind
和apply
方法是傳入物件,並替換掉函式裏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元素時,我們可以直接在元素上綁定監聽事件。以下例子中的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
作用域:函式會產生一個作用域,物件實字不會
箭頭函式和傳統函式的分別:
this
指向window,箭頭函式繼承上層作用域(即是函式)所指向的物件不可用箭頭函式的情況:
鐵人賽:箭頭函式 (Arrow functions)
從ES6開始的JavaScript學習生活
JS核心篇(六角學院)