何時你不能使用箭頭函數?

共 2670 字,讀完需 5 分鐘。編譯自 Dmitri Pavlutin文章,對原文內容作了精簡和代碼風格優化。ES6 中引入的箭頭函數可讓咱們寫出更簡潔的代碼,可是部分場景下使用箭頭函數會帶來嚴重的問題,有哪些場景?會致使什麼問題?該怎麼解決,容我慢慢道來。javascript

能見證天天在用的編程語言不斷演化是一件讓人很是興奮的事情,從錯誤中學習、探索更好的語言實現、創造新的語言特性是推進編程語言版本迭代的動力。JS 近幾年的變化就是最好的例子, 以 ES6 引入的箭頭函數(arrow functions)、class 等特性爲表明,把 JS 的易用性推到了新的高度。html

關於 ES6 中的箭頭函數,網上有不少文章解釋其做用和語法,若是你剛開始接觸 ES6,能夠從這裏開始。任何事物都具備兩面性,語言的新特性經常被誤解、濫用,好比箭頭函數的使用就存在不少誤區。接下來,筆者會經過實例介紹該避免使用箭頭函數的場景,以及在這些場景下該如何使用函數表達式(function expressions)、函數聲明或者方法簡寫(shorthand method)來保障代碼正確性和可讀性。java

1. 定義對象方法

JS 中對象方法的定義方式是在對象上定義一個指向函數的屬性,當方法被調用的時候,方法內的 this 就會指向方法所屬的對象。es6

1.1 定義字面量方法

由於箭頭函數的語法很簡潔,可能很多同窗會忍不住用它來定義字面量方法,好比下面的例子 JS Binweb

const calculator = {
    array: [1, 2, 3],
    sum: () => {
        console.log(this === window); // => true
        return this.array.reduce((result, item) => result + item);
    }
};

console.log(this === window); // => true

// Throws "TypeError: Cannot read property 'reduce' of undefined"
calculator.sum();複製代碼

calculator.sum 使用箭頭函數來定義,可是調用的時候會拋出 TypeError,由於運行時 this.array 是未定義的,調用 calculator.sum 的時候,執行上下文裏面的 this 仍然指向的是 window,緣由是箭頭函數把函數上下文綁定到了 window 上,this.array 等價於 window.array,顯而後者是未定義的。express

解決的辦法是,使用函數表達式或者方法簡寫(ES6 中已經支持)來定義方法,這樣能確保 this 是在運行時是由包含它的上下文決定的,修正後的代碼以下 JS Bin編程

const calculator = {
    array: [1, 2, 3],
    sum() {
        console.log(this === calculator); // => true
        return this.array.reduce((result, item) => result + item);
    }
};
calculator.sum(); // => 6複製代碼

這樣 calculator.sum 就變成了普通函數,執行時 this 就指向 calculator 對象,天然能獲得正確的計算結果。瀏覽器

1.2 定義原型方法

一樣的規則適用於原型方法(prototype method)的定義,使用箭頭函數會致使運行時的執行上下文錯誤,好比下面的例子 JS Bin編程語言

function Cat(name) {
    this.name = name;
}

Cat.prototype.sayCatName = () => {
    console.log(this === window); // => true
    return this.name;
};

const cat = new Cat('Mew');
cat.sayCatName(); // => undefined複製代碼

使用傳統的函數表達式就能解決問題 JS Bin函數

function Cat(name) {
    this.name = name;
}

Cat.prototype.sayCatName = function () {
    console.log(this === cat); // => true
    return this.name;
};

const cat = new Cat('Mew');
cat.sayCatName(); // => 'Mew'複製代碼

sayCatName 變成普通函數以後,被調用時的執行上下文就會指向新建立的 cat 實例。

2. 定義事件回調函數

this 是 JS 中很強大的特性,能夠經過多種方式改變函數執行上下文,JS 內部也有幾種不一樣的默認上下文指向,但普適的規則是在誰上面調用函數 this 就指向誰,這樣代碼理解起來也很天然,讀起來就像在說,某個對象上正在發生某件事情。

可是,箭頭函數在聲明的時候就綁定了執行上下文,要動態改變上下文是不可能的,在須要動態上下文的時候它的弊端就凸顯出來。好比在客戶端編程中常見的 DOM 事件回調函數(event listenner)綁定,觸發回調函數時 this 指向當前發生事件的 DOM 節點,而動態上下文這個時候就很是有用,好比下面這段代碼試圖使用箭頭函數來做事件回調函數 JS Bin

