【譯】函數式 JavaScript:你們所關心的高階函數

原文:FUNCTIONAL JAVASCRIPT: WHAT ARE HIGHER-ORDER FUNCTIONS, AND WHY SHOULD ANYONE CARE?javascript

聲明:翻譯原文從國外知名博客網站上獲取,並利用業餘時間進行翻譯。若是發現網絡上有其餘譯文,多是由於開始翻譯時沒有發現已存在譯文或是感受原譯文翻譯質量不佳而從新翻譯。不論出於哪類緣由,本譯文不會包含任何抄襲行爲。html


譯者序:

當前,面嚮對象語言盛行,不少人以爲函數式編程只存在於一些偏門語言中,並在特定的需求下使用。如今不少語言都引入了函數式編程的特性,並吸納其優勢,如咱們最熟悉的 JDK,JSDK8 已經引入了函數式編程的一些特性。而對於前端開發者而言,函數式編程看似遙遠,其實很近。前端

JavaScript 自然支持高階函數和閉包,其實已經讓函數式編程融入到平時的工做中。哪怕沒聽過函數式編程的人,也都使用過函數式編程的方式。java

廣義地說,全部 Callback 類的調用,例如 DOM 件的監聽、數組方法(forEach、Map)等的使用,都屬於函數式編程的範疇。react

這篇文章,立足於 JavaScript 中的函數,爲你們剖析函數式編程裏最重要的高階函數,讓讀者能夠對 JavaScript 中的函數式編程有必定的瞭解。程序員

正文

「高階函數」是人們拋出的一個概念,可是你們很難解釋清楚它意味着什麼?也許你已經知道什麼是高階函數,可是你並不清楚如何在現實中使用?什麼狀況下使用?使用後產生什麼效果?甚至說,使用了高階函數之後,獲得了什麼好處?是否值得炫耀?反過來,是否會由於爛用它們形成代碼複雜度上升?算法

我我的剛好認爲高階函數是很是有用的,而事實上,我認爲它們是 JavaScript 做爲一種語言最重要的特性之一,而上面的問題,將在文中一一解答。編程

但在開始以前,讓咱們先來深刻分析一下高階函數。 爲此,文章將從「把函數賦值給變量」開始。redux

函數做爲「一等公民」

在 Javascript 中,咱們至少有三種方式編寫一個函數。首先,能夠編寫一個函數聲明,示例以下。數組

// 拿到一個 Dom 對象,並放在 li 節點裏。
function itemise(el) {
    const li = document.createElement('li');
    li.appendChild(el);
    return li;
}
複製代碼

這種方式你們應該很熟悉。 固然,也能夠將其改寫爲函數表達式。 結果以下:

const itemise = function(el) {
    const li = document.createElement('li');
    li.appendChild(el);
    return li;
}
複製代碼

還有另外一種方法來編寫相同的函數,這種方式被稱爲箭頭函數:

const itemise = (el) => {
    const li = document.createElement('li');
    li.appendChild(el);
    return li;
}
複製代碼

就目的而言,上面的三個方式實現的功能基本相同。 但請注意,最後兩個示例將函數賦值給變量。看起來並無什麼不一樣,可是不必定全部編程語言均可以把函數賦值給變量,這是一個偉大的特性。JavaScript 中的函數是「一等公民」。 也就是說,咱們能夠:

  • 將函數賦值給變量;
  • 將函數做爲參數傳遞給其餘函數;
  • 從其餘函數返回函數。

以上看起來不錯,但與高階函數有什麼關係呢?咱們先來看上面所列的後兩點。先給出「將函數做爲參數傳遞給其餘函數」的例子,咱們編寫一個能夠與DOM 元素一塊兒使用的函數。 若是運行 document.querySelectorAll(),咱們會獲得一個 NodeList 而不是一個數組。NodeList 沒有像數組那樣的 .map() 方法,因此寫一個:

// 將給定函數應用於 NodeList 中的每一個項目並返回一個數組。
function elListMap(transform, list) {
    // list 多是 NodeList,它沒有 .map(),因此咱們轉換它變爲一個數組。
    return [...list].map(transform);
}
​
// 使用 「for-listing」 類抓取頁面上的全部 span。
const mySpans = document.querySelectorAll('span.for-listing');
​
// 將每一個包裹在 <li> 元素中。這裏,咱們從新使用了以前的 itemise() 函數。
const wrappedList = elListMap(itemise, mySpans);
複製代碼

