JavaScript進階之函式柯里化

EdwardXuan發表於2019-04-17

前言

函式柯里化(currying)就是將使用多個引數的函式轉換成一系列使用一個引數的函式的技術。
柯里化讓我們把注意力集中到函式本身,而不是冗長的資料引數之中,使程式碼更簡潔,幫助我們寫出Pointfree的程式。

概述

首先我們具象一下柯里化的概念。假設有一個接收3個引數的函式A

function A(a,b,c){
  //todo something
}
複製程式碼

如果我們使用一個柯里化轉換函式createCurrying,這個函式接受函式作為引數,並返回函式

const _A = createCurrying(A);
複製程式碼

函式_A可以接受1個或者多個引數,當總計的引數等於函式定義的引數個數時,輸出結果。如下所示:

_A(1,2,3);
_A(1)(2)(3);
_A(1)(2,3);
_A(1,2)(3);
複製程式碼

上述結果一次或者多次呼叫函式_A都返回相同的結果。
那麼我們將createCurrying稱為對函式A的柯里化

先撇開createCurrying,我們先對一個簡單的函式進行改造,讓其滿足我們的柯里化要求,如下:

function add(a,b){
  return a+b;
}
console.log(add(1+2)); //輸出3
function _add(a){
	return function(b){
  	return a+b;
  }
}
console.log(_add(1)(2));//輸出3
console.log(_add(2)(1));//輸出3
複製程式碼

上述 _add是對 add的柯里化。然而

  1. 對於引數個數少的函式,柯里化相對簡單,但是一旦引數增多,手動去柯里化不太現實,
  2. 我們也需要一個工具函式createCurrying,去柯里化我們任意一個函式,使用者只需要專注函式業務的實現

實現

上述主要闡述了柯里化的概念,實現效果。本節我們來實現createCurrying。
在實現之前,我們需要了解柯里化的思想。
什麼是柯里化思想?正如概述中所說

總計的引數等於函式定義的引數個數時,輸出結果

所以柯里化思想就是一個積累函式引數的過程,引數一旦達到函式執行要求,直接返回函式結果。
此處積累函式引數的手段,我們可以使用閉包。實現函式如下:

function createCurrying(fn,...args){
  let argsLength = fn.length; //函式定義的形參個數
  return function() {
    var newArgs = args.concat([].slice.call(arguments)); //將上一次呼叫函式的引數和本次的引數合併
    if(newArgs.length >= argsLength){
      return fn.apply(this,newArgs); //如果引數和執行的函式相等,執行函式
    }
    return createCurrying.call(this,fn,...newArgs); //否則遞迴呼叫
  }
}
複製程式碼

驗證一下:

function add(a,b,c){
	return a+b+c;
}
let _add = createCurrying(add);
console.log(_add(1,2)(3));
console.log(_add(1)(2,3));
console.log(_add(1)(2)(3));
複製程式碼

效果如下:

image.png

下面是柯里化的騷氣實現,對於ES6想深入的童鞋,可以好好看一下

var curry = fn =>
    judge = (...args) =>
        args.length === fn.length? fn(...args): (...arg) => judge(...args, ...arg)
複製程式碼

我把它轉化成ES5,幫助理解。

var curry = function (fn){
   let judge = function(...args){
   	if(args.length === fn.length){
      return  fn(...args)   		
   	}else{
   		return function (...arg){
   			return judge(...args, ...arg);
   		}
   	}
   }
   return judge;
}
複製程式碼

引申

在上述實現的createCurrying還是存在缺點,即柯里化之後的函式,只支援引數的順序呼叫,如果要支援亂序,實現方式如下:

