2022年了你還不瞭解箭頭函式與普通函式的區別嗎?

前端南玖發表於2022-03-01

前言

箭頭函式作為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) // 

箭頭函式prototype.png

從列印結果來看,箭頭函式與普通函式相比,缺少了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永遠指向它定義時所在的上層作用域,所以say2this應該指向的是全域性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.png

我們知道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

箭頭函式arguments.png

可以使用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只能普通函式使用
  • 接受引數restarguments更加靈活
  • 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/awaitgenerator生成器越來越沒人用了。猜測可能是因為這個原因新增一個使用頻率不高的語法,給規範帶來較大的複雜度可能不值當。

箭頭函式不適用場景

物件方法,且方法中使用了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.namenanperson2中的this.namejiu,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

推薦閱讀

原文首發地址點這裡,歡迎大家關注公眾號 「前端南玖」

我是南玖,我們下期見!!!

相關文章