「ES6系列」完全弄懂箭頭函數(內附習題)

引言

在 ES6 中,箭頭函數是其中最有趣也最受歡迎的新增特性。javascript

本文會分爲三個部分來介紹:前端

第一部主要介紹箭頭函數的基本語法與使用方式,其中關於this的指向問題會着重介紹。java

第二部分探究一下箭頭函數在自執行函數中的奇怪現象。node

第三部分將提供一些面試題目,用於幫助你們理解。git

什麼是箭頭函數

顧名思義,箭頭函數是一種使用 (=>) 定義函數的新語法,它與傳統的 ES5 函數有些許不一樣。es6

這是一個用 ES5 語法編寫的函數:github

function addTen(num){
  return num + 10;
}
addTen(5); // 15
複製代碼

有了 ES6 的箭頭函數後,咱們能夠用箭頭函數這樣表示:面試

var addTen = num => num + 10

addTen(5); // 15
複製代碼

箭頭函數的寫法短的多!因爲隱式返回,咱們能夠省略花括號和 return 語句。瀏覽器

與常規 ES5 函數相比,瞭解箭頭函數的行爲方式很是重要。微信

箭頭函數的特色

更短的語法

基礎語法以下:

(參數)=> { statements }
複製代碼

接下來,拆解一下箭頭函數的各類書寫形式:

當沒有參數時,使用一個圓括號表明參數部分

let f = ()=> 5;

f(); // 5
複製代碼

當只有一個參數時,能夠省略圓括號。

let f = num => num + 5;

f(10); // 15
複製代碼

當有多個參數時,在圓括號內定義多個參數用逗號分隔。

let f = (a,b) => a + b;

f(1,2); // 3
複製代碼

當箭頭函數的代碼塊部分多餘一條語句,就須要使用大括號括起來,而且使用 return 語句。

// 沒有大括號,默認返回表達式結果
let f1 = (a,b) => a + b
f1(1,2) // 3

// 有大括號,無return語句,沒有返回值
let f2 = (a,b) => {a + b}
f2(1,2) // undefined

// 有大括號,有return語句,返回結果
let f3 = (a,b) => {return a + b}
f3(1,2) // 3
複製代碼

因爲大括號被解釋爲代碼塊,因此若是箭頭函數直接返回一個對象,必須在對象外面加上括號,不然會報錯。

//報錯
let f1 = num => {num:num}

//不報錯
let f2 = num => ({num:num})
複製代碼

不能經過 new 關鍵字調用

箭頭函數沒有[[Construct]]方法,因此不能被用做構造函數。

let F = ()=>{};

// 報錯 TypeError: F is not a constructor
let f = new F();
複製代碼

沒有原型

因爲不能夠經過 new 關鍵字調用,於是沒有構建原型的需求,因此箭頭函數不存在 prototype 這個屬性。

let F = ()=>{};
console.log(F.prototype) // undefined
複製代碼

不能用做 Generator 函數

在箭頭函數中,不可使用 yield 命令,所以箭頭函數不能用做 Generator 函數。

沒有 arguments、super、new.target

箭頭函數中是沒有 arguments、super、new.target 的綁定,這些值由外圍最近一層非箭頭函數決定。

以 arguments 爲例,看以下代碼:

let f = ()=>console.log(arguments);

//報錯
f(); // arguments is not defined
複製代碼

因爲在全局環境下,定義箭頭函數 f,對於 f 來講,沒法獲取到外圍非箭頭函數的 arguments 值,因此此處報錯。

再看一個例子:

function fn(){
    let f = ()=> console.log(arguments)
    f();
}
fn(1,2,3) // [1,2,3]
複製代碼

上面的代碼,箭頭函數 f 內部的 arguments,實際上是函數 fn 的 arguments 變量。

若想在箭頭函數中獲取不定長度的參數列表,可使用 ES6 中的 rest 參數解決:

let f = (...args)=>console.log(args)

f(1,2,3,4,5) // [1,2,3,4,5]
複製代碼

沒有 this 綁定

在理解箭頭函數中的this指向問題以前,咱們先來回看在 ES5 中的一個例子:

var obj = {
  value:0,
  fn:function(){
    this.value ++
  }
}
obj.fn();
console.log(obj.value); // 1
複製代碼

這段代碼很簡單,在每次調用 obj.fn() 時,指望的是將 obj.value 加 1。

如今咱們將代碼修改一下:

var obj = {
  value:0,
  fn:function(){
    var f = function(){
      this.value ++
    }
    f();
  }
}
obj.fn();
console.log(obj.value); // 0
複製代碼

咱們將代碼修改了一下,在 obj.fn 方法內增長了一個函數 f ,並將 obj.value 加 1 的動做放到了函數 f 中。可是因爲 javascript 語言設計上的一個錯誤,函數 f 中的 this 並非 方法 obj.fn 中的 this,致使咱們無法獲取到 obj.value 。

