JavaScript進階之函數柯里化

前言

柯里化(currying)就是將使用多個參數的函數轉換成一系列使用部分參數的函數的技術。
使用柯里化(currying)使咱們把注意力集中於函數自己,而沒必要在乎冗長的參數個數,使執行函數的代碼更簡潔,寫出Pointfree的程序。
柯里化是JavaScript函數式編程的重點,其中應用函數閉包call和apply高階函數遞歸等知識點,因此也是Javascript中的難點。
本文整理了JavaScript柯里化的基本概念,實現和應用場景。拋磚引玉一下,幫助讀者掌握函數柯里化的基本知識點,可以在實際的開發中應用起來。
原創不易,您的點贊是我繼續寫做的動力,若是文章中有紕漏和錯誤,還望指出,謝謝!javascript

概述

首先咱們具象一下柯里化的概念。假設有一個接收3個參數的函數A前端

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

若是咱們使用一個柯里化轉換函數curry,這個函數接受函數做爲參數,並返回函數java

const _A = curry(A);
複製代碼

函數_A能夠接受1個或者多個參數,當總計傳入的參數等於函數定義的參數個數時,輸出結果。以下所示:python

_A(1,2,3);
_A(1)(2)(3);
_A(1)(2,3);
_A(1,2)(3);
複製代碼

上述結果一次或者屢次調用函數_A都返回相同的結果。
那麼咱們將curry稱爲對函數A的柯里化。由於curry接受一個函數並返回一個函數,curry又稱爲高階函數
先撇開curry,咱們先對一個簡單的函數進行柯里化,以下:git

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的柯里化。然而github

  1. 對於參數個數少的函數,柯里化相對簡單,可是一旦參數增多,手動去柯里化不太現實;
  2. 咱們也須要一個工具函數curry,去柯里化咱們任意一個函數,用戶只須要專一函數業務的實現
  3. 上述柯里化以後的參數順序不必定可變,例如減法subtraction(10,1),上述柯里化以後只能subtraction(10)(1)不能subtraction(_,1)(10)

實現

上述闡述了柯里化的概念和實現效果。本節咱們來實現工具函數curry
在開始以前,咱們須要瞭解柯里化的思想。在概述中提過編程

總計傳入的參數等於函數定義的參數個數時,輸出結果瀏覽器

故柯里化思想就是一個積累函數參數,當參數個數一旦達到函數執行要求,執行函數,返回結果的過程。 閉包

積累參數的過程,正如大壩後面的水庫,積累參數的過程稱爲閉包,後面的水庫就是內存app

參數一旦到達要求,返回結果,大壩一瀉千里

因而咱們的實現函數以下:

function curry(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 curry.call(this,fn,...newArgs); //不然遞歸調用
  }
}
複製代碼

驗證一下:

function add(a,b,c){
	return a+b+c;
}
let _add = curry(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;
}
複製代碼

引伸

在上述實現的curry仍是存在缺點,即柯里化以後的函數,只支持參數的順序調用,若是要支持亂序,實現方式以下:

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)
複製代碼

此處使用IIFE,執行if...else...語句提早返回咱們須要的func,這樣後續咱們就不用每次都去判斷,提升性能。

延遲執行

主要應用有節流和防抖,能夠參見這篇文章

參考文獻

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

相關文章
相關標籤/搜索