【函数柯里化】到底有什么用?

函数柯里化

定义:

是将接受多个参数的函数变换成可以单个参数分次传入的函数。

原理:

通过闭包保留已传入的参数,在后续的调用中,如参数不够就继续返回一个函数,直到参数传完整后调用执行。

作用:

可以将函数的某些参数提前固化下来。

比如对网络请求方法的封装

  1. 先有一个基础的request-Api
const request = (method, url, body) => {

    // 具体实现省略...

}
复制代码

总所周知,网络请求就是那几种,最常用的就是get、post。但每次调用此方法,都需要下方这样

request('get', 'url1', { a: 1 })

request('post', 'url2', { b: 2 })
复制代码

每次调用都必传method参数,比较麻烦,且有拼写错误的概率, 所以为了提高体验,希望可以将此参数传入一次后,就内部保存起来,不必每次都传。

  1. 固化method参数,提供更方便的get、post方法
const get = (url, body) => {
    return request('get', url, body)
}

const post = (url, body) => {
    return request('post', url, body)
}

// 接下来,就可以这样方便的调用了

get('url1', { a: 1 })

post('url2', { a: 1 })
复制代码

再举一个例子:

下边是一个基础的检测数据的方法,接受值val、类型type两个参数

const isType = (val,type)=>{
    return Object.prototype.toString.call(val) === `[object ${type}]` 
 }

// 用法是这样的

isType(1,"Number")

isType(2,"String")
复制代码

但js中的数据类型也就固定的那些。所以type参数与上边的method参数类似,也是可以提前固化下来, 接下来改造一下isType函数:

const isType = (type)=>{
     return (val)=>{
         return Object.prototype.toString.call(val) === `[object ${type}]` 
     }
 }

const isArray = isType('Array')

const isString = isType('String')

const isNumber = isType('Number')

// 用法

console.log(isArray([1,2]))

console.log(isString(111))
复制代码

固定参数

以上这些例子对函数参数的处理方式,虽然都达到了目的,但都不够通用。

调用次数首先只支持有限的次数(2次), 如果是多次的话,就不容易满足了。

比如,应对下边的参数求和场景就很吃力了:

需求:将求和函数改造成参数分批传入,一旦参数个数传完整时(==5)时,就立即进行求和计算

const sum = (a, b, c, d, e) => {
    return a + b + c + d + e
}

// 预期效果

sum(1)(2)(3)(4)(5)  // 15
复制代码

终于在自己的不懈努力下,端出了一碗香喷喷的意大利面条...

const sum = (a)=>{

         return (b)=>{

            return (c)=>{

                 return (d)=>{

                     return (e)=>{

                         return a + b + c + d + e

                     }

                 }

             }

         }

 }
复制代码

以上是开个玩笑,那要实现一个适应多参数场景的通用柯里化工具函数,可以这样:

 const curring = (fn,argArr = [])=>{

    const argLen = fn.length;

    return (...args)=>{

         argArr.push(...args)

         if(argArr.length < argLen){

             return curring(fn,argArr)            

         }else{

             return fn(...argArr)

         }

     }

 }

// console.log(curring(sum)(1)(2)(3)(4)(5))
复制代码

但这版在递归收集参数部分的实现有点问题,因为curring本身只需要一个fn参数,多出来的argArr参数,只是通过沿着递归向下传递的方式保存住一个内部变量,完全可以用闭包的方式来解决。

 const curring = (fn) => {
     const argLen = fn.length;
     const argArr = [];
     const callFn = (...args) => {
         argArr.push(...args)
         if (argArr.length < argLen) {
             return callFn
         } else {
             return fn(...argArr)
         }
     }
     return callFn

 }

// console.log(curring(sum)(1)(2)(3)(4)(5))
复制代码

这样就实现了一个较为完美的函数参数curring。

除此之外,笔者在一次面试中遇到过一道更棘手的问题: 如果一个函数的参数个数是不固定的,怎么实现柯里化?

不固定参数

 // 实现一个add方法,使计算结果能够满足如下预期:

add(1)(2)(3) // 6;

add(1, 2, 3)(4) // 10;

add(1)(2)(3)(4)(5) // 15;
复制代码

这种需求,其实极少有实际的应用场景,顶多面试问问, 本着服务到底的精神,这里也讲讲。

首先,这个需求理论上是无法实现的。因为你永远也无法知道这个高阶函数的调用停止的时机,也就无法最终的求和计算,但面试官既然考了,那么肯定也是有意义的。需要限定使用场景,那是什么样的场景下呢。先看一段代码

function add(){}

console.log(add) // [Function: add]
复制代码

稍稍改动一下,

function add(){}

add.toString=function(){
    return 5;
}

console.log(add) // [Function: add]

console.log(add + 0) // 5
复制代码

+0 对结果的输出改变非常大。这里就用到了隐式转换的知识:在对象类型(包括数组,对象,函数等)的值参与原始运算如算术或逻辑运算时,会尝试调用其toString或者valueOf得到一个原始值。详见笔者之前写过的这篇文章从【ECMA-262语言规范】出发,解读JS的强制数据类型转换规则 - 掘金

预想一下,sum函数的返回值如果一直返回一个重写了toString方法的函数,在最终隐式转换的那一步,触发toString里的求和计算,那我们的需求就实现了。那么将思路转换为代码,就是下方的代码:

function sum(...args1){
    const args = [...args1]
    function returnFn(...args2){
        args.push(...args2)
        return returnFn   
    }
    returnFn.toString = function(){
        return args.reduce((a,b)=>a+b,0)
    }
    return returnFn
}

console.log(sum(1)(2)(3)(4)(5) + 0) // 15
复制代码

所以,在面试问到这个问题时,先提示下考官,需要限定下使用场景,也就是需要发生隐式转换,才可以生效。

以上就是全部,感谢阅读。

Guess you like

Origin juejin.im/post/7075977990504448036