別讓你的偏心拖了後腿:快擁抱箭頭函數吧!

別讓你的偏心拖了後腿:快擁抱箭頭函數吧!

題圖:"錨"——錨212——(CC BY-NC-ND 2.0)

我以教 JavaScript 爲生。最近我給學生上了柯里化箭頭函數這個課程——這仍是最開始的幾節課。我認爲它是一個很好用的技能,所以將這個內容提到了課程的前面。而學生們沒有讓我失望,比我想象中地更快地掌握了使用箭頭函數進行柯里化。javascript

若是學生們可以理解它,而且能儘快由它獲益,爲何不早點將箭頭函數教給他們呢?前端

Note:個人課程並不適合那些歷來沒有接觸過代碼的人。大多數學生在加入咱們的課程以前至少有幾個月的編程經歷——不管他們是自學,仍是經過培訓班學習,或者自己就是專業的。然而,我發現許多隻有一點經驗或者沒有經驗的年輕開發者們可以很快地接受這些主題。java

我看到不少的學生在上了 1 小時的課以後就能很熟練地使用箭頭函數工做了。(若是你是「和 Eric Elliott 一塊兒學習 JavaScript」培訓班的同窗,你能夠看這個約 55 分鐘的視頻——ES6 的柯里化與組合)。react

看到學生們如此之快地掌握與應用他們新發現的柯里化方法,我想起了我在推特上發了柯里化箭頭函數的帖子,而後被一羣人噴「可讀性差」的事。我很驚訝爲何他們會堅持這個觀點。android

首先,咱們先來看看這個例子。我在推特發了這個函數,而後我發現有人強烈反對這種寫法:ios

const secret = msg => () => msg;複製代碼

我對有人在推特上指責我在誤導別人感到難以想象。我寫這個函數是爲了示範在 ES6 中寫柯里化函數是多麼的簡單。它是我能想到的在 JavaScript 中最簡單的實際運用與閉包表達式了。(相關閱讀:什麼是閉包git

它和下面的函數表達式等價:es6

const secret = function (msg) {
  return function () {
    return msg;
  };
};複製代碼

secret() 是一個函數,它須要傳入 msg 這個參數,而後會返回一個新的函數,這個函數將會返回 msg 的值。不管你向 secret() 中傳入什麼值,它都會利用閉包固定 msg 的值。github

你能夠這麼用它:編程

const mySecret = secret('hi');
mySecret(); // 'hi'複製代碼

事實證實,雙箭頭並無讓人感到困惑。我堅信:

對於熟悉的人來講,單行的箭頭函數是 JavaScript 表達柯里化函數最具備可讀性的方法了。

有許多人指責我,告訴我將代碼寫的長一些比簡短的代碼更容易閱讀。他們有時也許是對的,可是大多數狀況都錯了。更長、更詳細的代碼不必定更容易閱讀——至少,對熟悉箭頭函數的人來講就是如此。

我在推特上看到的持反對意見的人,並無像個人學生同樣享受平滑的學習箭頭函數的過程。在個人經驗裏,學生學習柯里化箭頭函數就像魚在水裏生活同樣。僅僅學了幾天,他們就開始使用箭頭了。它幫助學生們輕鬆地跨過了各類編程問題的鴻溝。

我沒有看到學習、閱讀、理解箭頭函數對那些學生形成了任何的「困難」——一旦他們決定學習,只要上個大概一小時的課就能基本掌握。

他們可以很輕鬆地讀懂柯里化箭頭函數,儘管他們歷來沒有見過這類的東西,他們仍是可以告訴我這些函數作了什麼事。當我給他們佈置任務後他們也可以很自如地本身完成任務。

從另外一方面說,他們可以很快熟悉柯里化箭頭函數,而且沒有爲此產生任何問題。他們閱讀這些函數就像你讀一句話同樣,他們對其的理解讓他們寫出了更簡單、更少 bug 的代碼。

爲何一些人認爲傳統的函數表達式看起來「更具備可讀性」?

偏心是一種顯著的人類認知誤差,它會讓咱們在有更好的選擇的狀況下作出自暴自棄的選擇。咱們會所以無視更舒服更好的方法,習慣性地選用之前使用過的老方法。

你能夠從這本書中更詳細地瞭解「偏心」這種心理:《The Undoing Project: A Friendship that Changed Our Minds》(不少狀況都是咱們自欺欺人)。每一個軟件工程師都應該讀一讀這本書,由於它會鼓勵你辯證地去看待問題,以及鼓勵你多對假設進行實驗,以避免掉入各類認知陷阱中。書中那些發現認知陷阱的故事也頗有趣。

傳統的函數表達式可能會在你的代碼中致使 Bug 的出現

今天我用 ES5 的語法重寫了一個 ES6 寫的柯里化箭頭函數,以便發佈開源模塊讓人們無需編譯就能在老瀏覽器中用。然而 ES5 版本讓我震驚。

ES6 版本的代碼很是簡短、簡介、優雅——僅僅只須要 4 行。

我以爲,這件事能夠發個推特,告訴你們箭頭函數是一種更加優越的實現,是時候如同放棄本身的壞習慣同樣,放棄傳統函數表達式的寫法了。

因此我發了一條推特:

爲了防止你看不清圖片,下面貼上這個函數的文本:

// 使用箭頭函數柯里化
const composeMixins = (...mixins) => (
  instance = {},
  mix = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x)
) => mix(...mixins)(instance);
// 對比一下 ES5 風格的代碼:
var composeMixins = function () {
  var mixins = [].slice.call(arguments);
  return function (instance, mix) {
    if (!instance) instance = {};
    if (!mix) {
      mix = function () {
        var fns = [].slice.call(arguments);
        return function (x) {
          return fns.reduce(function (acc, fn) {
            return fn(acc);
          }, x);
        };
      };
    }
    return mix.apply(null, mixins)(instance);
  };
};複製代碼

