Javascript 函數式編程:什麼是高階函數?咱們爲何須要了解它?

原文:jrsinclair.com/articles/20…
做者:James Sinclair
翻譯:前端小白javascript

高階函數是你們常常掛在嘴邊的,可是不多有人去解釋它是什麼,也許你已經知道什麼是高階函數了。可是咱們如何在現中使用它們?有哪些例子能夠告訴咱們何時使用,以及他們怎麼表現出實用性?咱們可使用他們操做DOM?或者,那些使用高階函數的人實在炫耀嗎?他們將代碼過度複雜化?html

我認爲高階函數頗有用。事實上,我認爲它們是JavaScript做爲一種語言最重要的特性之一,但在講這個以前,咱們先來分解一下什麼是高階函數,在理解這個概念以前,讓咱們從函數做爲變量開始提及。前端

函數是頭等公民

在JavaScript中,咱們至少有三種不一樣的方法來編寫函數。首先,咱們能夠寫一個函數聲明。例如:java

// 接受一個DOM元素,將他包裹在li裏面
function itemise(el) {
    const li = document.createElement('li');
    li.appendChild(el);
    return li;
}
複製代碼

但願你們都很熟悉。可是,你可能知道咱們也能夠把它寫成函數表達式。看起來是這樣的:react

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() 方法,因此咱們來寫一個:redux

// 將給定的函數應用於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' 的按鈕
const loadButtons = document.querySelectorAll('button.loader');

// 將 spinner 類名添加給全部的 button
elListMap(addSpinnerClass, loadButtons);
複製代碼

elListMap 函數接受 transform 函數做爲參數,這意味着咱們能夠重用 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 元素
複製代碼

返回的函數會「記住」某個東西,咱們有個專業的叫法:閉包。閉包很是實用,可是咱們如今還不用想太多。 因此,咱們已經看到了:

  • 將函數賦值給變量
  • 做爲參數傳遞
  • 從一個函數返回另外一個函數

總而言之,函數是頭等公民,這確實不錯。但這和高階函數有什麼關係呢?咱們來看看高階函數的定義。

什麼是高階函數

高階函數是:

將函數做爲參數傳入或做爲結果返回的函數

聽起來是否是很熟悉?在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) {
        // Note that the == is deliberate.
        if ((args.length === 0) || args.some(a => (a == null)) {
            return undefined;
        }
        return fn.apply(this, args);
    }
}
複製代碼

咱們先來看看如何使用它,而不是立刻理解它的原理,咱們繼續使用 elListMap() 函數:

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

若是咱們不當心將一個 nullundefined 值傳遞給 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年左右,聰明的程序員一直在竊取他們的想法,這些思想表現爲編程語言特性和庫。

若是咱們學習這些模式的模式,咱們有時能夠移除大片代碼。或者將複雜的問題分解爲簡單構建快之間的組合,那些構建快就是高階函數,這就是爲何高階函數很重要。由於有了它們,咱們就有了一個強大的工具來對抗代碼中的複雜性。

若是你想了解更多關於高階函數的知識,這裏有一些參考資料:

Higher-Order Functions Chapter 5 of Eloquent JavaScript by Marijn Haverbeke.
Higher-Order Functions Part of the Composing Sofware series by Eric Elliott.
Higher-Order Functions in JavaScript by M. David Green for Sitepoint.

可能你已經在使用高階函數了。JavaScript使它變得如此簡單,以致於咱們沒有過多地考慮它們。可是當人們拋出這個詞時,咱們知道這是什麼,這並不複雜。但在這看似簡單的概念背後,蘊藏着巨大的力量。

Update 3 July 2019:若是你是一名有經驗的函數式編程開發者,可能你已經注意到我使用了非純函數和一些冗長的函數名。這並非由於我不瞭解非純函數或通常函數編程原理。這不是我在生產環境中定義函數名的方式。這是一篇有教育意義的文章,因此我儘可能選擇一些初學者能理解的實際例子,做爲一種妥協。若是你有興趣,能夠看看我另外兩篇文章 functional puritygeneral functional programming principles

最後

  1. 函數有三種以上的寫法,不過咱們能夠下次再討論。
  2. 這並不老是正確的。這三種寫法在實踐中都有細微的差異。區別在於 this 關鍵字和函數調用堆棧跟蹤過程當中標籤的變化
  3. 維基百科:Wikipedia contributors (2019). ‘First–class citizen,’ Wikipedia, the free encyclopedia, viewed 19 June 2019, en.wikipedia.org/wiki/First-…
  4. 若是你想了解更多關於閉包,參考: Master the JavaScript Interview: What is a Closure? by Eric Elliott
  5. Higher Order Function (2014), viewed 19 June 2019, wiki.c2.com/?HigherOrde….
相關文章
相關標籤/搜索