爲了解決此類問題,在 ES5 中,咱們一般會將外部函數中的 this 賦值給一個臨時變量(一般命名爲 that、_this、self),在內層函數中若但願使用外層函數的 this 時,經過這個臨時變量來獲取。修改代碼以下:

var obj = {
  value:0,
  fn:function(){
    // 本人喜歡定義爲 _this,也有不少人喜歡定義成 that 或 self
    var _this = this;
    var f = function(){
      _this.value ++
    }
    f();
  }
}
obj.fn();
console.log(obj.value); // 1
複製代碼

從這個例子中,咱們知道了在 ES5 中如何解決內部函數獲取外部函數 this 的辦法。

而後咱們來看看箭頭函數相對於 ES5 中的函數來講,它的 this 指向有和不一樣。

先看一段定義,來源於ES6標準入門

箭頭函數體內的 this 對象就是定義時所在的對象,而不是使用時所在的對象。

那麼,如何來理解這句話呢?

咱們嘗試用babel來將以下代碼轉換成 ES5 格式的代碼,看看它都作了什麼。

function fn(){
  let f = ()=>{
    console.log(this)
  }
}
複製代碼

來看看轉化後的結果,直接上圖:

咱們發現了什麼,竟然和咱們以前在 ES5 中解決內層函數獲取外層函數 this 的方法同樣,定義一個臨時變量 _this ~

那麼,箭頭函數本身的 this 哪裏去了?

答案是,箭頭函數根本沒有本身的 this !

那麼,咱們能夠總結一下,將晦澀難懂的定義轉化成白話文:

  • 箭頭函數的外層若是有普通函數,那麼箭頭函數的 this 就是外層普通函數的this
  • 箭頭函數的外層若是沒有普通函數,那麼箭頭函數的 this 就是全局變量

讓咱們用幾個例子,來驗證一下咱們總結的規則:

let obj = {
    fn:function(){
        console.log('我是普通函數',this === obj)
        return ()=>{
            console.log('我是箭頭函數',this === obj)
        }
    }
}

console.log(obj.fn()())

// 我是普通函數 true
// 我是箭頭函數 true
複製代碼

從上面的例子,咱們可以看出,箭頭函數的 this 與外層函數的 this 是相等的。

在看一個多層箭頭函數嵌套的例子:

let obj = {
    fn:function(){
        console.log('我是普通函數',this === obj)
        return ()=>{
            console.log('第一個箭頭函數',this === obj)
            return ()=>{
                console.log('第二個箭頭函數',this === obj)
                return ()=>{
                    console.log('第三個箭頭函數',this === obj)
                }
            }
        }
    }
}

console.log(obj.fn()()()())
// 我是普通函數 true
// 第一個箭頭函數 true
// 第二個箭頭函數 true
// 第三個箭頭函數 true
複製代碼

在這個例子中,咱們可以知道,對於箭頭函數來講,箭頭函數的 this 與外層的第一個普通函數的 this 相等,與嵌套了幾層箭頭函數無關。

再來看一個沒有外層函數的例子:

let obj = {
    fn:()=>{
        console.log(this === window);
    }
}

console.log(obj.fn())

// true
複製代碼

這個例子,證實了,在箭頭函數外層沒有普通函數時,箭頭函數的 this 與全局對象相等。

須要注意的是,瀏覽器環境下全局對象爲 window,node 環境下全局對象爲 global,驗證的時候須要區分一下。

箭頭函數碰上 call、apply、bind

看到這裏,相信你們已經知道了,箭頭函數中根本沒有本身的 this ,那麼當箭頭函數碰到 call、apply、bind 時,會發生什麼呢?

咱們知道,call 和 apply 的做用是改變函數 this 的指向,傳遞參數,並將函數執行, 而 bind 的做用是生成一個綁定 this 並預設函數參數的新函數。

然而因爲箭頭函數根本沒有本身的 this ,因此:

  • 當對箭頭函數使用 call 或 apply 方法時,只會傳入參數並調用函數,並不會改變箭頭函數中 this 的指向;
  • 當對箭頭函數使用 bind 方法時,只會返回一個預設參數的新函數,並不會綁定新函數的 this 指向。

咱們來驗證一下:

window.name = 'window_name';

let f1 = function(){return this.name}
let f2 = ()=> this.name

let obj = {name:'obj_name'}

f1.call(obj) // obj_name
f2.call(obj) // window_name

f1.apply(obj) // obj_name
f2.apply(obj) // window_name

f1.bind(obj)() // obj_name
f2.bind(obj)() // window_name
複製代碼

上面代碼中,聲明瞭普通函數 f1,箭頭函數 f2。

普通函數的 this 指向是動態可變的,因此在對 f1 使用 call、apply、bind 時,f1 內部的 this 指向會發生改變。

箭頭函數的 this 指向在其定義時就已肯定,永遠不會發生改變,因此在對 f2 使用 call、apply、bind 時,會忽略傳入的上下文參數。

自執行函數

在 ES6 的箭頭函數出現以前,自執行函數通常會寫成這樣:

