函数式编程之合成与柯里化


函数式编程指南
阮一峰函数式编程入门教程

函数式编程概念

函数式编程倡导利用若干个简单的执行单元让计算结果不断渐进,逐层推导复杂的运算。
函数式编程有两个最基本的运算:合成(compose)和柯里化(Currying)
柯里化:一个函数原本有多个参数,只传入一个参数,生成一个新函数,由新函数接收剩下的参数运行得到结果。
偏函数:一个函数原本有多个参数,只传入一部分参数,生成一个新函数,由新函数接收剩下的参数运行得到结果。
高阶函数:一个函数参数是一个函数,该函数对参数这个函数进行加工,得到一个函数,这个加工用的函数就是高阶函数。

什么是合成

如果一个值要经过多个函数,才能变成另外一个值,就可以把所有中间步骤合并成一个函数。

合成的优点

使得代码变的简单而富有可读性
通过不同的组合方式,可以组合出其他常用函数,使得代码更具表现力

合成实例

function f1(arg){
	console.log("f1",arg)
	return arg;
}
function f2(arg){
	console.log("f2",arg)
	return arg;
}
function f3(arg){
	console.log("f3",arg)
	return arg;
}
function compose(...funcs){
	if(funcs.length === 0){
		return arg => arg;
	}
	if(funcs.length === 1){
		return funcs[0]
	}
	return funcs.reduce((a,b)=>(...args)=>a(b(...args)))
}
let res = compose(f1,f2,f3)("omg")//f1(f2(f3("omg")))
console.log('res',res)

什么是柯里化

柯里化通常也称部分求值,其含义是给函数分步传递参数,每次传递参数后,部分应用参数,并返回一个更具体的函数接受剩下的参数,中间可嵌套多层这样的接受部分参数的函数,逐步缩小函数的适用范围,逐步求解,直至返回最后结果。

听起来有些像递归的反复调用,但又和递归有明显的不同,递归是根据条件不断调用自己本身,满足条件跳出。柯里化是函数内部返回的一个匿名函数,匿名函数内部再根据条件不断将自己作为返回值返回。

直接上代码,如下

add(x,y,z,...) => add(x)(y)(z)...

为什么用柯里化

为了提升性能,使用柯里化可以缓存一部分能力,不用再重复去操作。

实例引入

实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) == 6;
add(1, 2, 3)(4) == 10;
add(1)(2)(3)(4)(5) == 15;
可以看到,函数的值如果等于所有参数的和,则不等式成立。
第一步
我们先用代码来实现第一个不等式。

function add(a){
	let sum = 0;
	sum += a;
	return function(b){
		sum += b;
		return function(c){
			sum += c;
			return sum;
		}
	}
}

第二步
很明显,调用参数多的时候,这种写法是不能满足需求的。我们需要通过递归的方式反复来将函数作为返回值返回。

function add(a){
	let sum = 0;
	sum += a;
	return function tmp(b){
		if(arguments.length == 0){
			return sum;
		}else{
			sum += b;
			return tmp;
		}
	}
}

上面的方法明显还是存在缺陷的,我们需要在函数调用最后的时候再执行一下,不然最后的返回值将是一个函数。
add(1)(2)(3)()
如何才能不显示的调用最后一次呢,我们可能会想到对象的toString和valueOf方法去隐式调用。

function add(a){
	let sum = 0;
	sum += a;
	let tmp = function(b){
		sum += b;
		return tmp;
	}
	tmp.toString = tmp.valueOf = function(){
		return sum;
	}
	return tmp;
}

何为隐式调用,就是只要我们调用对象,js解析器自动帮我们调用了其对象的toString和valueOf方法。
举个例子:

var obj={       
    i:10,     
     valueOf:function(){            
         console.log('执行了valueOf()');            
         return this.i+20	            
     },        
     toString:function(){      
         console.log('执行了toString()');
   	    return this.valueOf()+20
     }
 }
 //结果
 console.log( obj )    //50       执行了toString() 执行了valueOf()
 console.log( +obj )    //30       执行了valueOf()
 console.log( obj>40 )    //false       执行了valueOf()
 console.log( obj==30 )    //true       执行了valueOf()
 console.log( obj===30 )    //false
 console.log(String(obj))    //50 执行了toString() 执行了valueOf()
 console.log(Number(obj))	//30 执行了valueOf()
 //全等比较时,没有调用,个人猜想,js解析器直接先判断类型是否一样,不一样直接返回false,没有再往下执行

