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函数的柯里化》

相关文章
相关标签/搜索