在這個例子中,咱們將 itemise 函數做爲參數傳遞給 elListMap 函數。 可是可使用 elListMap 函數來建立列表。 例如,可使用它將類添加到一組元素中。

function addSpinnerClass(el) {
    el.classList.add('spinner');
    return el;
}
​
// 找到 'loader' 類的全部 button。
const loadButtons = document.querySelectorAll('button.loader');
​
// 將 spinner 類添加到咱們找到的全部 button 上。
elListMap(addSpinnerClass, loadButtons);
複製代碼

elLlistMap 函數將一個函數做爲參數進行轉換。 這意味着能夠重用 elListMap 函數來完成一堆不一樣的任務。

如今已經看到了將函數做爲參數傳遞的示例。 可是從函數返回函數是怎麼樣的呢? 那多是什麼樣的?

從編寫常規舊函數開始。 想要列出 <li> 元素並將它們包裝在 <ul> 中。 並非那麼困難:

function wrapWithUl(children) {
    const ul = document.createElement('ul');
    return [...children].reduce((listEl, child) => {
        listEl.appendChild(child);
        return listEl;
    }, ul);
}
複製代碼

可是,若是之後有一堆段落元素要包含在 <div> 中,要怎麼辦呢? 沒問題。 能夠爲此編寫了一個函數:

function wrapWithDiv(children) {
    const div = document.createElement('div');
    return [...children].reduce((divEl, child) => {
        divEl.appendChild(child);
        return divEl;
    }, div);
}
複製代碼

這樣就能夠正常工做了。 可是這兩個功能看起來很強大。 二者之間惟一有意義的變化是建立的父元素。

如今,能夠編寫一個帶有兩個參數的函數:父元素的類型和子元素列表。 可是,還有另外一種方法能夠作到這一點。 能夠建立一個返回函數的函數。 它可能看起來像這樣:

function createListWrapperFunction(elementType) {
    // 直接返回一個函數。
    return function wrap(children) {
      // 在 wrap 函數中,能夠看到 elementType 參數。
      const parent = document.createElement(elementType);
      return [...children].reduce((parentEl, child) => {
          parentEl.appendChild(child);
          return parentEl;
      }, parent);
    }
}
複製代碼

這可能看起來有點複雜,因此分解它。 建立了一個除了返回另外一個函數以外什麼都不作的函數。 可是,返回的函數會記住 elementType 參數。 而後,當調用返回的函數時,它知道要建立什麼類型的元素。 因此,能夠像這樣建立 wrapWithUlwrapWithDiv

const wrapWithUl  = createListWrapperFunction('ul');
// wrapWithUl() 函數如今「記住」它建立了一個 ul 元素。const wrapWithDiv = createListWreapperFunction('div');
// wrapWithDiv() 函數如今「記住」它建立了一個 div 元素。
複製代碼

返回的函數「記住」某些內容具備技術名稱的業務,這稱之爲封閉。 封閉過於方便,但如今不會過多擔憂它們。

因此,咱們已經看到:

  • 爲變量分配函數;
  • 將函數做爲參數傳遞;
  • 從另外一個函數返回一個函數。

總而言之,擁有這些高級的功能是至關不錯的。但這與高階函數有什麼關係呢? 下面讓咱們看看高階函數的定義。

高階函數是什麼?

高階函數是:

A function that takes a function as an argument, or returns a function as a result(將函數做爲參數的函數,或做爲結果返回函數的函數)

聽起來有點耳熟? 在 JavaScript 中,函數是一等公民,而「高階函數」則是利用此功能創造的更復雜的函數。

高階函數的例子

一旦你開始尋找,你會看到全部高階函數中最多見的是接受函數做爲參數的函數。所以,先來看看這些常見的,隨後再去介紹一些返回函數的函數的實際示例。

接受函數做爲參數的函數