那么两种方法具体什么时候起作用呢?
如果做加减乘除、比较运算的时候执行的是valueOf方法,如果是需要具体值呢就执行的是toString方法。

第三步
每次只有一个参数的我们已经解决,但是如果有多个参数的情况呢。我们可以将参数arguments转化为数组,然后求和处理。
伪数组转数组的两种方式:
es5: Array.prototype.slice.call(arguments)
es6: Array.from(arguments)
最终结果如下:

function add(){
	//es6伪数组转为数组
	let _args = Array.from(arguments)
	let _adder = function(){
		 _args.push(...arguments);
		 return _adder;
	}
	//toString隐式转换
	_adder.toString = function(){
		//数组求和
		return _args.reduce((a,b)=>{return a+b})
	}
	return _adder;
}

通用封装

// 支持多参数传递
function progressCurrying(fn, args) {
    var _this = this
    var len = fn.length;
    var args = args || [];
    return function() {
        var _args = Array.prototype.slice.call(arguments);
        Array.prototype.push.apply(args, _args);
        // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
        if (_args.length < len) {
            return progressCurrying.call(_this, fn, _args);
        }
        // 参数收集完毕,则执行fn
        return fn.apply(this, _args);
    }
}

作用好处

1.参数复用

function square(i) {
    return i * i;
}
function double(i) {
    return i *= 2;
} 
function map(handeler, list) {
    return list.map(handeler);
}
// 数组的每一项平方
map(square, [1, 2, 3, 4, 5]);
map(square, [6, 7, 8, 9, 10]);
map(square, [10, 20, 30, 40, 50]);
// 数组的每一项加倍
map(double, [1, 2, 3, 4, 5]);
map(double, [6, 7, 8, 9, 10]);
map(double, [10, 20, 30, 40, 50]);  

便于多地调用,减少了重复代码。
2、提前确认

var on = function(element, event, handler) {
    if (document.addEventListener) {
        if (element && event && handler) {
            element.addEventListener(event, handler, false);
        }
    } else {
        if (element && event && handler) {
            element.attachEvent('on' + event, handler);
        }
    }
}
var on = (function() {
    if (document.addEventListener) {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.addEventListener(event, handler, false);
            }
        };
    } else {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.attachEvent('on' + event, handler);
            }
        };
    }
})();

//换一种写法可能比较好理解一点,上面就是把isSupport这个参数给先确定下来了
var on = function(isSupport, element, event, handler) {
    isSupport = isSupport || document.addEventListener;
    if (isSupport) {
        return element.addEventListener(event, handler, false);
    } else {
        return element.attachEvent('on' + event, handler);
    }
}

对于一些需要校验的内容,我们没必要每次都去进行判断,可以提前把判断内容做好,然后传进来直接走对应的流程即可。
3、延迟执行,固定易变因素
柯里化特性决定了它这应用场景。提前把易变因素,传参固定下来,生成一个更明确的应用函数。最典型的代表应用,是bind函数用以固定this这个易变对象。

Function.prototype.bind = function (context) {
    var _this = this
    var args = Array.prototype.slice.call(arguments, 1)
    return function() {
        return _this.apply(context, args)
    }
}
function a(){
	return this.name;
}
console.log(a())//''
let b = {
	name:'xiaohong'
}
console.log(a.bind(b)())//xiaohong

性能问题

Currying的一些性能问题你只要知道下面四点就差不多了:

存取arguments对象通常要比存取命名参数要慢一点
一些老版本的浏览器在arguments.length的实现上是相当慢的
使用fn.apply( … ) 和 fn.call( … )通常比直接调用fn( … ) 稍微慢点
创建大量嵌套作用域和闭包函数会带来花销,无论是在内存还是速度上

其实在大部分应用中,主要的性能瓶颈是在操作DOM节点上,js的性能损耗基本是可以忽略不计的,所以Currying是可以直接放心的使用。

参考资料

https://www.jianshu.com/p/2975c25e4d71
https://blog.csdn.net/qq_39207948/article/details/80593715
https://www.cnblogs.com/barrior/p/4598354.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值