JavaScript因其灵活性和开放性而更加流行,但同时也带来了代码组织容易混乱冗余、变量与作用域容易互相影响等问题。本系列文章将通过介绍柯里化、代码组合、反柯里化等代码设计思想和方法,在函数式编程“灵活JS”的同时尽量避开JS的缺点,提升代码质量。
通过阅读本文可以了解到以下主要内容:
“柯里化”是什么?
柯里化的优点;
“通用”的柯里化函数及其包含的思想。
1 柯里化是什么?
柯里化(Currying,可译卡瑞化或加里化),这项技术以数理逻辑学家Haskell Brooks Curry命名,比较正式的解释是:
把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数并且能返回结果的新函数的技术。
太正式的解释一般比较晦涩,通俗点讲,柯里化的主要思想是在调用函数时,只传递给函数一部分参数来调用它,而它会返回一个新函数待处理剩下的参数,如下所示:
柯里化前的函数调用形式:func(x,y,z);
柯里化后:func(x)(y)(z);
这样从形态上就很清晰了吧?
2柯里化很常见
先看一个简单的示例,这是没有柯里化的版本:
/* 假设有一个需要根据c对str进行了一个复杂但可复用的逻辑处理,
这里用简单的split方法来代替说明 */
// 一般做法,这只是一个示例,实际开发中这样的封装是没有必要的
var splitStr = function (c,str) {
return str.split(c); // 根据c对字符串str进行切割
};
function myFun (x, y, z) {
var myStr = wx.cgiData.str, retStr = ' '; // ...这里假设省略了关于x,y,z的其它逻辑
retStr = splitStr('-', myStr); // 每次想要这样切分,都得在函数内配置参数'-'(这个实参本身可能是在调用过程中经常被反复传入的,这个过程很冗余)
return retStr;
}
myFun(x1, y1, z1); // 调用myFun,在调用时控制不了字符串的切割方法
看一下柯里化版:
// 这是柯里化版,在传入c调用split函数后将会返回另一个能接收字符串的函数
var split = function (c) {
return function (str) {
return str.split(c);
};
};
function myFun (x, y, z, splitFun) {
var myStr = wx.cgiData.str; // 这里假设方法中获得了一个已知字符串
// ...这里假设有关于形参x,y,z的其它逻辑
var retStr = splitFun(myStr); // 柯里化后,“split”会在调用myFun函数时就已经传递好了参数'-'
}
myFun(x1,x2,x3,split('-')); // split('-')实际上返回了能够接收str的另一个函数
看到这里,你也许会觉得,这不就是JS中闭包的常见用法么,就这个还得造一个概念出来?
没错,在JS中js切割字符串,函数可以作为参数传递,也可以作为返回值返回,这是产生“柯里化”这种用法的前提,在日常开发过程中,典型的闭包用法即包含了柯里化的一部分思想,并不是因为有了“柯里化”的发明我们才去使用它。
上面的例子其实对比已经比较明显,当一个函数的处理过程可以切分为互相有联系的多个步骤且各步骤可能可被重用时,将该函数进行柯里化是一个很不错的方案。
作为一种编程思想,我们需要探讨的是如何在实际工程开发中运用它,并发挥积极作用呢?
3从一个例子看“通用”的柯里化函数及其包含的思想
再看一个示例,在这里我们同样用一个简单的累加计算函数代替任何依赖于多参数并且拥有复杂逻辑的函数来进行说明。
function sumNum () { // 这是一个累加函数,可以有不定个参数
var _tmpNum = 0;
for (var i = 0; i < arguments.length; i++) { // 非常简单的累加循环,这里为了示例方便不做多余的容错判断
_tmpNum += +arguments[i];
}
return _tmpNum;
}
/* …… 假设这里从各个地方已经艰难地计算出了实参num1、num2、num3 如果实参个数是动态的(可变的),
则每次计算都要记录并传回之前的参数 …… */
var myNum = sumNum(num1, num2, num3); // 可能需要很多次这样的调用,造成重复计算
可以看到,每次调用累加函数时都需要传入多个参数(实际上,这里的“累加函数”只是一个示例,实际开发中这个函数里可能有更多的逻辑。这个方法要同时接收并处理这么多参数并正确返回结果,可能是非常复杂的),这样我们就需要在他处得到实参后设法将需要传入的实参暂存起来,并且人工确保调用时同时传入的所有参数都是正确的。
那么,柯里化后可以帮助我们解决什么问题呢?
这里我们给出一个比较通用的将函数柯里化的方法:
function curry (func) { // 通用柯里化函数
var __args = []; // 这个数组存放每一次调用时传入的参数
return function callee() {
if ( arguments.length === 0 ) { // 若没传参数
return func.apply(this, __args); // 使用之前已传入的参数执行方法
}
[].push.apply(__args, arguments); // 将本次调用传入的参数push入数组
return callee;
};
}
Tips:理论上讲,柯里化后的函数是接受的应该是单一的参数(如果不是,可继续柯里化),这样有利于配合代码组合。这个例子主要是为了演示柯里化将多参函数转化为单参函数链的方法,所以做了参数个数是否为0的判断)
然后,我们仅需要将上文定义的同一个sumNum函数柯里化:
var sum = curry(sumNum);
在调用的时候,就不需要一次性传入所有参数了:
// 每一次计算出一个实参后都可调用一次,但并不需要马上得到最终结果
sum(num1); // 假设num1从别处得到计算出了num1===10后调用
/* …假设这里包含了很多其它代码… */
sum(num2); // 假设num2===25
/* …… */
sum(num3, num4); // 假设num3===30、num4===15,柯里化后的函数一般是接受单一参数的(如果不是,可继续柯里化),这里仅为示例说明用
/* …… */
// 到了需要最终结果的时候,直接不传实参调用即可
sum(); // 结果是80
看起来似乎不错,那么在sumNum“走进”柯里化函数之后,到底经历了什么呢?
首先,在curry函数的作用域内,我们定义了__args数组:
var __args = [];
这个数组的作用是存放每一次调用柯里化后的函数的参数。因为JS闭包的特性,这个数组会一直存在在闭包函数的作用域内,起到存放每次调用累积下来的实参的效果。
if ( arguments.length === 0 ) {
return func.apply(this, __agrs);
}
这一段非常明显,当实参个数为0时,才执行柯里化时传入的原有函数(即上文中的sumNum,这里做这个判断只是为了方便演示),执行sumNum时的参数为先前多次调用时闭包内变量__args记录下的所有实参。
[].push.apply( __args, arguments );
这一句被执行到的条件已经是实参个数不为0,即有传入实际参数。这个时候其实无需执行sumNumjs切割字符串,该语句的作用只是将当前实参push入数组中(之所以不能直接使用“arguments.push(__args)”是因为arguments并不是Array对象,只是一个伪数组,本身并没有push方法)。
return callee;
最后返回闭包内函数供下一次调用使用。在这里其实也可以使用“return arguments.callee();”达到一样的效果(匿名递归),但因为该语句在JS严格模式下会报错,所以稳妥起见还是return显式命名的函数。
通过这个简单的例子可以说明柯里化的两个明显的优点:
参数复用:每一次调用sum函数都只需要关心当前传入的参数的正确性,无需关心之前传入过什么参数;之前传入过的参数在后面的调用中均会被复用;
延迟计算:将最终需要结果的计算过程放在最后,实现惰性求值,避免重复计算和调用失误,也减少临时变量的使用,在提高性能的同时更加稳妥安全。
柯里化的本质是用单参数的函数在多次调用中拼出调用多参数函数的效果,达到简化接口、复用代码的目的。
其实上面介绍的“通用”柯里化函数并不是重点,因为适用场景并不算多(在一些集中的数据处理或类库的实现中比较常见)。重点其实是理解这样一个通用函数之后,在各自的业务场景下运用函数式编程时,能够将一些自己定义的方法进行自主的柯里化,并运用代码组合等思想提升代码质量。
Tips:由于全文篇幅较长,与“代码组合”和“反柯里化”相关的内容将于今后分期推送,欢迎继续关注。
限时特惠:本站每日持续更新海量设计资源,一年会员只需29.9元,全站资源免费下载
站长微信:ziyuanshu688