js 高階函數之柯里化

博客地址:https://ainyi.com/74git

定義

在計算機科學中,柯里化(Currying)是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數且返回結果的新函數的技術github

就是隻傳遞給函數某一部分參數來調用,返回一個新函數去處理剩下的參數(==閉包==)數組

經常使用的封裝成 add 函數瀏覽器

// reduce 方法
const add = (...args) => args.reduce((a, b) => a + b)

// 傳入多個參數,執行 add 函數
add(1, 2) // 3

// 假設有一個 currying 函數
let sum = currying(params)
sum(1)(3) // 4

實際應用

延遲計算

部分求和例子,說明了延遲計算的特色閉包

const add = (...args) => args.reduce((a, b) => a + b)

// 簡化寫法
function currying(func) {
  const args = []
  return function result(...rest) {
    if (rest.length === 0) {
      return func(...args)
    } else {
      args.push(...rest)
        return result
    }
  }
}

const sum = currying(add)

sum(1, 2)(3)    // 未真正求值,收集參數的和
sum(4)      // 未真正求值,收集參數的和
sum()       // 輸出 10

上面的代碼理解:先定義 add 函數,而後 currying 函數就是用==閉包==把傳入參數保存起來,當傳入參數的數量足夠執行函數時,就開始執行函數app

上面的 currying 函數是一種簡化寫法,判斷傳入的參數長度是否爲 0,若爲 0 執行函數,不然收集參數到 args 數組函數

另外一種常見的應用是 bind 函數,咱們看下 bind 的使用this

let obj = {
  name: 'Krry'
}
const fun = function () {
  console.log(this.name)
}.bind(obj)

fun() // Krry

這裏 bind 用來改變函數執行時候的上下文==this==,可是函數自己並不執行,因此本質上是延遲計算,這一點和 call / apply 直接執行有所不一樣prototype

動態建立函數

有一種典型的應用情景是這樣的,每次調用函數都須要進行一次判斷,但其實第一次判斷計算以後,後續調用並不須要再次判斷,這種狀況下就很是適合使用柯里化方案來處理rest

即第一次判斷以後,動態建立一個新函數用於處理後續傳入的參數,並返回這個新函數。固然也可使用惰性函數來處理,本例最後一個方案會介紹

咱們看下面的這個例子,在 DOM 中添加事件時須要兼容現代瀏覽器和 IE 瀏覽器(IE < 9),方法就是對瀏覽器環境進行判斷,看瀏覽器是否支持,簡化寫法以下

// 簡化寫法
function addEvent (type, el, fn, capture = false) {
  if (window.addEventListener) {
    el.addEventListener(type, fn, capture);
  }
  else if(window.attachEvent) {
    el.attachEvent('on' + type, fn);
  }
}

可是這種寫法有一個問題,就是每次添加事件都會調用作一次判斷,比較麻煩

能夠利用閉包和當即調用函數表達式(IIFE)來實現只判斷一次,後續都無需判斷

const addEvent = (function(){
  if (window.addEventListener) {
    return function (type, el, fn, capture) { // 關鍵
      el.addEventListener(type, fn, capture)
    }
  }
  else if(window.attachEvent) {
    return function (type, el, fn) { // 關鍵
      el.attachEvent('on' + type, fn)
    }
  }
})()

上面這種實現方案就是一種典型的柯里化應用,在第一次的 if...else if... 判斷以後完成第一次計算,而後動態建立返回新的函數用於處理後續傳入的參數

這樣作的好處就是以後調用以後就不須要再次調用計算了

固然可使用惰性函數來實現這一功能,原理很簡單,就是重寫函數

function addEvent (type, el, fn, capture = false) {
  // 重寫函數
  if (window.addEventListener) {
    addEvent = function (type, el, fn, capture) {
      el.addEventListener(type, fn, capture);
    }
  }
  else if(window.attachEvent) {
    addEvent = function (type, el, fn) {
      el.attachEvent('on' + type, fn);
    }
  }
  // 執行函數,有循環爆棧風險
  addEvent(type, el, fn, capture); 
}

第一次調用 addEvent 函數後,會進行一次環境判斷,在這以後 addEvent 函數被重寫,因此下次調用時就不會再次判斷環境

參數複用

咱們知道調用 toString() 能夠獲取每一個對象的類型,可是不一樣對象的 toString() 有不一樣的實現

因此須要經過 Object.prototype.toString() 來獲取 Object 上的實現

同時以 call() / apply() 的形式來調用,並傳遞要檢查的對象做爲第一個參數

例以下面這個例子

function isArray(obj) { 
  return Object.prototype.toString.call(obj) === '[object Array]';
}

function isNumber(obj) {
  return Object.prototype.toString.call(obj) === '[object Number]';
}

function isString(obj) {
  return Object.prototype.toString.call(obj) === '[object String]';
}

// Test
isArray([1, 2, 3])  // true
isNumber(123)       // true
isString('123')     // true

可是上面方案有一個問題,那就是每種類型都須要定義一個方法,這裏咱們可使用 bind 來擴展,優勢是能夠直接使用改造後的 toStr

const toStr = Function.prototype.call.bind(Object.prototype.toString);