經過「回調」功能的任何地方,你都在使用高階函數。 這些在前端開發中無處不在,其中最多見的是 .addEventListener() 方法。 當想要響應事件而採起行動時,咱們會使用此功能。 例如,若是我想開發一個按鈕彈出警報:

function showAlert() {
  alert('Fallacies do not cease to be fallacies because they become fashions');
}
​
document.body.innerHTML += `<button type="button" class="js-alertbtn"> Show alert </button>`;
​
const btn = document.querySelector('.js-alertbtn');
​
btn.addEventListener('click', showAlert);
複製代碼

在此示例中,咱們建立一個顯示警報的函數。 而後在頁面上添加一個按鈕。 最後,將 showAlert() 函數做爲參數傳遞給 btn.addEventListener()

當使用數組迭代方法時,也會看到高階函數。 也就是說,像 .map().filter().reduce() 這樣的方法。 這裏已經經過 elListMap() 函數看到了這種方式:

function elListMap(transform, list) {
    return [...list].map(transform);
}
複製代碼

高階函數也有助於處理延遲和時序。 setTimeout()setInterval() 函數均可以幫助管理函數執行的時間。 例如,若是想在 30 秒後刪除高亮類,可能會這樣作:

function removeHighlights() {
    const highlightedElements = document.querySelectorAll('.highlighted');
    elListMap(el => el.classList.remove('highlighted'), highlightedElements);
}
​
setTimeout(removeHighlights, 30000);
複製代碼

一樣,建立一個函數並將其做爲參數傳遞給另外一個函數。

如你所見,在 JavaScript 中常用接受函數的函數。 事實上,你可能已經使用過它們了。

返回函數的函數

返回函數的函數不像接受函數的函數那樣常見。 但它們仍然有用。 其中一個最有用的例子是 maybe() 函數。 我從 Reginald Braithewaite 的 JavaScript Allongé 改編了這個。 它看起來像這樣:

function maybe(fn) return function _maybe(...args) {
        // 注意,== 是故意的。
        if ((args.length === 0) || args.some(a => (a == null)) {
            return undefined;
        }
        return fn.apply(this, args);
    }
}
複製代碼

如今先看看如何使用它,而不是解釋它如何工做。 再次查看函數 elListMap()

// 將給定函數應用於 NodeList 中的每一個項目並返回一個數組。
function elListMap(transform, list) {
    // list 多是 NodeList,它沒有 .map(),因此咱們轉換它變爲一個數組。.
    return [...list].map(transform);
}

複製代碼

若是將 null 或未定義的值傳遞給 elListMap() 會發生什麼? 會獲得一個 TypeError,不管作什麼都會崩潰。 maybe() 函數能夠解決這個問題。 這樣使用它:

const safeElListMap = maybe(elListMap);
safeElListMap(x => x, null);
// ← undefined
複製代碼

該函數返回 undefined,而不是一切都崩潰。 若是將它傳遞給另外一個受 maybe() 保護的函數,它將再次返回 undefined。 能夠繼續使用 maybe() 來保護咱們喜歡的任何數量的函數。 比編寫一個無數的 if 語句簡單得多。

返回函數的函數在 React 社區中也很常見。 例如,來自 react-redux 的 connect() 是一個返回函數的函數。

接下來是什麼

前文,咱們已經看到了一些高階函數的例子。 但又怎麼樣呢? 它們賦予咱們什麼能力?沒有它們,咱們會失去什麼? 有比通常更大的示例嗎?

要回答這個問題,讓咱們再看一個例子,內置數組方法 .sort()。(雖然和通常的高階函數不同,它會改變數組而不是返回一個新數組, 可是讓咱們暫時忽略這點。) .sort() 方法是一個高階函數,它須要一個函數做爲其參數之一。

它是如何工做的? 若是想對一組數字進行排序,首先要建立一個比較功能的函數,它可能看起來像這樣:

function compareNumbers(a, b) {
    if (a === b) return 0;
    if (a > b)   return 1;
    /* else */   return -1;
}
複製代碼

而後,爲了對數組進行排序,能夠這樣使用它:

let nums = [7, 3, 1, 5, 8, 9, 6, 4, 2];
nums.sort(compareNumbers);
console.log(nums);
// => [1, 2, 3, 4, 5, 6, 7, 8, 9]

