转载地址: 函数式编程
应该很多童鞋都听过函数式编程(Functional programming)的概念吧,可能有的童鞋有听说过函数式编程但并不是特别了解,但其实在我们的开发过程中,或多或少都已经应用了函数式编程的思想。
相对于面向对象编程(Object-oriented programming)关注的是数据而言,函数式编程关注的则是动作,其是一种过程抽象的思维,就是对当前的动作去进行抽象。
比如说我要计算一个数加上4再乘以4 的值,按照正常写代码的逻辑,我们可能会这么去实现
function calculate(x){
return (x + 4) * 4;
}
console.log(calculate(1)) // 20
这是没有任何问题的,我们在平时开发的过程中会经常将需要重复的操作封装成函数以便在不同的地方能够调用。但从函数式编程的思维来看的话,我们关注的则是这一系列操作的动作,先「加上 4」再「乘以 4」。
如何封装函数才是最佳实践呢?如何封装才能使函数更加通用,使用起来让人感觉更加舒服呢?函数式编程或许能给我们一些启发。
函数式编程具有两个基本特征。
- 函数是第一等公民
- 函数是纯函数
函数是第一等公民
第一等公民是指函数跟其它的数据类型一样处于平等地位,可以赋值给其他变量,可以作为参数传入另一个函数,也可以作为别的函数的返回值。
// 赋值
var a = function fn1() { }
// 函数作为参数
function fn2(fn) {
fn()
}
// 函数作为返回值
function fn3() {
return function() {}
}
函数是纯函数
纯函数是指相同的输入总会得到相同的输出,并且不会产生副作用的函数。
从纯函数的概念我们可以知道纯函数具有两个特点:
- 同输入同输出
- 无副作用
无副作用指的是函数内部的操作不会对外部产生影响(如修改全局变量的值、修改 dom 节点等)。
// 是纯函数
function add(x,y){
return x + y
}
// 输出不确定,不是纯函数
function random(x){
return Math.random() * x
}
// 有副作用,不是纯函数
function setColor(el,color){
el.style.color = color ;
}
// 输出不确定、有副作用,不是纯函数
var count = 0;
function addCount(x){
count+=x;
return count;
}
函数式编程具有两个最基本的运算:合成(compose)和柯里化(Currying)。
函数合成(compose)
函数合成指的是将代表各个动作的多个函数合并成一个函数。
上面讲到,函数式编程是对过程的抽象,关注的是动作。以上面计算的例子为例,我们关注的是它的动作,先「加上 4」再「乘以 4」。那么我们的代码实现如下
function add4(x) {
return x + 4
}
function multiply4(x) {
return x * 4
}
console.log(multiply4(add4(1))) // 20
根据函数合成的定义,我们能够将上述代表两个动作的两个函数的合成一个函数。我们将合成的动作抽象为一个函数 compose,这里可以比较容易地知道,函数 compose 的代码如下
function compose(f,g) {
return function(x) {
return f(g(x));
};
}
所以我们可以通过如下的方式得到合成函数
var calculate=compose(multiply4,add4); //执行动作的顺序是从右往左
console.log(calculate(1)) // 20
可以看到,只要往 compose 函数中传入代表各个动作的函数,我们便能得到最终的合成函数。但上述 compose 函数的局限性是只能够合成两个函数,如果需要合成的函数不止两个呢,所以我们需要一个通用的 compose 函数。
这里我直接给出通用 compose 函数的代码
function compose() {
var args = arguments;
var start = args.length - 1;
return function () {
var i = start - 1;
var result = args[start].apply(this, arguments);
while (i >= 0){
result = args[i].call(this, result);
i--;
}
return result;
};
}
让我们来实践下上述通用的 compose 函数~
function addHello(str){
return 'hello '+str;
}
function toUpperCase(str) {
return str.toUpperCase();
}
function reverse(str){
return str.split('').reverse().join('');
}
var composeFn=compose(reverse,toUpperCase,addHello);
console.log(composeFn('ttsy')); // YSTT OLLEH
上述过程有三个动作,「hello」、「转换大写」、「反转」,可以看到通过 compose 将上述三个动作代表的函数合并成了一个,最终输出了正确的结果。
函数柯里化(Currying)
在维基百科中对柯里化的定义是:在计算机科学中,柯里化,又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
柯里化函数则是将函数柯里化之后得到的一个新函数。由上述定义可知,柯里化函数有如下两个特性:
- 接受一个单一参数;
- 返回接受余下的参数而且返回结果的新函数;
举个例子~
function add(a, b) {
return a + b;
}
console.log(add(1, 2)) // 3
假设函数 add 的柯里化函数是 addCurry,那么从上述定义可知,addCurry(1)(2) 应该实现与上述代码相同的效果,输出 3 。这里我们可以比较容易的知道,addCurry 的代码如下
// addCurry 是 add 的柯里化函数
function addCurry(a) {
return function(b) {
return a + b;
}
}
console.log(addCurry(1)(2)); // 3
但假设如果有一个函数 createCurry 能够实现柯里化,那么我们便可以通过下述的方式来得出相同的结果
// createCurry 返回一个柯里化函数
var addCurry=createCurry(add);
console.log(addCurry(1)(2)); // 3
可以看到,函数 createCurry 传入一个函数 add 作为参数,返回一个柯里化函数 addCurry,函数 addCurry 能够处理 add 中的剩余参数。这个过程称为函数柯里化,我们称 addCurry 是 add 的柯里化函数。
那么,怎么得到实现柯里化的函数 createCurry 呢?这里我直接给出 createCurry 的代码
// 参数只能从左到右传递
function createCurry(func, arrArgs) {
var args=arguments;
var funcLength = func.length;
var arrArgs = arrArgs || [];
return function(param) {
var allArrArgs=arrArgs.concat([param])
// 如果参数个数小于最初的func.length,则递归调用,继续收集参数
if (allArrArgs.length < funcLength) {
return args.callee.call(this, func, allArrArgs);
}
// 参数收集完毕,则执行func
return func.apply(this, allArrArgs);
}
}
我们可以通过如下方式去调用
// createCurry 返回一个柯里化函数
var addCurry=createCurry(function(a, b, c) {
return a + b + c;
});
console.log(addCurry(1)(2)(3)); // 6
上述 createCurry 函数已经能够实现柯里化的过程,但是其并没有那么完美,如果我希望以 addCurry(1, 2)(3) 的方式来调用呢?则上述代码并不能给出我们想要的结果,所以我们要对 createCurry 做一个优化,优化后的 createCurry 代码如下
// 参数只能从左到右传递
function createCurry(func, arrArgs) {
var args=arguments;
var funcLength = func.length;
var arrArgs = arrArgs || [];
return function() {
var _arrArgs = Array.prototype.slice.call(arguments);
var allArrArgs=arrArgs.concat(_arrArgs)
// 如果参数个数小于最初的func.length,则递归调用,继续收集参数
if (allArrArgs.length < funcLength) {
return args.callee.call(this, func, allArrArgs);
}
// 参数收集完毕,则执行func
return func.apply(this, allArrArgs);
}
}
优化之后的 createCurry 函数则显得更加强大
// createCurry 返回一个柯里化函数
var addCurry=createCurry(function(a, b, c) {
return a + b + c;
});
console.log(addCurry(1)(2)(3)); // 6
console.log(addCurry(1, 2, 3)); // 6
console.log(addCurry(1, 2)(3)); // 6
console.log(addCurry(1)(2, 3)); // 6
柯里化实际上是把简答的问题复杂化了,但是复杂化的同时,我们在使用函数时拥有了更加多的自由度。
那么,柯里化有什么用途呢?举个例子~
现在我们需要实现一个功能,将一个全是数字的数组中的数字转换成百分数的形式。按照正常的逻辑,我们可以按如下代码实现
function getNewArray(array) {
return array.map(function(item) {
return item * 100 + '%'
})
}
console.log(getNewArray([1, 0.2, 3, 0.4])); // ['100%', '20%', '300%', '40%']
而如果通过柯里化的方式来实现
function map(func, array) {
return array.map(func);
}
var mapCurry = createCurry(map);
var getNewArray = mapCurry(function(item) {
return item * 100 + '%'
})
console.log(getNewArray([1, 0.2, 3, 0.4])); // ['100%', '20%', '300%', '40%']
上述例子可能太简单以致不能表现出柯里化的强大,具体柯里化的使用还需要结合具体的场景来用,没有必要为了柯里化而柯里化