這裏的函數封裝了一個 pipe(),它是標準的函數式編程的工具函數,一般用於組合函數。這個 pipe() 函數在 lodash 中是 lodash/flow,在 Ramda 中是 R.pipe(),在一些函數式編程語言中它甚至自己就是一個運算符號。

每一個熟悉函數式編程的人都應該很熟悉它。它的實現主要依賴於Reduce

在這個例子中,它用來組合混合函數,不過這點可有可無(有專門寫這方面的博客文章)。咱們須要注意是如下幾個重要的細節:

這個函數能夠將任何數量的函數混合,最終返回一個函數,這個函數在管道中應用了其它的函數——就像流水線同樣。每一個混合函數都將實例(instance)做爲輸入,而後在將本身傳遞給管道中下一個函數以前,將一些變量傳入。

若是你沒有傳入 instance,它將會爲你建立一個新的對象。

有時你可能會想用別的混合方式。例如,使用 compose() 代替 pipe() 來傳遞函數,讓組合順序反過來。

若是你不須要自定義函數混合時的行爲,你能夠簡單地使用默認設定,使用 pipe() 來完成過程。

事實

除了可讀性的區別以外,如下列舉了一些與這個例子有關的客觀事實

  • 我有多年的 ES5 與 ES6 編程經驗,不管是箭頭函數表達式仍是別的函數表達式我都很熟悉。所以「偏心」對我來講不是一個變化無常的因素。
  • 我沒幾秒就寫好了 ES6 版本的代碼,它沒有任何 bug(它經過了全部的單元測試,所以我敢確定這點)。
  • 寫 ES5 版本的代碼花了我好幾分鐘。一個是幾秒,一個是幾分鐘,差距仍是挺大的。寫 ES5 代碼時,我有 2 次弄錯了函數的做用範圍;寫出了 3 個 bug,而後要花時間去分別調試與修復;還有 2 次我不得不使用 console.log() 來弄清函數執行的狀況。
  • ES6 版本代碼僅僅只有 4 行。
  • ES5 版本代碼有 21 行(其中真正有代碼的有 17 行)。
  • 儘管 ES5 版本的代碼更加冗長,可是它比起 ES6 版本的代碼來講仍然缺乏了一些信息。它雖然長,可是表達的東西更少。這個問題在後面會提到。
  • ES6 版本代碼在代碼中有 2 個 speard 運算符。而 ES5 版本代碼中沒有這個運算符,而是使用了意義晦澀arguments 對象,它將嚴重影響函數內容的可讀性。(不推薦緣由之一)
  • ES6 版本代碼在函數片斷中定義了 mix 的默認值,由此你能夠很清楚地看到它是參數的值。而 ES5 版本代碼卻混淆了這個細節問題,將它隱藏在函數體中。(不推薦緣由之二)
  • ES6 版本代碼僅有 2 層代碼塊,這將會幫助讀者理解代碼結構,以及知道如何去閱讀這個代碼。而 ES5 代碼有 6 層代碼塊,複雜的層級結構會讓函數結構的可讀性變得不好。(不推薦緣由之三)