// 改造前直接調用
[1, 2, 3].toString()    // "1,2,3"
'123'.toString()    // "123"
123.toString()      // SyntaxError: Invalid or unexpected token
Object(123).toString()  // "123"

// 改造後調用 toStr
toStr([1, 2, 3])    // "[object Array]"
toStr('123')        // "[object String]"
toStr(123)      // "[object Number]"
toStr(Object(123))  // "[object Number]"

上面例子首先使用 Function.prototype.call 函數指定一個 this 值,而後 .bind 返回一個新的函數,始終將 Object.prototype.toString 設置爲傳入參數,其實等價於 Object.prototype.toString.call()

實現 Currying 函數

能夠理解所謂的柯里化函數,就是封裝==一系列的處理步驟==,經過閉包將參數集中起來計算,最後再把須要處理的參數傳進去

實現原理就是用閉包把傳入參數保存起來,當傳入參數的數量足夠執行函數時,就開始執行函數

上面延遲計算部分已經實現了一個簡化版的 Currying 函數

下面實現一個更加健壯的 Currying 函數

function currying(fn, length) {
  // 第一次調用獲取函數 fn 參數的長度,後續調用獲取 fn 剩餘參數的長度
  length = length || fn.length
  return function (...args) { // 返回一個新函數,接收參數爲 ...args
    // 新函數接收的參數長度是否大於等於 fn 剩餘參數須要接收的長度
    return args.length >= length
        ? fn.apply(this, args) // 知足要求,執行 fn 函數,傳入新函數的參數
      : currying(fn.bind(this, ...args), length - args.length)
      // 不知足要求,遞歸 currying 函數
      // 新的 fn 爲 bind 返回的新函數,新的 length 爲 fn 剩餘參數的長度
  }
}

// Test
const fn = currying(function(a, b, c) {
  console.log([a, b, c]);
})

fn("a", "b", "c")   // ["a", "b", "c"]
fn("a", "b")("c")   // ["a", "b", "c"]
fn("a")("b")("c")   // ["a", "b", "c"]
fn("a")("b", "c")   // ["a", "b", "c"]

上面使用的是 ES5 和 ES6 的混合語法

那若是不想使用 call/apply/bind 這些方法呢,天然是能夠的,看下面的 ES6 極簡寫法,更加簡潔也更加易懂

const currying = fn =>
    judge = (...args) =>
        args.length >= fn.length
            ? fn(...args)
            : (...arg) => judge(...args, ...arg)

// Test
const fn = currying(function(a, b, c) {
    console.log([a, b, c]);
})

fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]

若是還很難理解,看下面例子

function currying(fn, length) {
  length = length || fn.length;     
  return function (...args) {           
    return args.length >= length    
        ? fn.apply(this, args)          
      : currying(fn.bind(this, ...args), length - args.length) 
  }
}

const add = currying(function(a, b, c) {
    console.log([a, b, c].reduce((a, b) => a + b))
})

add(1, 2, 3) // 6
add(1, 2)(3) // 6
add(1)(2)(3) // 6
add(1)(2, 3) // 6

擴展:函數參數 length

函數 currying 的實現中,使用了 fn.length 來表示函數參數的個數,那 fn.length 表示函數的全部參數個數嗎?並非

函數的 length 屬性獲取的是形參的個數,可是形參的數量不包括剩餘參數個數,並且僅包括第一個具備默認值以前的參數個數,看下面的例子

((a, b, c) => {}).length; // 3

((a, b, c = 3) => {}).length; // 2 

((a, b = 2, c) => {}).length; // 1 

((a = 1, b, c) => {}).length; // 0 

((...args) => {}).length; // 0

const fn = (...args) => {
  console.log(args.length);
} 
fn(1, 2, 3) // 3

因此在柯里化的場景中,不建議使用 ES6 的函數參數默認值

const fn = currying((a = 1, b, c) => {
  console.log([a, b, c])
})

fn() // [1, undefined, undefined]

fn()(2)(3) // Uncaught TypeError: fn(...) is not a function

咱們指望函數 fn 輸出 1, 2, 3,可是實際上調用柯里化函數時 ((a = 1, b, c) => {}).length === 0

因此調用 fn() 時就已經執行並輸出了 1, undefined, undefined,而不是理想中的返回閉包函數

因此後續調用 fn()(2)(3) 將會報錯

小結&連接

定義:柯里化是一種將使用多個參數的函數轉換成一系列使用一個參數的函數,而且返回接受餘下的參數並且返回結果的新函數的技術

實際應用

  1. 延遲計算:部分求和、bind 函數
  2. 動態建立函數:添加監聽 addEvent、惰性函數
  3. 參數複用:Function.prototype.call.bind(Object.prototype.toString)

實現 Currying 函數:用閉包把傳入參數保存起來,當傳入參數的數量足夠執行函數時,就開始執行函數

函數參數 length:獲取的是形參的個數,可是形參的數量不包括剩餘參數個數,並且僅包括==第一個參數有默認值以前的參數個數==

參考文章:JavaScript專題之函數柯里化

博客地址:https://ainyi.com/74

相關文章
相關標籤/搜索