JavaScript函數語言程式設計(純函式、柯里化以及組合函式)

MomentYY發表於2022-02-20

JavaScript函數語言程式設計(純函式、柯里化以及組合函式)

前言

函數語言程式設計(Functional Programming),又稱為泛函程式設計,是一種程式設計正規化。早在很久以前就提出了函數語言程式設計這個概念了,而後面一直長期被物件導向程式設計所統治著,最近幾年函數語言程式設計又回到了大家的視野中,JavaScript是一門以函式為第一公民的語言,必定是支援這一種程式設計正規化的,下面就來談談JavaScript函數語言程式設計中的核心概念純函式、柯里化以及組合函式。

1.純函式

1.1.純函式的概念

對於純函式的定義,維基百科中是這樣描述的:在程式設計中,若函式符合以下條件,那麼這個函式被稱之為純函式。

  • 此函式在相同的輸入值時,需產生相同的輸出
  • 函式的輸入和輸出值以外的其他隱藏資訊或狀態無關,也和由I/O裝置產生的外部輸出無關
  • 該函式不能有語義上可觀察的函式副作用,諸如“觸發事件”,使輸出裝置輸出,或更改輸出值以外物件的內容等;

對以上描述總結就是:

  • 對於相同的輸入,永遠會得到相同的輸出;
  • 在函式的執行過程中,沒有任何可觀察的副作用;
  • 同時也不依賴外部環境的狀態;

1.2.副作用

上面提到了一個詞叫“副作用”,那麼什麼是副作用呢?

  • 通常我們所說的副作用大多數是指藥會產生的副作用;
  • 而在電腦科學中,副作用指在執行一個函式時,除了得到函式的返回值以外,還在函式呼叫時產生了附加的影響,比如修改了全域性變數的狀態,修改了傳入的引數或得到了其它的輸出內容等;

1.3.純函式案例

  • 編寫一個求和的函式sum,只要我們輸入了固定的值,sum函式就會給我們返回固定的結果,且不會產生任何副作用。

    function sum(a, b) {
      return a + b
    }
    
    const res = sum(10, 20)
    console.log(res) // 30
    
  • 以下的sum函式雖然對於固定的輸入也會返回固定的輸出,但是函式內部修改了全域性變數message,就認定為產生了副作用,不屬於純函式。

    let message = 'hello'
    function sum(a, b) {
      message = 'hi'
      return a + b
    }
    
  • 在JavaScript中也提供了許多的內建方法,有些是純函式,有些則不是。像運算元組的兩個方法slice和splice。

    • slice方法就是一個純函式,因為對於同一個陣列固定的輸入可以得到固定的輸出,且沒有任何副作用;

      const nums = [1, 2, 3, 4, 5]
      const newNums = nums.slice(1, 3)
      console.log(newNums) // [2, 3]
      console.log(nums) // [ 1, 2, 3, 4, 5 ]
      
    • splice方法不是一個純函式,因為它改變了原陣列nums;

      const nums = [1, 2, 3, 4, 5]
      const newNums = nums.splice(1, 3)
      console.log(newNums) // [ 2, 3, 4 ]
      console.log(nums) // [ 1, 5 ]
      

2.柯里化

2.1.柯里化的概念

對於柯里化的定義,維基百科中是這樣解釋的:

  • 柯里化是指把接收多個引數的函式,變成接收一個單一引數(最初函式的第一個引數)的函式,並且返回接收餘下的引數,而且返回結果的新函式的技術;
  • 柯里化聲稱“如果你固定某些引數,你將得到接受餘下引數的一個函式”

總結:只傳遞給函式一部分引數來呼叫它,讓它返回一個函式去處理剩餘的引數的過程就稱之為柯里化。

2.2.函式柯里化的過程

編寫一個普通的三值求和函式:

function sum(x, y, z) {
  return x + y + z
}

const res = sum(10, 20, 30)
console.log(res) // 60

將以上求和函式柯里化得:

  • 將傳入的三個引數進行拆解,依次返回一個函式,並傳入一個引數;
  • 在保證同樣功能的同時,其呼叫方式卻發生了變化;
  • 注意:在拆解引數時,不一定非要將引數拆成一個個的,也可以拆成2+1或1+2;
function sum(x) {
  return function(y) {
    return function(z) {
      return x + y + z
    }
  }
}

const res = sum(10)(20)(30)
console.log(res)

使用ES6箭頭函式簡寫為:

const sum = x => y => z => x + y + z

