轉載至 http://www.zcfy.cc/article/when-not-to-use-arrow-functions-482.html
看到你使用的編程語言每天都在不斷地進化,是一件非常高興的事情。從錯誤中學習,發現更好的實現方法,創造新的特性,讓這個過程持續迭代。
JavaScript 這些年正在發生變化,ECMAScript 6 提升了這門語言的可用性:箭頭函數,類以及更多內容。這些都很棒!
最有價值的新特性就是箭頭函數。現在有許多好文章都在描述它的上下文透明性和短語法。如果你新接觸 ES6 ,先讀讀這篇文章。
但是凡事都有兩面。通常新的特性會介紹的有些混亂,其中之一就是箭頭函數被誤用。
這篇文章通過情景引導,讓你知曉哪些情況下應該繞過箭頭函數,使用更贊成的舊的函數表達式 或者是新的短方法語法。並且注意一下短語法,因為它可能會影響代碼可讀性。
1. 在對象上定義方法
在 JavaScript 中,方法作為一個函數存儲為對象的一個屬性。當調用方法時,this
指向該方法的從屬對象。
1a. 對象字面量
自從箭頭函數有了短語法,非常吸引人用它來定義方法。讓我們試試看:
var calculate = { 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" calculate.sum();
calculate.sum
方法是通過箭頭函數定義的。但是在調用 calculate.sum()
的時候拋出了一個 TypeError
,因為 this.array
的值是 undefined
。
當調用 calculate
對象上的方法 sum()
時,上下文仍然是 window
。這個情況發生是因為箭頭函數綁定了該window
對象的詞法上下文。
執行 this.array
等同於 window.array
, 值為 undefined
。
解決方案是使用函數表達式或者方法定義的短語法 (ECMAScript 6可用)。 在這種情況下 this
決定於調用的對象,而不是緊鄰上下文。 讓我們看看修正的版本:
var calculate = { array: [1, 2, 3], sum() { console.log(this === calculate); // => true return this.array.reduce((result, item) => result + item); } }; calculate.sum(); // => 6
因為 sum
是一個普通函數,調用 calculate.sum()
的 this
是 calculate
對象。 this.array
是數組的引用,因此元素之和計算正確,結果是: 6
.
1b. 對象原型
相同的規則也適用於在 prototype
對象上定義方法。
用箭頭函數來定義 sayCatName
方法,會帶來一個不正確的上下文 window
:
function MyCat(name) { this.catName = name; } MyCat.prototype.sayCatName = () => { console.log(this === window); // => true return this.catName; }; var cat = new MyCat('Mew'); cat.sayCatName(); // => undefined
使用 保守派 函數表達式:
function MyCat(name) { this.catName = name; } MyCat.prototype.sayCatName = function() { console.log(this === cat); // => true return this.catName; }; var cat = new MyCat('Mew'); cat.sayCatName(); // => 'Mew'
sayCatName
普通函數被當做方法調用的時候 cat.sayCatName()
,會把上下文改變為 cat
對象。
2. 結合動態上下文的回調函數
this
在 JavaScript 中是一個很強大的特性。它允許利用函數調用的方式改變上下文。通常來說,上下文是一個調用發生時候的目標對象,讓代碼更加 自然化。這就好像 “某些事情正發生在該對象上”。
無論如何,箭頭函數在聲明的時候都會綁定靜態的上下文,而不會是動態的。這是詞素 this
不是很必要的一種情況。
給 DOM 元素裝配事件監聽器是客戶端編程的一個通常的任務。一個事件用 this
作為目標元素去觸發處理函數。這是一個動態上下文的簡便用法。
接下來的例子試圖使用一個箭頭函數觸發一個處理函數:
var button = document.getElementById('myButton'); button.addEventListener('click', () => { console.log(this === window); // => true this.innerHTML = 'Clicked button'; });
this
在箭頭函數中是 window
,也就是被定義為全局上下文(譯者注:這里應該描述的就是上文的例子)。當一個點擊事件發生的時候,瀏覽器試着用 button
上下文去調用處理函數,但是箭頭函數病不會改變它已經預定義的上下文。
this.innerHTML
等價於 window.innerHTML
,並沒有什么意義。
你不得不應用一個函數表達式,去允許目標元素改變其上下文。
var 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 MyFunction()
,該構造器的上下文 MyFunction
是一個新的對象: this instanceof MyFunction === true
.
注意一個箭頭函數不能作為構造器。 JavaScript 會通過拋出異常的方式進行隱式地預防。
無論怎樣,this
還是會從緊鄰上下文中獲取,而不是那個新創建的對象。 換句話說,一個箭頭函數構造器的調用過程沒有什么意義,反而會產生歧義。
讓我們看看,如果試圖去嘗試使用箭頭函數作為構造器,會發生什么:
var Message = (text) => { this.text = text; }; // Throws "TypeError: Message is not a constructor" var helloMessage = new Message('Hello World!');
執行 new Message('Hello World!')
, Message
是一個箭頭函數, JavaScript 拋出了一個 TypeError
,這意味着 Message
不能被用作於構造器。
與一些之前特定版本的 JavaScript 靜默失敗 相比,我認為 ECMAScript 6 在這些情況下提供含有錯誤信息的失敗會更加高效。
上面的例子可以使用一個 函數表達式 來修正,這才是創建構造器正確的方式 (包括 函數聲明):
var Message = function(text) { this.text = text; }; var helloMessage = new Message('Hello World!'); console.log(helloMessage.text); // => 'Hello World!'
4. 最短語法
箭頭函數有一個非常棒的屬性,如果函數體只有一條語句的話,可以省略參數的括號 ()
,代碼塊的花括號 {}
以及 return
(譯者注:此處省略參數的括號,與函數體只有一條語句沒關系。)。這對寫特別短的函數很有幫助。
我大學的編程教授布置給學生一個有趣的任務:用C語言寫一個最短的函數去統計字符串長度。這是一個學習和探索新語言非常好的途徑。
然而在真實世界中應用程序的代碼是會被許多其他的開發者進行閱讀的。最短語法不太適合去幫助你的同事快速理解函數的意義。
在某些程度上來說,壓縮的函數會變得閱讀困難,所以最好別走入憤怒的深淵。讓我們來看一個例子:
let multiply = (a, b) => b === undefined ? b => a * b : a * b; let double = multiply(2); double(3); // => 6 multiply(2, 3); // => 6
multiply
返回了兩個數字的乘積結果,或者說是一個為了接下來的乘法運算,而關聯了第一個參數的閉包。
這個函數運行的很好並且看上去很短。但是它可能第一眼看上去有點難以理解。
為了讓它更具有可讀性,可以通過給箭頭函數添加一些可選的花括號,以及 return
語句,或者是干脆用一個普通函數:
function multiply(a, b) { if (b === undefined) { return function(b) { return a * b; } } return a * b; } let double = multiply(2); double(3); // => 6 multiply(2, 3); // => 6
最好可以在短和冗長之間尋找到一個平衡點,讓你的 JavaScript 更加直接。
5. 結論
毫無疑問,箭頭函數是一個非常好的特性增強。使用正確的話,它會在很多地方帶來方便,比如早期的時候,你不得不使用 .bind()
或者 試圖去捕獲上下文。當然,它也讓代碼變得更加輕便。
在某些情況下,優勢也會帶來劣勢。當要求動態上下文的時候,你就不能使用箭頭函數,比如:定義方法,用構造器創建對象,處理時間時用 this
獲取目標。