(function(){
    console.log(1)
})()
複製代碼

或者寫成這樣:

(function(){
    console.log(1)
}())
複製代碼

箭頭函數固然也能夠被用做自執行函數,能夠這樣寫:

(() => {
    console.log(1)
})()
複製代碼

可是,令大多數人想不到的是,下面這種寫法會報錯:

(() => {
    console.log(1)
}())
複製代碼

那麼,爲何會報錯呢?

這個問題,曾困擾了我好久,直到我翻閱了ECMAScript® 2015 規範,從中得知箭頭函數是屬於 AssignmentExpression 的一種,而函數調用屬於 CallExpression,規範中要求當 CallExpression 時,左邊的表達式必須是 MemberExpression 或其餘的 CallExpression,而箭頭函數不屬於這兩種表達式,因此在編譯時就會報錯。

原理就是這樣了,具體可參見ECMAScript® 2015 規範

關於箭頭函數的題目

在面試中關於箭頭函數的考察,主要集中在 arguments 關鍵字的指向和箭頭函數的this指向上,下面幾道題目,由淺入深,供你們參考一下。

題目一

function foo(n) {
  var f = () => arguments[0] + n;
  return f();
}

let res = foo(2);

console.log(res); // 問 輸出結果
複製代碼
答案及解析

答案: 4

箭頭函數沒有本身的 arguments ,因此題中的 arguments 指代的是 foo 函數的 arguments 對象。因此 arguments[0] 等於 2 ,n 等於 2,結果爲 4。

題目二

function A() {
  this.foo = 1
}

A.prototype.bar = () => console.log(this.foo)

let a = new A()
a.bar() // 問 輸出結果
複製代碼
答案

答案: undefined

箭頭函數沒有本身的 this,因此箭頭函數的 this 等價於外層非箭頭函數做用域的this。 因爲箭頭函數的外層沒有普通函數,因此箭頭函數中的 this 等價於全局對象,因此輸出爲 undefined。

題目三

let res = (function pt() {
  return (() => this.x).bind({ x: 'inner' })();
}).call({ x: 'outer' });

console.log(res)  // 問 輸出結果
複製代碼
答案

答案:'outer'

此題稍微複雜一點,求 res 的輸出結果。

分析以下:

  1. 求函數 pt 經過 call 調用後的返回值。
  2. pt 函數內的 this 被 call 轉換爲 {x:'outer'}。
  3. pt 函數內,箭頭函數經過 bind 生成了新函數,並執行,執行結果爲 pt 函數的返回值。
  4. 箭頭函數中的 this 沒法經過 bind 方法綁定,箭頭函數執行時的 this 就是外層做用域的 this。
  5. 箭頭函數執行時,外層做用域的 this 是由 call 方法指定的 {x:'outer'}。
  6. 最終結果 res 爲 'outer'。

題目四

window.name = 'window_name';

let obj1 = {
    name:'obj1_name',
    print:()=>console.log(this.name)
}

let obj2 = {name:'obj2_name'}

obj1.print()  // 問 輸出結果
obj1.print.call(obj2)  // 問 輸出結果
複製代碼
答案

答案:'window_name' 'window_name'

箭頭函數沒有本身的 this ,也沒法經過 call、apply、bind 改變箭頭函數中的 this。 箭頭函數的 this 取決於外層是否有普通函數,有普通函數 this 指向普通函數中的this,外層沒有普通函數,箭頭函數中的 this 就是全局對象。

此題中,箭頭函數外層沒有普通函數,因此 this 指向全局對象,因此結果爲 'window_name'、'window_name'。

題目五

let obj1 = {
    name:'obj1_name',
    print:function(){
        return ()=>console.log(this.name)
    }
}

let obj2 = {name:'obj2_name'}


obj1.print()() // 問 輸出結果
obj1.print().call(obj2) // 問 輸出結果
obj1.print.call(obj2)() // 問 輸出結果
複製代碼
答案

答案: 'obj1_name' 'obj1_name' 'obj2_name'

箭頭函數的 this 與其外層的普通函數的 this 一致,與 call、apply、bind 無關。

此題,obj1.print 返回一個箭頭函數,此箭頭函數中的 this 就是 obj1.print 調用時的 this。

  1. obj1.print()():此時obj1.print 中的 this 爲 obj1,因此輸出爲 obj1_name
  2. obj1.print().call(obj2):此時obj1.print 中的 this 爲 obj1,因此輸出爲 obj1_name
  3. obj1.print.call(obj2)():此時obj1.print 中的 this 爲 obj2,因此輸出爲 obj2_name

參考

寫在最後

  • 文中若有錯誤,歡迎在評論區指正,若是這篇文章幫到了你,歡迎點贊關注
  • 本文同步首發與github,可在github中找到更多精品文章,歡迎Watch & Star ★
  • 後續文章參見:計劃

歡迎關注微信公衆號【前端小黑屋】,每週1-3篇精品優質文章推送,助你走上進階之旅

相關文章
相關標籤/搜索