全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2022-09-27_从柯里化讲起,一网打尽 JavaScript 重要的高阶函数

您的位置:首页 >> 新闻 >> 行业资讯

从柯里化讲起,一网打尽 JavaScript 重要的高阶函数 本文为稀土掘金社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!! 专栏简介作为一名 5 年经验的 JavaScript 技能拥有者,笔者时常在想,它的核心是什么?后来我确信答案是:闭包和异步。而函数式编程能完美串联了这两大核心,从高阶函数到函数组合;从无副作用到延迟处理;从函数响应式到事件流,从命令式风格到代码重用。所以,本专栏将从函数式编程角度来再看 JavaScript 精要,欢迎关注! 前情回顾我们在前篇《?从历史讲起,JavaScript 基因里写着函数式编程》讲到了 JavaScript 的函数式基因最早可追溯到 1930 年的 lambda 运算,这个时间比第一台计算机诞生的时间都还要早十几年。JavaScript 闭包的概念也来源于 lambda 运算中变量的被绑定关系。 因为在 lambda 演算的设定中,参数只能是一个,所以通过柯里化的天才想法来实现接收多个参数: lambdax.(lambday.plusxy) 说这个想法是“天才”一点不为过,把函数自身作为输入参数或输出返回值,至今受用,也就是【高阶函数】的定义。 将上述 lambda 演算柯里化写法转变到 JavaScript 中,就变成了: functionadd(a){ returnfunction(b){ returna+b } } add(1)(2) 所以,剖析闭包从柯里化开始,柯里化是闭包的“孪生子”。 读完本篇,你会发现 JavaScript 高阶函数中处处是闭包、处处是柯里化~ 百变柯里化最开始,本瓜理解柯里化 == 闭包 + 递归,得出的柯里化写法是这样的: letarr=[] functionaddCurry(){ letarg=Array.prototype.slice.call(arguments);//递归获取后续参数 arr=arr.concat(arg); if(arg.length===0){//如果参数为空,则判断递归结束 returnarr.reduce((a,b)={returna+b})//求和 }else{ returnaddCurry; } } addCurry(1)(2)(3)() 但这样的写法,addCurry函数会引用一个外部变量arr,不符合纯函数的特性,于是就优化为: functionaddCurry(){ letarr=[...arguments] letfn=function(){ if(arguments.length===0){ returnarr.reduce((a,b)=a+b) }else{ arr.push(...arguments) returnfn } } returnfn } 上述写法,又总是要以 ‘( )’ 空括号结尾,于是再改进为隐式转换.toString写法: functionaddCurry(){ letarr=[...arguments] //利用闭包的特性收集所有参数值 varfn=function(){ arr.push(...arguments); return //利用toString隐式转换 fn.toString=function(){ returnarr.reduce(function(a,b){ returna+ } return } 注意隐士转换在浏览器环境和 node 环境下的不一致;好了,到这一步,如果你把上述三种柯里化写法都会手写了,那面试中考柯里化的基础一关算是过了。 然而,不止于此,柯里化实际存在很多变体,只有深刻吃透它的思想,而非停留在一种写法上,才能算得上“高级”、“优雅”。 5a06-kicwvzp7773475.gif接下来,让我们看看它怎么变?! 缓存传参柯里化最基础的用法是缓存传参。 我们经常遇到这样的场景: 已知一个ajax函数,它有 3 个参数 url、data、callback functionajax(url,data,callback){ //... } 不用柯里化是怎样减少传参的呢?通常是以下这样,写死参数位置的方式来减少传参: functionajaxTest1(data,callback){ ajax('http://www.test.com/test1',data,callback); } 而通过柯里化,则是这样: functionajax(url,data,callback){ //... } letajaxTest2=partial(ajax,'http://www.test.com/test2') ajaxTest2(data,callback) 其中partial函数是这样写的: functionpartial(fn,...presetArgs){//presetArgs是需要先被绑定下来的参数 returnfunctionpartiallyApplied(...laterArgs){//...laterArgs是后续参数 letallArgs=presetArgs.concat(laterArgs)//收集到一起 returnfn.apply(this,allArgs)//传给回调函数fn } } 柯里化固定参数的好处在:复用了原本的 ajax 函数,并在原有基础上做了修改,取其精华,弃其糟粕,封装原有函数之后,就能为我所用。 并且partial函数不止对ajax函数有作用,对于其它想减少传参的函数同样适用。 缓存判断我们可以设想一个通用场景,假设有一个 handleOption 函数,当符合条件 'A',执行语句:console.log('A');不符合时,则执行语句:console.log('others') 转为代码即: consthandleOption=(param)={ if(param==='A'){ console.log('A') }else{ console.log('others') } } 现在的问题是:我们每次调用handleOption('A'),都必须要走完 if...else... 的判断流程。比如: consthandleOption=(param)={ console.log('每次调用handleOption都要执行if...else...') if(param==='A'){ console.log('A') }else{ console.log('others') } } handleOption('A') handleOption('A') handleOption('A') 控制台打印: image.png有没有什么办法,多次调用handleOption('A'),却只走一次 if...else...? 答案是:柯里化。 consthandleOption=((param)={ console.log('从始至终只用执行一次if...else...') if(param==='A'){ return()=console.log('A') }else{ return()=console.log('others') } }) consttmp=handleOption('A') tmp() tmp() tmp() 控制台打印: image.png这样的场景是有实战意义的,当我们做前端兼容时,经常要先判断是来源于哪个环境,再执行某个方法。比如说在 firefox 和 chrome 环境下,添加事件监听是addEventListener方法,而在 IE 下,添加事件是attachEvent方法;如果每次绑定这个监听,都要判断是来自于哪个环境,那肯定是很费劲。我们通过上述封装的方法,可以做到一处判断,多次使用。 肯定有小伙伴会问了:这也是柯里化? image.png嗯。。。怎么不算呢? 把 'A' 条件先固定下来,也可叫“缓存下来”,后续的函数执行将不再传 'A' 这个参数,实打实的:把多参数转化为单参数,逐个传递。 缓存计算我们再设想这样一个场景,现在有一个函数是来做大数计算的: constcalculateFn=(num)={ conststartTime=newDate() for(letii++){}//大数计算 constendTime=newDate() console.log(endTime-startTime) return"Calculatebignumbers" } calculateFn(10_000_000_000) 这是一个非常耗时的函数,复制代码在控制台看看,需要 8s+ 如果业务代码中需要多次用到这个大数计算结果,多次调用calculateFn(10_000_000_000)肯定是不明智的,太费时。 一般的做法就是声明一个全局变量,把运算结果保存下来: 比如const resNums = calculateFn(10_000_000_000) 如果有多个大数运算呢?沿着这个思路,即声名多个变量: constresNumsA=calculateFn(10_000_000_000) constresNumsB=calculateFn(20_000_000_000) constresNumsC=calculateFn(30_000_000_000) 我们讲就是说:奥卡姆剃刀原则 —— 如无必要、勿增实体。 申明这么多全局变量,先不谈占内存、占命名空间这事,就把calculateFn()函数的参数和声名的常量名一一对应,都是一个麻烦事。 有没有什么办法?只用函数,不增加多个全局常量,就实现多次调用,只计算一次? 答案是:柯里化。 代码如下: functioncached(fn){ constcacheObj=Object.create(null);//创建一个对象 returnfunctioncachedFn(str){//返回回调函数 if(!cacheObj[str]){//在对象里面查询,函数结果是否被计算过 letresult=fn(str); cacheObj[str]=result;//没有则要执行原函数,并把计算结果缓存起来 } returncacheObj[str]//被缓存过,直接返回 } } constcalculateFn=(num)={ console.log("计算即缓存") conststartTime=newDate() for(letii++){}//大数计算 constendTime=newDate() console.log(endTime-startTime)//耗时 return"Calculatebignumbers" } letcashedCalculate=cached(calculateFn) console.log(cashedCalculate(10_000_000_000))//计算即缓存//9944//Calculatebignumbers console.log(cashedCalculate(10_000_000_000))//Calculatebignumbers console.log(cashedCalculate(20_000_000_000))//计算即缓存//22126//Calculatebignumbers console.log(cashedCalculate(20_000_000_000))//Calculatebignumbers 这样只用通过一个cached缓存函数的处理,所有的大数计算都能保证:输入参数相同的情况下,全局只用计算一次,后续可直接使用更加语义话的函数调用来得到之前计算的结果。 此处也是柯里化的应用,在cached函数中先传需要处理的函数参数,后续再传入具体需要操作得值,将多参转化为单个参数逐一传入。 缓存函数柯里化的思想不仅可以缓存判断条件,缓存计算结果、缓存传参,还能缓存“函数”。 设想,我们有一个数字 7 要经过两个函数的计算,先乘以 10 ,再加 100,写法如下: constmulti10=function(x){returnx*} constadd100=function(x){returnx+} add100(multi10(7)) 用柯里化处理后,即变成: constmulti10=function(x){returnx*} constadd100=function(x){returnx+} constcompose=function(f,g){ returnfunction(x){ returnf(g(x)) } } compose(add100,multi10)(7) 前者写法有两个传参是写在一起的,而后者则逐一传参。把最后的执行函数改写: letcompute=compose(add100,multi10) compute(7) 所以,这里的柯里化直接把函数处理给缓存了,当声明 compute 变量时,并没有执行操作,只是为了拿到 ()= f(g(x)),最后执行 compute(7),才会执行整个运算; 怎么样?柯里化确实百变吧?柯里化的起源和闭包的定义是同宗同源。正如前文最开始所说,柯里化是闭包的一对“孪生子”。 f5d872902efd115.gif我们对闭包的解释:“闭包是一个函数内有另外一个函数,内部的函数可以访问外部函数的变量,这样的语法结构是闭包。”与我们对柯里化的解释“把接受多个参数的函数变换成接受一个单一参数(或部分)的函数,并且返回接受余下的参数和返回结果的新函数的技术”,这两种说法几乎是“等效的”,只是从不同角度对同一问题作出的解释,就像 lambda 演算和图灵机对希尔伯特第十问题的解释一样。 同一问题:指的是在 lambda 演算诞生之时,提出的:怎样用 lambda 演算实现接收多个参数? 防抖与节流好了,我们再来看看除了其它高阶函数中闭包思想(柯里化思想)的应用。首先是最最常用的防抖与节流函数。 防抖:就像英雄联盟的回城键,按了之后,间隔一定秒数才会执行生效。 functiondebounce(fn,delay){ delay=delay|| lettimer=null; returnfunction(){ letarg=arguments; //每次操作时,清除上次的定时器 clearTimeout(timer); timer=null; //定义新的定时器,一段时间后进行操作 timer=setTimeout(function(){ fn.apply(this,arg); },delay); } }; varcount= window.onscroll=debounce(function(e){ console.log(e.type,++count);//scroll },500); 节流函数:就像英雄联盟的技能键,是有 CD 的,一段时间内只能按一次,按了之后就要等 CD; //函数节流,频繁操作中间隔delay的时间才处理一次 functionthrottle(fn,delay){ delay=delay|| lettimer=null; //每次滚动初始的标识 lettimestamp= returnfunction(){ letarg=arguments; letnow=Date.now(); //设置开始时间 if(timestamp===0){ timestamp= } clearTimeout(timer); timer=null; //已经到了delay的一段时间,进行处理 if(now-timestamp=delay){ fn.apply(this,arg); timestamp= } //添加定时器,确保最后一次的操作也能处理 else{ timer=setTimeout(function(){ fn.apply(this,arg); //恢复标识 timestamp= },delay); } } }; varcount= window.onscroll=throttle(function(e){ console.log(e.type,++count);//scroll },500); 代码均可复制到控制台中测试。在防抖和节流的场景下,被预先固定住的变量是timer。 lodash 高阶函数lodash 大家肯定不陌生,它是最流行的 JavaScript 库之一,透过函数式编程模式为开发者提供常用的函数。 其中有一些封装的高阶函数,让一些平平无奇的普通函数也能有相应的高阶功能。 举几个例子: //防抖动 _.debounce(func,[wait=0],[options={}]) //节流 _.throttle(func,[wait=0],[options={}]) //将一个断言函数结果取反 _.negate(predicate) //柯里化函数 _.curry(func,[arity=func.length]) //部分应用 _.partial(func,[partials]) //返回一个带记忆的函数 _.memoize(func,[resolver]) //包装函数 _.wrap(value,[wrapper=identity]) 研究源码你就会发现,_.debounce 防抖、_.throttle 节流上面说过,_.curry 柯里化上面说过、_.partial 在“缓存传参”里说过、_.memoize 在“缓存计算”里也说过...... 再举一个例子: 现在要求一个函数在达到 n 次之前,每次都正常执行,第 n 次不执行。 也是非常常见的业务场景!JavaScript 实现: functionbefore(n,func){ letresult,count= returnfunction(...args){ count=count-1 if(count0)result=func.apply(this,args) if(count=1)func=undefined returnresult } } constfn=before(3,(x)=console.log(x)) fn(1)//1 fn(2)//2 fn(3)//不执行 反过来:函数只有到 n 次的时候才执行,n 之前的都不执行。 functionafter(n,func){ letcount=n||0 returnfunction(...args){ count=count-1 if(count1)returnfunc.apply(this,args) } } constfn=after(3,(x)=console.log(x)) fn(1)//不执行 fn(2)//不执行 fn(3)//3 全是“闭包”、全是把参数“柯里化”。 细细体会,在控制台上敲一敲、改一改、跑一跑,下次或许你就可以自己写出这些有特定功能的高阶函数了。 结语综合以上,可见由函数式启发的“闭包”、“柯里化”思想对 JavaScript 有多重要。几乎所有的高阶函数都离不开闭包、参数由多转逐一的柯里化传参思想。所在在很多面试中,都会问闭包,不管是一两年、还是三五年经验的前端程序员。定义一个前端的 JavaScript 技能是初级,还是中高级,这是其中很重要的一个判断点。 对闭包概念模糊不清的、或者只会背概念的 = 初级 会写防抖、节流、或柯里化等高阶函数的 = 中级 深刻理解高阶函数封装思想、能自主用闭包封装高阶函数 = 高级 OK,以上便是本篇分享,专栏第 2 篇,希望各位工友喜欢~ 欢迎点赞、收藏、评论 ?? 后文会再深入 JavaScript 函数式编程,展开讲解:纯函数、延迟处理、JS 迭代器等、敬请期待~ image.png关注专栏# JavaScript 函数式编程精要 —— 签约作者安东尼[2] 我是掘金安东尼 ?? 100 万人气前端技术博主 ?? INFP 写作人格坚持 1000 日更文 ? 关注我,安东尼陪你一起度过漫长编程岁月 ?? [1]https://juejin.cn/column/7140154838981017613:https://juejin.cn/column/7140154838981017613 [2]https://juejin.cn/column/7140154838981017613:https://juejin.cn/column/7140154838981017613 阅读原文

上一篇:2025-06-08_北京影像人,集合时间到了! 下一篇:2025-06-11_一个vue3指令让el-table自动轮播

TAG标签:

14
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价