在 ES5 版本代碼中,pipe() 佔據了函數體的大部份內容——要把它們放到同一行中去簡直是個荒唐的想法。很是有必要pipe() 這個函數單獨抽離出來,讓咱們的 ES5 版本代碼更具備可讀性:

var pipe = function () {
  var fns = [].slice.call(arguments);

  return function (x) {
    return fns.reduce(function (acc, fn) {
      return fn(acc);
    }, x);
  };
};

var composeMixins = function () {
  var mixins = [].slice.call(arguments);

  return function (instance, mix) {
    if (!instance) instance = {};
    if (!mix) mix = pipe;

    return mix.apply(null, mixins)(instance);
  };
};複製代碼

這樣,我以爲它更具可讀性,而且更容易理解它的意思了。

讓咱們看看若是咱們對 ES6 版本代碼作一些可讀性「優化」會怎麼樣:

const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);

const composeMixins = (...mixins) => (
  instance = {},
  mix = pipe
) => mix(...mixins)(instance);複製代碼

就像 ES5 版本代碼的優化同樣,這個「優化」後的代碼更加冗長(它加入了以前沒有的新變量)。與 ES5 版本代碼不一樣,這個版本在將管道的概念抽象出來後並無明顯的提升代碼可讀性。不過畢竟函數裏已經清楚的寫明瞭 mix 這個變量,它仍是更容易讓人理解一些。

mix 的定義自己在它的那一行就已經存在了,它不太可能會讓閱讀代碼的人找不到什麼時候結束 mix、剩下的代碼什麼時候執行。

而如今咱們用了 2 個變量來表示同一個東西。咱們所以而獲益了嗎?徹底沒有。

那麼爲何 ES5 函數在對函數進行抽象以後會變得更具可讀性呢?

由於以前 ES5 版本的代碼明顯更復雜。這種複雜度的來源是咱們討論的問題重點。我能夠斷言,它的複雜度的來源歸根結底就是語法干擾,這種語法干擾只會讓函數的自己含義變得費解,並無別的用處。

讓咱們換種方法,把一些多餘的變量去掉,在例子中都使用 ES6 代碼,只比較箭頭函數傳統函數表達式

var composeMixins = function (...mixins) {
  return function ( instance = {}, mix = function (...fns) {
      return function (x) {
        return fns.reduce(function (acc, fn) {
          return fn(acc);
        }, x);
      };
    }
  ) {
    return mix(...mixins)(instance);
  };
};複製代碼

如今,至少我以爲它的可讀性顯著的提高了。咱們利用 rest 語法以及默認參數語法對它進行了修改。固然,你得對 rest 語法和默認參數語法很熟悉纔會以爲這個版本的代碼更可讀。不過即便你不瞭解這些,我以爲這個版本也會看起來更加有條理

如今已經改進了許多了,可是我以爲這個版本仍是比較簡潔。將 pipe() 抽象出來,寫到它本身的函數裏可能會有所幫助

const pipe = function (...fns) {
  return function (x) {
    return fns.reduce(function (acc, fn) {
      return fn(acc);
    }, x);
  };
};

// 傳統函數表達式
const composeMixins = function (...mixins) {
  return function ( instance = {}, mix = pipe ) {
    return mix(...mixins)(instance);
  };
};複製代碼

這樣是否是更好了?如今 mix 只佔了單獨的同樣,函數結構也更加的清晰——可是這樣作不符合個人胃口,它的語法干擾實在是太多了。在如今的 composeMixins() 中,我以爲描述一個函數在哪結束、另外一個函數從哪開始還不夠清楚。