function curry(fn, args, holes) {
    length = fn.length;

    args = args || [];

    holes = holes || [];

    return function() {

        var _args = args.slice(0),
            _holes = holes.slice(0),
            argsLen = args.length,
            holesLen = holes.length,
            arg, i, index = 0;

        for (i = 0; i < arguments.length; i++) {
            arg = arguments[i];
            // 處理類似 fn(1, _, _, 4)(_, 3) 這種情況,index 需要指向 holes 正確的下標
            if (arg === _ && holesLen) {
                index++
                if (index > holesLen) {
                    _args.push(arg);
                    _holes.push(argsLen - 1 + index - holesLen)
                }
            }
            // 處理類似 fn(1)(_) 這種情況
            else if (arg === _) {
                _args.push(arg);
                _holes.push(argsLen + i);
            }
            // 處理類似 fn(_, 2)(1) 這種情況
            else if (holesLen) {
                // fn(_, 2)(_, 3)
                if (index >= holesLen) {
                    _args.push(arg);
                }
                // fn(_, 2)(1) 用引數 1 替換佔位符
                else {
                    _args.splice(_holes[index], 1, arg);
                    _holes.splice(index, 1)
                }
            }
            else {
                _args.push(arg);
            }

        }
        if (_holes.length || _args.length < length) {
            return curry.call(this, fn, _args, _holes);
        }
        else {
            return fn.apply(this, _args);
        }
    }
}
複製程式碼

應用

柯里化的作用包括提高函式引數複用,延遲計算等,一般有如下幾種應用:

偏函式

偏函式(Partial function),在python中應用較多,詳情可檢視這裡,在Javascript也可以應用,如有一個int函式,如下:

function int(chars,hex=10){
		//將字串chars轉換成以hex進位制的整數
}
int('10') //將10轉換成10進位制
int('10',2)//將10轉換成2進位制
int('10',8)//將10轉換成8進位制
複製程式碼

該函式可以將預設的數字字串轉化成10進位制整數,也可以指定hex的值。此處我們可以引申的柯里化函式,如下

let int2 = createCurrying(int,_,2);
int2('10');
let int8 = createCurrying(int,_,8);
int8('10');
複製程式碼

簡化回撥

var persons = [{name: 'kevin', age: 11}, {name: 'daisy', age: 24}]

let getProp = createCurrying(function (key, obj) {
    return obj[key]
});
let names2 = persons.map(getProp('name'))
console.log(names2); //['kevin', 'daisy']

let ages2 = persons.map(getProp('age'))
console.log(ages2); //[11,24]
複製程式碼

上述getProp經過柯里化,可以提升函式的複用性

延遲執行

原生事件監聽的方法在現代瀏覽器和IE瀏覽器會有相容問題,解決該相容性問題的方法是進行一層封裝,若不考慮柯里化函式,我們正常情況下會像下面這樣進行封裝,如下:

/*
* @param    ele        Object      DOM元素物件
* @param    type       String      事件型別
* @param    fn         Function    事件處理函式
* @param    isCapture  Boolean     是否捕獲
*/
var addEvent = function(ele, type, fn, isCapture) {
    if(window.addEventListener) {
        ele.addEventListener(type, fn, isCapture)
    } else if(window.attachEvent) {

        ele.attachEvent("on" + type, fn)
    }
}
addEvent(document.getElementById('button'), "click", function() {
            alert("function currying");
 }, false)
複製程式碼

柯里化之後,如下:

var addEvent = (function(){
    if (window.addEventListener) {
        return function(el, sType, fn, capture) {
            el.addEventListener(sType, function(e) {
                fn.call(el, e);
            }, (capture));
        };
    } else if (window.attachEvent) {
        return function(el, sType, fn, capture) {
            el.attachEvent("on" + sType, function(e) {
                fn.call(el, e);
            });
        };
    }
})();
addEvent(document.getElementById('button'), "click", function() {
            alert("function currying");
 }, false)
複製程式碼

此處使用柯里化延遲執行的特點,返回新函式,在新函式呼叫相容的事件方法。等待addEvent新函式呼叫,延遲執行。

參考文獻

場景去理解函式柯里化》
《JavaScript專題之函式柯里化》
JS中的柯里化(currying)
《前端基礎進階(八):深入詳解函式的柯里化》
《掌握JavaScript函式的柯里化》

相關文章