当前位置: 首页 > 后端技术 > Node.js

尾调用与尾递归

时间:2023-04-03 20:13:32 Node.js

尾调用一、定义尾调用是函数式编程中一个非常重要的概念。当一个函数执行时,最后一步是返回另一个函数的调用,称为尾调用。注意这里不管函数怎么调用,有以下几种方法:函数调用:func(···)方法调用:obj.method(···)call调用:func.call(···)apply调用:func.apply(···)只有以下表达式将包含尾调用:条件运算符:?:逻辑或:||逻辑和:&&逗号:,例如:consta=x=>x?f():g();//f()和g()都在最后。consta=()=>f()||g();//g()可能是尾调用,f()不是//因为上面的写法等价于下面的:consta=()=>{constfResult=f();//不是尾调用if(fResult){returnfResult;}else{返回g();//尾部调用}}//只有当f()的结果为假时,g()才是尾部调用consta=()=>f()&&g();//g()可能是尾部调用,f()不是//因为上面的写法等同于以下内容:consta=()=>{constfResult=f();//不是尾调用if(fResult){returng();//尾调用}else{returnfResult;}}//仅当f()的结果为真时,g()为尾调用consta=()=>(f(),g());//g()为尾调用//因为上面的写法等同于:consta=()=>{f();returng();}2.尾调用优化函数在被调用时会在调用栈(callstack)中存储记录,每条记录称为一个调用帧(callframe),每次调用一个函数,一条记录被压入栈中,函数执行完后,一个一个弹出,直到调用栈清空,如下图:functionfoo(){console.log(111);}函数栏(){foo();}functionbaz(){bar();}巴兹();这个结果是因为每个函数在调用另一个函数的时候都没有返回调用,所以JS引擎会认为你还没有执行完,你的调用帧就会被保留下来。bar()函数是在baz()中调用的,没有return调用,所以在调用栈中保留了自己的调用帧,同时bar()函数的调用帧在baz()中生成调用堆栈。同理,bar()函数也会调用foo()函数,当foo()函数最终执行时,不会再调用其他函数。这里没有return的声明,所以这里默认returnundefined。foo()执行后,销毁自身在调用栈中的记录,依次销毁bar()和baz()的调用帧,最终完成整个过程。如果将上面的例子修改如下:functionfoo(){console.log(111);}functionbar(){returnfoo();}functionbaz(){returnbar();}巴兹();这里注意:尾调用优化只在严格模式下可用。在非严格模式下,大多数引擎都会包含以下两个属性,供开发者查看调用栈:在尾调用优化中,这些属性不再有用,因为相关信息也可能被删除。因此,严格模式(strictmode)禁止了这些属性,尾调用优化只在严格模式下有效。如果尾调用优化生效,流程图会是这样:内部函数直接使用调用记录可以代替外部函数的调用记录,调用栈中始终只保留一个调用帧。这称为尾调用优化。如果所有的函数都是尾调用,调用栈中永远只有一个调用帧,这样会节省很大一部分内存,这也是尾调用优化的意义所在。尾递归1.定义先来看递归。当一个函数调用自身时,它被称为递归。functionfoo(){foo();}上面的操作称为递归,但是注意这里没有结束条件,是死递归,所以会报栈溢出错误。写代码的时候一定要给递归加上结束条件。那么什么是尾递归呢?之前我们知道尾调用的概念。当函数尾部调用自身时,称为尾部递归。functionfoo(){returnfoo();}2、尾递归和递归有什么区别?让我们看一下下面的阶乘示例:functionfactorial(num){if(num===1)return1;返回num*factorial(num-1);}factorial(5);//120阶乘(10);//3628800阶乘(500000);//UncaughtRangeError:Maximumcallstacksizeexceeded上面是一个使用递归计算阶乘的例子。操作系统为JS引擎调用栈分配的内存是有大小限制的。如果计算出来的数量足够大,超过了内存的最大范围,就会出现栈溢出错误。这里500000不是临界值,但我使用了一个足以导致堆栈溢出的数字。如果使用尾递归来计算阶乘呢?'usestrict';functionfactorial(num,total){if(num===1)返回总计;返回阶乘(num-1,num*total);}阶乘(5,1);//120阶乘(10,1);//3628800阶乘(500000,1);//视情况而定//注意这里虽然开启了严格模式,但是经过测试,在Chrome和Firefox下,依然会报栈溢出错误,并且没有进行tailcall优化//Safari浏览器对tail进行了优化call,factorial(500000,1)的结果是Infinity,因为结果超出了JS所能表示的数字范围//如果在nodev6版本下执行,需要加上--harmony_tailcalls参数,node--harmony_tailcallstest.js//最新版本的node已经去掉--harmony_tailcalls函数通过尾递归,我们把复杂度从O(n)降低到O(1),如果数据足够大,会省很多计算时间。可见尾调用优化对于递归操作意义重大,所以一些函数式编程语言将其写进了语言规范。避免重写递归函数尾递归实现往往需要重写递归函数,以保证最后一步只调用自身。为此,需要重写函数内部使用的所有中间变量作为函数的参数,就像上面重写factorial()函数一样。这样做的缺点是语义不明显。计算阶乘函数,为什么需要额外传入一个参数total?有两种方法可以解决这个问题:1、ES6参数默认值'usestrict';functionfactorial(num,total=1){if(num===1)返回总计;返回阶乘(num-1,num*total);}阶乘(5);//120阶乘(10);//36288002.使用符合语义的函数调用重写的尾递归函数functiontailFactorial(num,total){if(num===1)returntotal;返回tailFactorial(num-1,num*total);}functionfactorial(num){returntailFactorial(num,1);}factorial(5);//120阶乘(10);//上面的3628800其实有点类似于柯里化函数,但是并不完全符合柯里化的概念。函数柯里化是指将一个接受多个参数的函数转换为一个接受单个参数(原函数的第一个参数)的函数,并返回一个接受其余参数并返回结果的新函数。概念看着比较乱,举个例子感受一下://普通加法函数functionadd(x,y,z){returnx+y+z;}add(1,2,3);//6//重写为柯里化加法函数functionadd(x){returnfunction(y){returnfunction(z){returnx+y+z;}}}添加(1)(2)(3);//6可以看出柯里化函数通过闭包找到父作用域中的变量,最后将输出结果一一相加。通过这个例子,你可能看不出为什么要使用柯里化,有什么好处。这个我们以后再说,这里先说一个概念。它是一种将接受多个参数的函数转换为接受单个参数(原函数的第一个参数)的函数,并返回一个接受其余参数并返回结果的新函数的技术。如果你用柯里化重写阶乘例子://Curryfunctionfunctioncurry(fn){var_fnArgLength=fn.length;functionwrap(...args){var_args=args;var_argLength=_args.length;//如果所有参数传完,直接返回fncallif(_fnArgLength===_argLength){returnfn.apply(null,args);}functionact(...args){_args=_args.concat(args);如果(_args.length===_fnArgLength){返回fn.apply(null,_args);}返回行为;}返回行为;}returnwrap;}//尾递归函数functiontailFactorial(num,total){if(num===1)returntotal;returntailFactorial(num-1,num*total);}//重写varfactorial=curry(tailFactorial);factorial(5)(1);//120阶乘(10)(1);//3628800这是一种符合柯里化理念的写法。在阮一峰老师的文章中是这样写的:functioncurrying(fn,n){returnfunction(m){returnfn.call(this,m,n);};}functiontailFactorial(n,total){if(n===1)returntotal;返回tailFactorial(n-1,n*total);}constfactorial=currying(tailFactorial,1);factorial(5)//120我个人认为这种写法其实不算柯里化,因为多参数的tailFacrotial是重写为接受单个参数,但写法不同,其含义与以下相同:functionfactorial(num){returntailFactorial(num,1);}functiontailFactorial(num,total){if(num===1)返回总数;返回tailFactorial(num-1,num*total);}factorial(5);//120阶乘(10);//3628800本文结束我们主要讨论了尾调用优化和Curry需要注意的是,经过测试,Chrome和Firefox没有优化尾调用,而Safari优化了尾调用。更高版本的Node还删除了通过--harmony_tailcalls参数启用尾调用优化。有什么问题欢迎留言讨论,附上我的博客网址,快来哦~~欢迎关注我的公众号参考链接http://www.ruanyifeng.com/blo...https://觉进。im/post/5a4d89…https://github.com/lamdu/lamd…