2022年了你還不了解箭頭函數與普通函數的區別嗎?
- 2022 年 3 月 1 日
- 筆記
- javascript, 前端面試題
前言
箭頭函數作為ES6中新加入的語法,以其簡化了我們的代碼和讓開發人員擺脫了「飄忽不定」的this指向等特點,深受廣大開發者的喜愛,同時也深受面試官的喜愛,箭頭函數常因其不同於普通函數的特點出現在各大公司的面試題中,so,本文會對箭頭函數與普通函數進行一些分析。
如果這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者,文章公眾號首發,關注 前端南玖
第一時間獲取最新的文章~
介紹箭頭函數(Arrow Function)
ES6中允許使用「箭頭」(=>) 來定義函數。箭頭函數相當於匿名函數,並且簡化了函數定義。
我們來看一下如何使用 (=>) 來聲明一個函數:
// 箭頭函數
let foo = (name) => `我是${name}`
foo('南玖') // 我是南玖
// 等同於下面這個普通函數
let foo2 = function(name) {
return `我是${name}`
}
箭頭函數有兩種格式,一種像上面的,只包含一個表達式,連{ ... }
和return
都省略掉了。還有一種可以包含多條語句,這時候就不能省略{ ... }
和return
:
let foo = (name) => {
if(name){
return `我是${name}`
}
return '前端南玖'
}
foo('南玖') // 我是南玖
⚠️這裡需要注意的是如果箭頭函數返回的是一個字面量對象,則需要用括號包裹該字面量對象返回
let foo = (name) => ({
name,
job: 'front end'
})
// 等同於
let foo2 = function (name) {
return {
name,
job: 'front end'
}
}
OK,箭頭函數的基本介紹我們先看到這裡,下面我們通過對比箭頭函數與普通函數的區別來進一步了解箭頭函數~
箭頭函數與普通函數的區別
我們可以通過打印箭頭函數和普通函數來看看兩者到底有什麼區別:
let fn = name => {
console.log(name)
}
let fn2 = function(name) {
console.log(name)
}
console.dir(fn) //
console.dir(fn2) //
從打印結果來看,箭頭函數與普通函數相比,缺少了caller,arguments,prototype
聲明方式不同,匿名函數
- 聲明一個普通函數需要使用關鍵字
function
來完成,並且使用function
既可以聲明成一個具名函數也可以聲明成一個匿名函數 - 聲明一個箭頭函數則只需要使用箭頭就可以,無需使用關鍵字
function
,比普通函數聲明更簡潔。 - 箭頭函數只能聲明成匿名函數,但可以通過表達式的方式讓箭頭函數具名
this指向不同
對於普通函數來說,內部的this
指向函數運行時所在的對象,但是這一點對箭頭函數不成立。它沒有自己的this
對象,內部的this
就是定義時上層作用域中的this
。也就是說,箭頭函數內部的this
指向是固定的,相比之下,普通函數的this
指向是可變的。
var name = '南玖'
var person = {
name: 'nanjiu',
say: function() {
console.log('say:',this.name)
},
say2: () => {
console.log('say2:',this.name)
}
}
person.say() // say: nanjiu
person.say2() // say2: 南玖
這裡第一個say
定義的是一個普通函數,並且它是作為對象person
的方法來進行調用的,所以它的this
指向的就是person
,所以它應該會輸出say: nanjiu
而第二個say2
定義的是一個箭頭函數,我們知道箭頭函數本身沒有this
,它的this
永遠指向它定義時所在的上層作用域,所以say2
的this
應該指向的是全局window,所以它會輸出say2: 南玖
我們也可以通過Babel
轉箭頭函數產生的 ES5
代碼來證明箭頭函數沒有自己的this
,而是引用的上層作用域中this
。
// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
轉換後的 ES5 版本清楚地說明了,箭頭函數裏面根本沒有自己的this
,而是引用的上層作用域中this
。
箭頭函數的this永遠不會變,call、apply、bind也無法改變
我們可以用call、apply、bind來改變普通函數的this指向,但是由於箭頭函數的this指向在它定義時就已經確定了,永遠指向它定義時的上層作用域中的this,所以使用這些方法永遠也改變不了箭頭函數this
的指向。
var name = '南玖'
var person = {
name: 'nanjiu',
say: function() {
console.log('say:',this.name)
},
say2: () => {
console.log('say2:',this.name)
}
}
person.say.call({name:'小明'}) // say: 小明
person.say2.call({name:'小紅'}) // say2: 南玖
還是上面那個例子,只不過我們在調用的時候使用call
試圖改變this
指向,第一個say
是一個普通函數,它經過call調用,打印出的是say: 小明
,這說明普通函數的this已經改變了,第二個say2
是一個箭頭函數,它也經過call調用,但它打印出的仍然是say2: 南玖
,這就能夠證明箭頭函數的this永遠不會變,即使使用call、apply、bind也無法改變
箭頭函數沒有原型prototype
let fn = name => {
console.log(name)
}
let fn2 = function(name) {
console.log(name)
}
console.log(fn.prototype) // undefined
console.dir(fn2.prototype) // {constructor: ƒ}
箭頭函數不能當成一個構造函數
為什麼箭頭函數不能當成一個構造函數呢?我們先來用new
調用一下看看會發生什麼:
let fn = name => {
console.log(name)
}
const f = new fn('nanjiu')
結果符合我們的預期,這樣調用會報錯
我們知道new內部實現其實是分為以下四步:
-
新建一個空對象
-
鏈接到原型
-
綁定this,執行構造函數
-
返回新對象
function myNew() {
// 1.新建一個空對象
let obj = {}
// 2.獲得構造函數
let con = arguments.__proto__.constructor
// 3.鏈接原型
obj.__proto__ = con.prototype
// 4.綁定this,執行構造函數
let res = con.apply(obj, arguments)
// 5.返回新對象
return typeof res === 'object' ? res : obj
}
因為箭頭函數沒有自己的this
,它的this
其實是繼承了外層執行環境中的this
,且this
指向永遠不會變,並且箭頭函數沒有原型prototype
,沒法讓他的實例的__proto__
屬性指向,所以箭頭函數也就無法作為構造函數,否則用new
調用時會報錯!
沒有new.target
new
是從構造函數生成實例對象的命令。ES6 為new
命令引入了一個new.target
屬性,這個屬性一般用在構造函數中,返回new
調用的那個構造函數。如果構造函數不是通過new
命令或Reflect.construct()
調用的,new.target
會返回undefined
,所以這個屬性可以用來確定構造函數是怎麼調用的。
function fn(name) {
console.log('fn:',new.target)
}
fn('nanjiu') // undefined
new fn('nanjiu')
/*
fn: ƒ fn(name) {
console.log('fn:',new.target)
}
*/
let fn2 = (name) => {
console.log('fn2',new.target)
}
fn2('nan') // 報錯 Uncaught SyntaxError: new.target expression is not allowed here
⚠️注意:
-
new.target
屬性一般用在構造函數中,返回new
調用的那個構造函數 -
箭頭函數的this指向全局對象,在箭頭函數中使用
new.target
會報錯 -
箭頭函數的this指向普通函數,它的
new.target
就是指向該普通函數的引用
箭頭函數沒有自己的arguments
箭頭函數處於全局作用域中,則沒有arguments
let fn = name => {
console.log(arguments)
}
let fn2 = function(name) {
console.log(arguments)
}
fn2() // Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ]
fn() // 報錯 Uncaught ReferenceError: arguments is not defined
還是用這兩個函數來比較,普通函數能夠打印出arguments
,箭頭函數使用arguments
則會報錯,因為箭頭函數自身是沒有arguments的,然後它會往上層作用域中去查找arguments
,由於全局作用域中並沒有定義arguments
,所以會報錯。
箭頭函數處於普通函數的函數作用域中,arguments則是上層普通函數的arguments
let fn2 = function(name) {
console.log('fn2:',arguments)
let fn = name => {
console.log('fn:',arguments)
}
fn()
}
fn2('nanjiu')
這裡兩個函數打印的arguments
相同,都是fn2函數的arguments
可以使用rest參數代替
ES6 引入 rest
參數,用於獲取函數不定數量的參數數組,這個API是用來替代arguments
的,形式為...變量名
,rest 參數搭配的變量是一個數組,該變量將多餘的參數放入數組中。
let fn3 = (a,...arr) => {
console.log(a,arr) //1, [2,3,4,5,6]
}
fn3(1,2,3,4,5,6)
上面就是rest參數的基本用法,需要⚠️注意的是:
rest
參數只能作為函數的最後一個參數
// 報錯
function f(a, ...b, c) {
// ...
}
- 函數的
length
屬性,不包括rest
參數
rest參數與arguments的比較:
- 箭頭函數和普通函數都可以使用
rest
參數,而arguments
只能普通函數使用 - 接受參數
rest
比arguments
更加靈活 rest
參數是一個真正的數組,而arguments
是一個類數組對象,不能直接使用數組方法
箭頭函數不能重複函數參數名稱
function fn(name,name) {
console.log('fn2:',name)
}
let fn2 = (name,name) => {
console.log('fn',name)
}
fn('nan','jiu') // 'jiu'
fn2('nan','jiu') // 報錯
不可以使用yield
命令,因此箭頭函數不能用作 Generator 函數。
這個可能是由於歷史原因哈,TC39 在 2013 年和 2016 年分別討論過兩次,從*()
、*=>
、=*>
、=>*
中選出了=>*
,勉強進入了 stage 1。而且因為有了異步生成器(async generator),所以還得同時考慮異步箭頭生成器(async arrow generator)的東西,之前生成器 99.999% 的用途都是拿它來實現異步編程
,並不是真的需要生成器本來的用途,自從有了 async/await
,generator
生成器越來越沒人用了。猜測可能是因為這個原因添加一個使用頻率不高的語法,給規範帶來較大的複雜度可能不值當。
箭頭函數不適用場景
對象方法,且方法中使用了this
var name = '南玖'
var person = {
name: 'nanjiu',
say: function() {
console.log('say:',this.name)
},
say2: () => {
console.log('say2:',this.name)
}
}
person.say() // say: nanjiu
person.say2() //say2: 南玖
上面代碼中,person.say2()
方法是一個箭頭函數,調用person.say2()
時,使得this
指向全局對象,因此不會得到預期結果。這是因為對象不構成單獨的作用域,導致say2()
箭頭函數定義時的作用域就是全局作用域。而say()
定義的是一個普通函數,它內部的this就指向調用它的那個對象,所以使用普通函數符合預期。
當函數需要動態this時
var button = document.querySelector('.btn');
button.addEventListener('click', () => {
this.classList.toggle('on');
});
這裡很顯然會報錯,因為按鈕點擊的回調是一個箭頭函數,而箭頭函數內部的this
永遠都是指向它的上層作用域中的this,在這裡就是window
,所以會報錯。這裡只需要將箭頭函數改成普通函數就能正常調用了!
看完來做個題吧~
var name = '南玖'
function Person (name) {
this.name = name
this.foo1 = function () {
console.log(this.name)
},
this.foo2 = () => console.log(this.name),
this.foo3 = function () {
return function () {
console.log(this.name)
}
},
this.foo4 = function () {
return () => {
console.log(this.name)
}
}
}
var person1 = new Person('nan')
var person2 = new Person('jiu')
person1.foo1() // 'nan'
person1.foo1.call(person2) // 'jiu'
person1.foo2() // 'nan'
person1.foo2.call(person2) // 'nan'
person1.foo3()() // '南玖'
person1.foo3.call(person2)() // '南玖'
person1.foo3().call(person2) // 'jiu'
person1.foo4()() // 'nan'
person1.foo4.call(person2)() // 'jiu'
person1.foo4().call(person2) // 'nan'
解析:
全局代碼執行,person1 = new Person('nan'),person2 = new Person('jiu')
執行完,person1
中的this.name
為nan
,person2
中的this.name
為jiu
,OK這一點清楚後,繼續往下看:
- 執行
person1.foo1()
,foo1
為普通函數,所以this應該指向person1
,打印出nan
- 執行
person1.foo1.call(person2)
,foo1
為普通函數,並且用call改變了this指向,所以它裏面的this應該指向person2
,打印出jiu
- 執行
person1.foo2()
,foo2
為箭頭函數,它的this指向上層作用域,也就是person1,所以打印出nan
- 執行
person1.foo2.call(person2)
,箭頭函數的this指向無法使用call改變,所以它的this還是指向person1,打印出nan
- 執行
person1.foo3()()
,這裡先執行person1.foo3()
,它返回了一個普通函數,接着再執行這個函數,此時就相當於在全局作用域中執行了一個普通函數,所以它的this指向window,打印出南玖
- 執行
person1.foo3.call(person2)()
這個與上面類似,也是返回了一個普通函數再執行,其實前面的執行都不用關心,它也是相當於在全局作用域中執行了一個普通函數,所以它的this指向window,打印出南玖
- 執行
person1.foo3().call(person2)
這裡就是把foo3返回的普通函數的this綁定到person2上,所以打印出jiu
- 執行
person1.foo4()()
,先執行person1.foo4()
返回了一個箭頭函數,再執行這個箭頭函數,由於箭頭函數的this始終指向它的上層作用域,所以打印出nan
- 執行
person1.foo4.call(person2)()
,與上面類似只不過使用call把上層作用域的this改成了person2,所以打印出jiu
- 執行
person1.foo4().call(person2)
,這裡是先執行了person1.foo4()
,返回了箭頭函數,再試圖通過call改變改變該箭頭函數的this指向,上面我們說到箭頭函數的this始終指向它的上層作用域,所以打印出nan
推薦閱讀
- 前端常見的安全問題及防範措施
- 為什麼大廠前端監控都在用GIF做埋點?
- 介紹迴流與重繪(Reflow & Repaint),以及如何進行優化?
- Promise、Generator、Async有什麼區別?
- JS定時器執行不可靠的原因及解決方案
- 從如何使用到如何實現一個Promise
- 超詳細講解頁面加載過程
原文首發地址點這裡,歡迎大家關注公眾號 「前端南玖」
我是南玖,我們下期見!!!