我们先做一些判断,当用户输入不是函数时报错
`let curry = fn =>{
if(typeof fn !== ‘function’){
throw Error(‘No function provided’)
}
return function curriedFn(…args){
if(args.length < fn.length){
return function(){
return curriedFn.apply(null,args.concat([…arguments]));
}
}
return fn.apply(null, args)
}
}`
我们来逐步分析这段代码发生了什么
首先,我们判断如果用户传的不是函数就报错
然后返回一个函数,接受多个参数
判断传入参数的长度是否小于函数参数列表的长度。如果是就进入 if 代码块,如果不是,就调用传入的函数
现在来看 if 代码块里面
我们先判断 args 的长度和 fn 参数的长度是否一致,如果大于等于则直接调用函数。如果小于的话则递归调用 curriedFn。注意使用 concat 把参数连接起来,当参数大于或等于的时候调用原函数。直接看个例子吧
`const multiply = (x, y, z) => x * y * z
let curriedMul1 = curry(multiply)(1,2,3)
// 6
// 这个 args 是三个,等于 multiply 的参数长度,所以直接返回结果
let curriedMul2 = curry(multiply)(2,3)
// 这个 args 是两个,小于参数长度,所以返回的还是一个函数,这个函数现在有两个参数,当再传入一个参数(也可以传入多个)时,就会执行该函数,例如
curriedMul2(4) // 24
curriedMul2(4,5,6) // 24
let curriedMul3 = curry(multiply)(2)
// 这个 args 是两个,小于参数长度,所以返回的还是一个函数,这个函数现在有一个参数,当再传入一个参数时,还是会返回一个函数,就变成第二种情况
curriedMul3(3) // 返回的还是一个函数,变成第二种情况
curriedMul3(3,4) // 24
curriedMul3(3)(4) // 24`
现在 curry 函数可以把一个多参数函数转化为一个一元函数了
6.2.4 回顾日志函数
现在我们可以用 curry 函数重写这个函数了,下面通过 curry 解决重复使用前两个参数的问题
`let errorLogger = curry(loggerHelper)(“ERROR”)(“Error At Stats.js”)
let debugLogger = curry(loggerHelper)(“DEBUG”)(“Debug At Stats.js”)
let warnLogger = curry(loggerHelper)(“WARN”)(“Warn At Stats.js”)`
现在我们能够轻松使用上面的柯里化函数并在各自的上下文中使用它们了
`// 用于错误
errorLogger(“Error message”,21)
// Error At Stats.js Error message at line:21
// 用于调试
debugLogger(“Debug message”,223)
// Debug At Stats.js Debug message at line:223
// 用于警告
warnLogger(“Warn message”,34)
// Warn At Stats.js Warn message at line:223`
这太棒了,curry 函数有助于移除很多函数调用中的样板代码
6.3 柯里化实战
在上一节中我们看到了使用 curry 函数的简单示例,在本节这种,我们将看到柯里化技术在小巧而简洁的示例中的应用。本节中的示例将让你在日常工作中如何使用柯里化有更好的理解
假设我们要查找含有数字的数组内容,可以通过如下方式解决
`let match = curry(function(expr,str){
return str.match(expr)
})`
返回的 match 函数是一个柯里化函数。我们可以给第一个参数 expr 一个正则表达式 /\d+/,这将表明内容中是否含有数字
let hasNumber = match(/\d+/)
现在我们创建一个柯里化的 filter 函数
`let filter = curry(function(f, ary){
return ary.filter(f);
})`
通过 hasNumber 和 filter 我们就可以创建一个新的名为 findNumbersInArray 的函数
`let findNumbersInArray = filter(hasNumber)
// 使用
finNumbersInArray([‘js’,‘number1’])
// [‘number1’]`
大功告成
6.4 偏应用
可能这一章有人就看不下去了,因为我也是,但是其实照做下来的话会发现没有那么难,也不是特别难理解,不要被吓倒!!!
什么是偏应用呢?我们先看这么一个功能
假设我们要 10ms 后做一组操作,可以通过 setTimeout 来实现
`setTimeout(()=>console.log(‘do something’),10)
setTimeout(()=>console.log(‘do anotherthing’),10)`
这个函数可以柯里化吗?答案是否定的,因为 curry 的参数列表是从左往右的。一个变通的方案是这样
`const setTimeoutWrapper = (time,fn)=>{
setTimeout(fn,time)
}
const delayTenMs = curry(setTimeoutWrapper)(10)
delayTenMs(()=>console.log(‘do something’))
delayTenMs(()=>console.log(‘do anotherthing’))`
但是这样的话我们需要创建一个包裹函数,这也是一种开销,这里就可以使用偏应用技术
6.4.1 实现偏函数
为了全面理解偏应用技术的机制,我们将先创建一个偏(partial)函数。实现以后,我们将通过一个简单的例子学习如何使用偏函数(不要觉得难,自己跟着做一下,拿个例子试下就很清楚了)
`const partial = function(fn,…partialArgs){
let args = partialArgs
return function(…fullArguments){
let arg = 0;
for(let i = 0; i < args.length && arg < fullArguments.length; i++){
if(args[i] === undefined){
args[i] = fullArguments[arg++];
}
}
return fn.apply(null,args);
}
}`
可能有些看不懂,来用个例子慢慢解释一下
const delayTenMs = partial(setTimeout,undefined,10)
现在 args 就是 [undefined,10],然后返回的是一个函数,所以 delayTenMs 其实是如下形式
`function delayTenMs(…fullArguments){
let arg = 0;
// 其中 args 是 [undefined,10]
for(let i = 0; i < args.length && arg < fullArguments.length; i++){
if(args[i] === undefined){
args[i] = fullArguments[arg++];
}
}
// fn 是 setTimeout
return fn.apply(null,args);
}`
然后,我们来使用 delayTenMs
`delayTenMs(()=>console.log(‘do something’))
// 10ms 后将打印出 do something,说明功能已经实现`
我们来看看为什么会这样
`// 现在的 delayTenMs 函数是这样
function delayTenMs(…fullArguments){
let arg = 0;
// 其中 args 是 [undefined,10]
for(let i = 0; i < args.length && arg < fullArguments.length; i++){
if(args[i] === undefined){
args[i] = fullArguments[arg++];
}
}
// fn 是 setTimeout
return fn.apply(null,args);
}
// 执行
delayTenMs(()=>console.log(‘do something’))`
这时候 fullArguments
就变成了一个只有一个参数的数组,即 [()=>console.log('do something')]
,因为数组的每一项可以为任意类型,所以这里就是只有一个函数的数组
args.length
等于 2,fullArguments.length
等于 1,所以进入循环
args[0]===undefined
满足条件,所以把 args[0]
变为 fullArguments[0]
即 args[0] = ()=>console.log('do something')
,这时 args = [()=>console.log('do something'),10]
,最后执行 fn 即 setTimeout,并把 args 作为参数传进去,即得到我们想要的结果,怎么样,是不是没有想象的那么难?
好了,接下来我们再看一个例子,用 JSON.stringify
来格式化输出
`let obj = {foo:‘bar’, bar:‘foo’}
JSON.stringify(obj, null, 2)
// JSON.stringify 接收三个参数,第二个是一个函数或数组,感兴趣的可以自己查一下。第三个是缩进的字符数`
由于后两个参数是固定的,所以我们可以用 partial 来移除它们(最好自己想一下再看)
`let prettyPrintJson = partial(JSON.stringify,undefined,null,2);
// 使用方法
prettyPrintJson(obj);
/*
'{
“foo”:“bar”,
“bar”:“foo”
}’
*/`
这个程序其实有个 bug,如果你用一个不同的参数再次调用的时候还是给出第一次的结果,为什么呢?因为 args 是数组,传的是引
真题解析、进阶学习笔记、最新讲解视频、实战项目源码、学习路线大纲
详情关注公中号【编程进阶路】
用,而不是一个函数。所以这里第二次调用的时候 args[0] 已经不等于 undefined 了。所以不会修改这个值。
6.4.2 柯里化与偏应用
那么我们什么时候用柯里化,什么时候用偏应用呢?取决于 API 是如何定义的。如果 API 如 map、filter 一样定义,我们就可以轻松的使用 curry。而如果不是这样的话,可能选择偏应用更合适一些。
可能有些人还是不太理解它们的区别,这么来说吧,我们先弄清楚偏函数到底是要干嘛
`function foo(a,b,c){
return a+b+c;
}
function foo6(a,c){
foo(a,6,c);
}`
最后
其实前端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
这里再分享一个复习的路线:(以下体系的复习资料是我从各路大佬收集整理好的)
《前端开发四大模块核心知识笔记》
最后,说个题外话,我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。
我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。