Javascript进阶之柯里化

什么是柯里化(Currying)?什么是一元函数(Unary)?什么是偏函数(Partial)?

柯里化(Currying) 是一种函数式编程的概念,在JavaScript中可以用来创建灵活且可复用的函数。它指的是将一个接受多个参数的函数转换为一系列函数链,每个函数只接受一个单一的参数,这些函数称为一元函数(Unary)。这个过程中产生的一系列固定了部分参数的函数,就称为偏函数(Partial)

例如一个普通版本的相加函数add如下


function add (x, y) {
	return x + y;
}

或者用ES6 Lambda表达式写成


const add = (x, y) => x + y;
//调用
console.log( add (3, 5)); //8

这里add函数一次接受两个参数xy,返回它们的和。
柯里化的过程就是将add分拆成两个函数,每个函数都只接受单一参数,也就是说每个都是Unary函数。


function curriedAdd (x) {
	return function (y) {
		return x + y;
	}
}

或者用ES6 Lambda表达式写成


const curriedAdd = x => y => x + y;
//调用
const add3 = curriedAdd (3);
console.log( add3 (5)); //8
console.log( curriedAdd (5)(8));//13

把前述add函数变成curriedAdd函数的过程就是柯里化。而新函数add3是一个一元函数(Unary),同时因为它固定了参与加法的第一个参数为3,因此它也是一个偏函数(Partial)

为什么要柯里化?

柯里化能带来一系列的好处。最基本的点包括:

  1. 提高代码复用性
    这是最显然的,柯里化之后形成一系列模块化的一元函数,它们可以复用。
  2. 提高安全性,降低潜在副作用
    为讲清楚这个问题,先介绍纯函数的概念,它是指具有确定性(给定相同的输入,始终返回相同的输出),并且没有副作用(即不修改外部变量,不改变外部状态)的函数。柯里化形成一系列一元函数,这使得它们的行为简单透明易懂,通常就是或者非常接近于纯函数,这样有助于编写可预测、易于测试和易于维护的代码,可以提高系统的可靠性和健壮程度。
  3. 延迟计算
    柯里化的函数在收集到足够参数之前不会进行计算。这意味着你可以延迟某些计算直到真正需要的时候才进行,从而避免不必要的计算,节省计算资源。这个延迟计算类似于懒惰求值(Lazy Evaluation) 的机制,不过前者是针对高阶函数而言,后者通常是针对表达式。
  4. 优雅
    这也是显然的,柯里化、纯函数、Lambda表达式的组合可以让代码非常优雅。

当然缺点也是明显的,学习曲线陡峭化,代码可读性降低,这两者本质上都是函数式编程的高门槛与生俱来的副作用。此外,如果能理解在Javascript中实现柯里化对闭包的依赖(因为形参其实都是局部变量,每多一层调用,就依赖一次闭包),不难想象闭包必然伴随的内存开销副作用。

Javascript怎么实现柯里化?

JS中没有原生的柯里化方法。我们可以自己实现。


function curry (fn, acc=[]) {//fn是待柯里化的函数,acc是一个参数收集装置,初始化为空数组
	return function (...args){//args收集下一次调用给的参数,无论是多少个
		if([...acc,...args].length>=fn.length){//已经收集到足够的参数,调用fn
			return fn(...acc,...args);
		}
		else{//收集到的参数不够执行,继续迭代
			return curry(fn,[...acc,...args]);
		}
	}
};

优雅的Lambda表达式写法


const curry=(fn, acc=[])=>(...args)=>(a=>a.length>=fn.length?fn(...a):curry(fn,a))([...acc,...args]);

然后我们就可以调用这个curry方法将任意函数柯里化了


const sum = (a, b, c, d) => a + b + c + d;//一个四元求和函数sum
const curriedSum = curry(sum);//柯里化
console.log(curriedSum(1)(2)(3)(4));//10
console.log(curriedSum(1,2)(3)(4));//10
console.log(curriedSum(1)(2,3,4));//10
console.log(curriedSum(1,2,3,4));//10
console.log(curriedSum(1,2,3)(4,5,6));//输出结果仍然是10。多余参数5和6也传给了函数,但是被忽略

以上实现方法是基于递归的,递归调用可能存在栈溢出的问题,当然现代JS引擎的调用栈一般都在数千到一万个函数调用,Node.js更是可以修改调用栈的上限。
不过为了规避这个潜在问题,同时也为提升运行速度,我们可以改用迭代的方式来实现柯里化:


const curryIterative =fn=>(...args)=>{
	while(args.length<fn.length){
		return (...moreArgs)=>curryIterative(fn)(...args,...moreArgs);
	}
	return fn(...args);
} ;

测试一下


console.log(curryIterative((a,b,c)=>a*b*c)(1)(2)(5));//10
console.log(curryIterative((a,b,c)=>a*b*c)(1,2)(5,9,0));//10

//perfectly

更多版本的柯里化实现方法

版本一


const curry = function(fn){
    return function curriedFn(...args){
        if(args.length<fn.length){
            return function(){
                return curriedFn(...args.concat([...arguments]));
            }
        }
        return fn(...args);
    }
}

这个版本利用arguments数组,感觉写的一般,没有充分利用ES6+的新特性,不够优雅。

版本二


function currying (func, ...preArgs) {
  let self = this
  return function (...args) {
    return func.apply(self, [].concat(preArgs, args))
  }
};

function unCurrying (func) {
  return function (reference, ...args) {
    return func.apply(reference, args)
  }
};

这个版本还带有 反柯里化(unCurrying) 的实现。反柯里化不止是柯里化的逆向过程,还能用于扩大适用范围,例如下面的例子:


const map = unCurrying([].map);
let tags = map(document.querySelectorAll('*'), item => item.tagName);

tags = [...new Set(tags)];

这里document.querySelectorAll()方法返回的是一个NodeList对象,它不是数组,没有map方法,但是借助unCurrying,可以把map方法扩展到NodeList上调用。

版本三


const curry = fn => {
    if (typeof fn !== 'function') {
        throw new Error('No function provided!');
    }
    
    return function curriedFn(...args) {
        if (args.length < fn.length) {
            return function () {
                // 使用 concat 方法连接累积参数和新传入的参数
                const newArgs = args.concat(Array.from(arguments));
                return curriedFn.apply(null, newArgs);
            };
        }
        return fn.apply(null, args);
    };
};

这个版本带有错误检测。

版本四


const curry =fn=>curried=(...args)=>args.length>=fn.length?fn(...args):(...nextArg)=>curried(...args,...nextArg);

就一个字,优雅。

  • 8
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值