2.3.函式柯里化的特點及應用

  • 讓函式的職責更加單一。柯里化可以實現讓一個函式處理的問題儘可能的單一,而不是將一大堆邏輯交給一個函式來處理。

    • 將上面的三值求和函式增加一個需求,在計算結果之前給每個值加上2,先看看不使用柯里化的實現效果:

      function sum(x, y, z) {
        x = x + 2
        y = y + 2
        z = z + 2
        return x + y + z
      }
      
    • 柯里化的實現效果:

      function sum(x) {
        x = x + 2
        return function(y) {
          y = y + 2
          return function(z) {
            z = z + 2
            return x + y + z
          }
        }
      }
      
    • 很明顯函式柯里化後,讓我們對每個引數的處理更加單一

  • 提高函式引數邏輯複用。同樣使用上面的求和函式,增加另一個需求,固定第一個引數的值為10,直接看柯里化的實現效果吧,後續函式呼叫時第一個引數值都為10的話,就可以直接呼叫sum10函式了。

    function sum(x) {
      return function(y) {
        return function(z) {
          return x + y + z
        }
      }
    }
    
    const sum10 = sum(10) // 指定第一個引數值為10的函式
    const res = sum10(20)(30)
    console.log(res) // 60
    

2.4.自動柯里化函式的實現

function autoCurrying(fn) {
  // 1.拿到當前需要柯里化函式的引數個數
  const fnLen = fn.length

  // 2.定義一個柯里化之後的函式
  function curried_1(...args1) {
    // 2.1拿到當前傳入引數的個數
    const argsLen = args1.length

    // 2.1.將當前傳入引數個數和fn需要的引數個數進行比較
    if (argsLen >= fnLen) {
      // 如果當前傳入的引數個數已經大於等於fn需要的引數個數
      // 直接執行fn,並在執行時繫結this,並將對應的引數陣列傳入
      return fn.apply(this, args1)
    } else {
      // 如果傳入的引數不夠,說明需要繼續返回函式來接收引數
      function curried_2(...args2) {
        // 將引數進行合併,遞迴呼叫curried_1,直到引數達到fn需要的引數個數
        return curried_1.apply(this, [...args1, ...args2])
      }

      // 返回繼續接收引數函式
      return curried_2
    }
  }

  // 3.將柯里化的函式返回
  return curried_1
}

測試:

function sum(x, y, z) {
  return x + y + z
}

const curryingSum = autoCurrying(sum)

const res1 = curryingSum(10)(20)(30)
const res2 = curryingSum(10, 20)(30)
const res3 = curryingSum(10)(20, 30)
const res4 = curryingSum(10, 20, 30)
console.log(res1) // 60
console.log(res2) // 60
console.log(res3) // 60
console.log(res4) // 60

3.組合函式

組合函式(Compose Function)是在JavaScript開發過程中一種對函式的使用技巧、模式。對某一個資料進行函式呼叫,執行兩個函式,這兩個函式需要依次執行,所以需要將這兩個函式組合起來,自動依次呼叫,而這個過程就叫做函式的組合,組合形成的函式就叫做組合函式。

需求:對一個數字先進行乘法運算,再進行平方運算。

  • 一般情況下,需要先定義兩個函式,然後再對其依次呼叫:

    function double(num) {
      return num * 2
    }
    function square(num) {
      return num ** 2
    }
    
    const duobleResult = double(10)
    const squareResult = square(duobleResult)
    console.log(squareResult) // 400
    
  • 實現一個組合函式,將duoble和square兩個函式組合起來:

    function composeFn(fn1, fn2) {
      return function(num) {
        return fn2(fn1(num))
      }
    }
    
    const execFn = composeFn(double, square)
    const res = execFn(10)
    console.log(res) // 400
    

實現一個自動組合函式的函式:

function autoComposeFn(...fns) {
  // 1.拿到需要組合的函式個數
  const fnsLen = fns.length

  // 2.對傳入的函式進行邊界判斷,所有引數必須為函式
  for (let i = 0; i < fnsLen; i++) {
    if (typeof fns[i] !== 'function') {
      throw TypeError('The argument passed must be a function.')
    }
  }

  // 3.定義一個組合之後的函式
  function composeFn(...args) {
    // 3.1.拿到第一個函式的返回值
    let result = fns[0].apply(this, args)

    // 3.1.判斷傳入的函式個數
    if (fnsLen === 1) {
      // 如果傳入的函式個數為一個,直接將結果返回
      return result
    } else {
      // 如果傳入的函式個數 >= 2
      // 依次將函式取出進行呼叫,將上一個函式的返回值作為引數傳給下一個函式
      // 從第二個函式開始遍歷
      for (let i = 1; i < fnsLen; i++) {
        result = fns[i].call(this, result)
      }

      // 將結果返回
      return result
    }
  }

  // 4.將組合之後的函式返回
  return composeFn
}

測試:

function double(num) {
  return num * 2
}
function square(num) {
  return num ** 2 
}

const composeFn = autoComposeFn(double, square)
const res = composeFn(10)
console.log(res) // 400

相關文章