const button = document.getElementById('myButton');
button.addEventListener('click', () => {
    console.log(this === window); // => true
    this.innerHTML = 'Clicked button';
});複製代碼

在全局上下文下定義的箭頭函數執行時 this 會指向 window,當單擊事件發生時,瀏覽器會嘗試用 button 做爲上下文來執行事件回調函數,可是箭頭函數預約義的上下文是不能被修改的,這樣 this.innerHTML 就等價於 window.innerHTML,然後者是沒有任何意義的。

使用函數表達式就能夠在運行時動態的改變 this,修正後的代碼 JS Bin

const button = document.getElementById('myButton');
button.addEventListener('click', function() {
    console.log(this === button); // => true
    this.innerHTML = 'Clicked button';
});複製代碼

當用戶單擊按鈕時,事件回調函數中的 this 實際指向 button,這樣的 this.innerHTML = 'Clicked button' 就能按照預期修改按鈕中的文字。

3. 定義構造函數

構造函數中的 this 指向新建立的對象,當執行 new Car() 的時候,構造函數 Car 的上下文就是新建立的對象,也就是說 this instanceof Car === true。顯然,箭頭函數是不能用來作構造函數, 實際上 JS 會禁止你這麼作,若是你這麼作了,它就會拋出異常。

換句話說,箭頭構造函數的執行並無任何意義,而且是有歧義的。好比,當咱們運行下面的代碼 JS Bin

const Message = (text) => {
    this.text = text;
};
// Throws "TypeError: Message is not a constructor"
const helloMessage = new Message('Hello World!');複製代碼

構造新的 Message 實例時,JS 引擎拋了錯誤,由於 Message 不是構造函數。在筆者看來,相比舊的 JS 引擎在出錯時悄悄失敗的設計,ES6 在出錯時給出具體錯誤消息是很是不錯的實踐。能夠經過使用函數表達式或者函數聲明 來聲明構造函數修復上面的例子 JS Bin

const Message = function(text) {
    this.text = text;
};
const helloMessage = new Message('Hello World!');
console.log(helloMessage.text); // => 'Hello World!'複製代碼

4. 追求太短的代碼

箭頭函數容許你省略參數兩邊的括號、函數體的花括號、甚至 return 關鍵詞,這對編寫更簡短的代碼很是有幫助。這讓我想起大學計算機老師給學生留過的有趣做業:看誰能使用 C 語言編寫出最短的函數來計算字符串的長度,這對學習和探索新語言特性是個不錯的法子。可是,在實際的軟件工程中,代碼寫完以後會被不少工程師閱讀,真正的 write once, read many times,在代碼可讀性方面,最短的代碼可能並不老是最好的。必定程度上,壓縮了太多邏輯的簡短代碼,閱讀起來就沒有那麼直觀,好比下面的例子 JS Bin

const multiply = (a, b) => b === undefined ? b => a * b : a * b;
const double = multiply(2);
double(3);      // => 6
multiply(2, 3); // => 6複製代碼

multiply 函數會返回兩個數字的乘積或者返回一個能夠繼續調用的固定了一個參數的函數。代碼看起來很簡短,但大多數人第一眼看上去可能沒法當即搞清楚它幹了什麼,怎麼讓這段代碼可讀性更高呢?有不少辦法,能夠在箭頭函數中加上括號、條件判斷、返回語句,或者使用普通的函數 JS Bin

function multiply(a, b) {
    if (b === undefined) {
        return function (b) {
            return a * b;
        }
    }
    return a * b;
}

const double = multiply(2);
double(3); // => 6
multiply(2, 3); // => 6複製代碼

爲了讓代碼可讀性更高,在簡短和囉嗦之間把握好平衡是很是有必要的。

5. 總結

箭頭函數無疑是 ES6 帶來的重大改進,在正確的場合使用箭頭函數能讓代碼變的簡潔、短小,但某些方面的優點在另一些方面可能就變成了劣勢,在須要動態上下文的場景中使用箭頭函數你要格外的當心,這些場景包括:定義對象方法、定義原型方法、定義構造函數、定義事件回調函數。

One More Thing

本文做者王仕軍,商業轉載請聯繫做者得到受權,非商業轉載請註明出處。若是你以爲本文對你有幫助,請點贊!若是對文中的內容有任何疑問,歡迎留言討論。想知道我接下來會寫些什麼?歡迎訂閱個人掘金專欄

相關文章
相關標籤/搜索