JavaScript函数式编程-柯里化与偏应用04

251 阅读3分钟

这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战

1.术语:

下面来介绍一些后续会用到的术语。

  • 一元函数:只接收一个参数的函数。

    //一元函数
    const func = (x) => x;
    
  • 二元函数:接收两个参数的函数。

    //二元函数
    const add = (a,b)=> a + b;
    
  • 变参函数:接收可变数量参数的函数

    function variableFunc (a){
      console.log(a);
      console.log(arguments)
    }
    variableFunc(214,520,920);
    //打印结果
    //214
    // [214,520,920]
    
    • argumnets可以捕获调用函数的参数
    //es6中的拓展运算符
    function variableFunc (a,...variables){
      console.log(a)
      console.log(variables)
    }
    variableFunc(214,520,920);
    //打印结果
    //214
    // [214,520,920]
    
    • es6的拓展运算符

2.柯里化:

🚀柯里化:把一个多参数函数转换为一个嵌套的一元函数的过程称之为柯里化

例如:const add = (x,y) => x +y柯里化后const addCurry = x => y => x+y

🚀 多参数函数柯里化实现:

const curriedN = function(func){
  if(typeof func !== "function"){
    throw new Error('func');
  }
  return function curried(...args){
    if(args.length < func.length){
      return function(){
        return curried.apply(null,args.concat([].slice.call(arguments)));
      }
    }
    return func.apply(null,args);
  }
}

🚀 分析:

  • args.length < func.length是如果传入的参数args小于func函数所需要的参数个数就进入判断;否则就直接返回func.apply(null,args)的调用结果。

  • 下面重点说说图片中的代码:

  • 图片中的return funciton(){...略}为了获取到后面返回函数传入的参数,也就是获取到的arguments参数

  • 注意闭包的使用-args,通过返回函数后,curried函数的args并没有释放,因为在返回的内层的匿名函数中有使用,这样在下次递归调用curried函数的时候就能完成拼接操作--args.concat([].slice.call(arguments));

    🚀 柯里化函数实战使用测试:

    例子1:求三个数的乘积:

    const multiply = (x,y,z)=> x*y*z;
    let curried = curriedN(multiply);
    let sixty = curried(3)(4)(5);
    console.log(sixty); //60
    
    let res1 = curried(3);
    let res2 = res1(4);
    let res3 = res2(10);
    console.log(res3); //120
    

    可以看到,无论直接传入还是分步骤传入,结果没有问题。

    例子2:求数组的平方:

let map1 = curriedN(function (func, arr) {
  return arr.map(func);
});
let squareAll = map1((x) => x * x);
let res4 = squareAll([1,2,3,4]);
console.log(res4);
//[1,4,9,16]
  • 通过curriedN对函数进行柯里化,--map1
  • map1传入执行函数,也就是x=>x*x,--squareAll
  • squareAll传入真实的参数,也就是数组--[1,2,3,4]

3.偏应用:

什么是偏应用呢?先来看下面的一个场景:

假设我们要 10ms 后做一组操作,可以通过 setTimeout 来实现

setTimeout(()=>console.log('吃饭饭🍚'),10)
setTimeout(()=>console.log('睡觉觉😴'),10)

这个函数可以柯里化吗?答案是否定的。curry参数列表是从左往右的。不过可以通过

const setTimeoutWrapper = (time,func)=>{
	setTimeout(func,time);
};
const delayTenMs = curriedN(setTimeoutWrapper)(10);
dealyTenMs(()=>console.log('摸🐟🐟'));
delayTenMs(()=>console.log('扣🦵🏻🦵🏻'));
  • 为了能是函数正常的进行柯里化,需要创建类似上面的一个包裹函数,这也是一种开销,由此可以使用偏应用技术。

🚀 下面来看下偏应用的实现:

//偏应用函数实现
function partial(fn){
  var args = Array.prototype.slice.call(arguments); //将类数组对象arguments转为数组
  args.shift();                                     //除去fn参数
  for (var i = args.length;i<fn.length;i++)         //补齐,跟fn的参数列表对应上
    args.push(undefined)
  	return function() {
        var remainArgs = Array.prototype.slice.call(arguments), // 剩余参数
            index = 0;
        for (i = 0; i < args.length; i++) {
            args[i] === undefined && (args[i] = remainArgs[index++])
        }
        return fn.apply(this,args);
  }
}
//偏应用函数使用
function computeFourNums(a,b,c,d){
  return a + b + c + d;
}
var partialComputeFourNums = partial(computeFourNums,1,undefined,3,undefined);
console.log(partialComputeFourNums(2,4));
//10
  • undefined给下次要传入的参数做占位
  • partial函数体开始将参数fnarguments剔除
  • 通过for循环将参数补齐(因为传参数不一定都按照fn的所需的参数个数去传--partial(computeFourNums,1,undefined));为了避免这种情况的发生就需要对参数进行一个补齐。