函数式编程指南
阮一峰函数式编程入门教程
函数式编程概念
函数式编程倡导利用若干个简单的执行单元让计算结果不断渐进,逐层推导复杂的运算。
函数式编程有两个最基本的运算:合成(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