除了調用函數體以外,funcion 這個關鍵字彷佛和其它的代碼混淆在一塊兒了。個人函數的真正的功能被隱藏了起來!參數的調用和函數體的起始到底在哪裏?若是我仔細看也可以分析出來,可是它對我來講實在是不容易閱讀。

那麼若是咱們去掉 function 這個關鍵字,而後經過一個大箭頭 => 指向返回值來代替 return 關鍵字,避免它們和其它關鍵部分混在一塊兒,如今會怎麼樣呢?

咱們固然能夠這麼作,代碼會是這樣的:

const composeMixins = (...mixins) => (
  instance = {},
  mix = pipe
) => mix(...mixins)(instance);複製代碼

如今應該能夠很清楚這段代碼作了什麼事了。composeMixins() 是一個函數,它傳入了任意數量的 mixins,最終會返回一個獲得兩個額外參數(instancemix)的函數。它返回了經過 mixins 管道組合的 instance 的結果。

還有一件事……若是咱們對 pipe() 進行一樣的優化,能夠神奇地將它寫到一行中:

const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);複製代碼

當它在一行內被定義的時候,將它抽象成一個函數這件事反而變得不那麼明瞭了。

另外請記住,這個函數在 Lodash、Ramda 以及其它庫中都有用到,可是僅僅爲了用這個函數就去 import 這些庫並非一件划得來的事。

那麼咱們本身寫一行這個函數有必要嗎?應該有的。它其實是兩個不一樣的函數,把它們分開會讓代碼更加清晰。

另外一方面,若是將其寫在一行中,當你看參數命名的時候,你就已經明瞭了其類型以及用例。咱們將它寫在一行,就以下面代碼所示:

const composeMixins = (...mixins) => (
  instance = {},
  mix = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x)
) => mix(...mixins)(instance);複製代碼

如今讓咱們回頭看看最初的函數。不管咱們後面作了什麼調整,咱們都沒有丟棄任何原本就有的信息。而且,經過在行內聲明變量和默認值,咱們還給這個函數增長了信息量,描述了這個函數是怎麼使用的以及參數值是什麼樣子的。

ES5 版本中增長的額外的代碼其實都是語法干擾。這些代碼對於熟悉柯里化箭頭函數的人來講沒有任何有用之處

只要你熟悉柯里化箭頭函數,你就會以爲最開頭的代碼更加清晰並具備可讀性,由於它沒有多餘的語法糊弄人。

柯里化箭頭函數還能減小錯誤的藏身之處,由於它能讓 bug 隱藏的部分更少。我猜測,在傳統函數表達式中必定隱藏了許多的 bug,一旦你升級使用箭頭函數就能找到並排除這些 bug。

我但願你的團隊也能支持、學習與應用 ES6 的更加簡潔的代碼風格,提升工做效率。

有時,在代碼中詳細地進行描述是正確的行爲,但一般來講,代碼越少越好。若是更少的代碼可以實現一樣的東西,可以傳達更多的信息,不用丟棄任何信息量,那麼它明顯更加優越。認知這些不一樣點的關鍵就是看它們表達的信息。若是加上的代碼沒有更多的意義,那麼這種代碼就不該該存在。這個道理很簡單,就和天然語言的風格規範同樣(不說廢話)。將這種表達風格規範應用到代碼中。擁抱它,你將能寫出更好的代碼。

一天過去,天色已黑,仍然有其它推特的回覆在說 ES6 版本的代碼更加缺少可讀性:

我只想說:是時候熟練去掌握 ES六、柯里化與組合函數了。

下一步

「與 Eric Elliott 一塊兒學習 JavaScript」會員如今能夠看這個大約 55 分鐘的視頻課程——ES6 柯里化與組合

若是你還不是咱們的會員,你可會遺憾地錯過這個機會哦!

做者簡介

Eric Elliott 是 O'Reilly 出版的《Programming JavaScript Applications》書籍、「與 Eric Elliott 學習 JavaScript」課程做者。他曾經幫助 Adobe、萊美、華爾街日報、ESPN、BBC 進行軟件開發,以及幫助 Usher、Frank Ocean、Metallica 等著名音樂家作網站。

最後喂狗糧

他與世界上最美麗的女人在舊金山灣區共度一輩子。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃

相關文章
相關標籤/搜索