複製代碼

這裏能夠對數字列表進行排序。 但有多大用處呢? 多久有一個須要排序的數字列表? 其實不常見。 可是我常常須要對一組對象進行排序,例如這樣的數組:

let typeaheadMatches = [
    {
        keyword: 'bogey',
        weight: 0.25,
        matchedChars: ['bog'],
    },
    {
        keyword: 'bog',
        weight: 0.5,
        matchedChars: ['bog'],
    },
    {
        keyword: 'boggle',
        weight: 0.3,
        matchedChars: ['bog'],
    },
    {
        keyword: 'bogey',
        weight: 0.25,
        matchedChars: ['bog'],
    },
    {
        keyword: 'toboggan',
        weight: 0.15,
        matchedChars: ['bog'],
    },
    {
        keyword: 'bag',
        weight: 0.1,
        matchedChars: ['b', 'g'],
    }
];
複製代碼

想象一下,想要按每一個條目的權重對此數組進行排序。 咱們能夠從頭開始編寫新的排序功能,但並不須要。 相反,咱們能夠跟據以前的函數建立一個新的比較函數。

function compareTypeaheadResult(word1, word2) {
    return -1 * compareNumbers(word1.weight, word2.weight);
}
​
typeaheadMatches.sort(compareTypeaheadResult);
console.log(typeaheadMatches);
// => [{keyword: "bog", weight: 0.5, matchedChars: ["bog"]}, … ]
複製代碼

咱們能夠爲想要的任何類型的數組編寫比較函數。 .sort() 方法彷佛與咱們達成了協議 —— 「若是你能給我一個比較函數,我會對任何數組進行排序。不要擔憂數組中的內容。若是你給我一個比較函數,我會對它進行排序。「所以,沒必要擔憂本身編寫排序算法,只須要專一於比較兩個元素的更簡單任務。

如今,想象一下,若是沒有高階函數,沒法將函數傳遞給 .sort() 方法。每當須要對不一樣類型的數組進行排序時,咱們就必須編寫一個新的排序函數。或者,最終會用函數指針或對象從新發明相同的東西。不管哪一種方式都會更加笨拙。

不過,確實有更高階的功能,這將排序功能與比較功能分開。想象一下,若是一位聰明的瀏覽器工程師出現並更新 .sort() 以使用更快的算法。不管他們排序的數組內部是什麼,每一個人的代碼都會受益。並且,如今已經有一整套高階數組函數遵循這種模式。

這帶來了更普遍的想法。 .sort() 方法抽象了對數組中的內容進行排序的任務,這就是所謂的「關注點分離」。高階函數讓咱們建立笨拙或不可能的抽象。建立抽象是軟件工程的 80%。

每當重構代碼以消除重複時,咱們就會建立抽象。看到一個模式,並用該模式的抽象表示來替換它。所以,代碼變得更簡潔,更容易理解。至少,這就是其中一個方式。

高階函數是建立抽象的強大工具,而且有一個與抽象相關的整個數學領域,它被稱爲 類屬理論(範疇論)。其更準確的表述是,類屬理論是用於發現抽象的抽象。換句話說,它是用於尋找模式的模式。在過去的70年左右,聰明的程序員一直在借鑑它們的想法,這些想法主要表現爲編程語言功能和庫。若是學習這些模式的模式,有時候能夠刪除整個代碼,或者將複雜問題簡化爲多個簡單構建塊的優雅組合。這些構建塊就是高階函數。上面所說就是高階函數很重要的緣由,由於有了它們,就有用了一個能對抗代碼中複雜性的強大工具。

結語

若是你想了解有關高階函數的更多信息,請參考如下內容:

由於 JavaScript 已經支持了高階函數,避免了考慮使用方式的問題,讓咱們能夠很容易使用高階函數的方式去實現、優化一些功能。而你們在瞭解這些以後,會發現高階函數並不複雜,它很方便地幫咱們去完成一些事情。

可是,在這個看似簡單的高階函數背後,包含着函數式編程的思想、理論和範式。當你步入這個領域,你會發現它如此強大。

相關文章
相關標籤/搜索