前言
柯里化(currying)就是将使用多个参数的函数
转换成一系列使用部分参数的函数
的技术。
函数闭包
,call和apply
,高阶函数
,递归
等知识点,所以也是Javascript中的难点。 本文整理了JavaScript柯里化的基本概念,实现和应用场景。抛砖引玉一下,帮助读者掌握函数柯里化的基本知识点,能够在实际的开发中应用起来。 原创不易,您的点赞是我继续写作的动力,如果文章中有纰漏和错误,还望指出,谢谢! 概述
首先我们具象一下柯里化的概念。假设有一个接收3个参数的函数A
function A(a,b,c){ //todo something}复制代码
如果我们使用一个柯里化转换函数curry
,这个函数接受函数作为参数,并返回函数
const _A = curry(A);复制代码
函数_A
可以接受1个或者多个参数,当总计传入的参数等于函数定义的参数个数时,输出结果。如下所示:
_A(1,2,3);_A(1)(2)(3);_A(1)(2,3);_A(1,2)(3);复制代码
上述结果一次或者多次调用函数_A
都返回相同的结果。
curry
称为对函数A
的柯里化。因为curry
接受一个函数并返回一个函数,curry
又称为高阶函数
。 先撇开curry
,我们先对一个简单的函数进行柯里化,如下: function add(a,b){ return a+b;}console.log(add(1+2)); //输出3function _add(a){ return function(b){ return a+b; }}console.log(_add(1)(2));//输出3console.log(_add(2)(1));//输出3复制代码
上述 _add
是对 add
的柯里化。然而
- 对于参数个数少的函数,柯里化相对简单,但是一旦参数增多,手动去柯里化不太现实;
- 我们也需要一个工具函数
curry
,去柯里化我们任意一个函数,用户只需要专注函数业务的实现- 上述柯里化之后的参数顺序不一定可变,例如减法
subtraction(10,1)
,上述柯里化之后只能subtraction(10)(1)
不能subtraction(_,1)(10)
实现
上述阐述了柯里化的概念和实现效果。本节我们来实现工具函数curry
。
总计传入的参数等于函数定义的参数个数时,输出结果
故柯里化思想就是一个积累函数参数,当参数个数一旦达到函数执行要求,执行函数,返回结果的过程。
积累参数的过程,正如大坝后面的水库,积累参数的过程称为闭包,后面的水库就是内存
参数一旦到达要求,返回结果,大坝一泻千里
于是我们的实现函数如下:
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));复制代码
效果如下:
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,这样后续我们就不用每次都去判断,提高性能。
延迟执行
主要应用有节流和防抖,可以参见这篇。