建设网站主机要买什么的好,品牌推广的渠道有哪些,交换友情链接的途径有哪些,京山网站开发尾调用1. 定义尾调用是函数式编程中一个很重要的概念#xff0c;当一个函数执行时的最后一个步骤是返回另一个函数的调用#xff0c;这就叫做尾调用。注意这里函数的调用方式是无所谓的#xff0c;以下方式均可#xff1a;函数调用: func()方法调用: obj.method()call调用:… 尾调用1. 定义尾调用是函数式编程中一个很重要的概念当一个函数执行时的最后一个步骤是返回另一个函数的调用这就叫做尾调用。注意这里函数的调用方式是无所谓的以下方式均可函数调用: func(···)方法调用: obj.method(···)call调用: func.call(···)apply调用: func.apply(···)并且只有下列表达式会包含尾调用条件操作符: ? :逻辑或: ||逻辑与: 逗号: ,依次举例const a x x ? f() : g();// f() 和 g() 都在尾部。const a () f() || g();// g()有可能是尾调用f()不是// 因为上述写法和下面的写法等效const a () { const fResult f(); // not a tail call if (fResult) { return fResult; } else { return g(); // tail call }}// 只有当f()的结果为falsey的时候g()才是尾调用const a () f() g();// g()有可能是尾调用f()不是// 因为上述写法和下面的写法等效const a () { const fResult f(); // not a tail call if (fResult) { return g(); // tail call } else { return fResult; }}// 只有当f()的结果为truthy的时候g()才是尾调用const a () (f() , g());// g()是尾调用// 因为上述写法和下面的写法等效const a () { f(); return g();}2. 尾调用优化函数在调用的时候会在调用栈(call stack)中存有记录每一条记录叫做一个调用帧(call frame)每调用一个函数就向栈中push一条记录函数执行结束后依次向外弹出直到清空调用栈参考下图function foo () { console.log(111); }function bar () { foo(); }function baz () { bar(); }baz();造成这种结果是因为每个函数在调用另一个函数的时候并没有 return 该调用所以JS引擎会认为你还没有执行完会保留你的调用帧。baz() 里面调用了 bar() 函数并没有 return 该调用所以在调用栈中保持自己的调用帧同时 bar() 函数的调用帧在调用栈中生成同理bar() 函数又调用了 foo() 函数最后执行到 foo() 函数的时候没有再调用其他函数这里没有显示声明 return所以这里默认 return undefined。foo() 执行完了销毁调用栈中自己的记录依次销毁 bar() 和 baz() 的调用帧最后完成整个流程。如果对上面的例子做如下修改function foo () { console.log(111); }function bar () { return foo(); }function baz () { return bar(); }baz();这里要注意尾调用优化只在严格模式下有效。在非严格模式下大多数引擎会包含下面两个属性以便开发者检查调用栈func.arguments: 表示对 func最近一次调用所包含的参数func.caller: 引用对 func最近一次调用的那个函数在尾调用优化中这些属性不再有用因为相关的信息可能以及被移除了。因此严格模式(strict mode)禁止这些属性并且尾调用优化只在严格模式下有效。如果尾调用优化生效流程图就会变成这样我们可以很清楚的看到尾调用由于是函数的最后一步操作所以不需要保留外层函数的调用记录只要直接用内层函数的调用记录取代外层函数的调用记录就可以了调用栈中始终只保持了一条调用帧。这就叫做尾调用优化如果所有的函数都是尾调用的话那么在调用栈中的调用帧始终只有一条这样会节省很大一部分的内存这也是尾调用优化的意义。尾递归1. 定义先来看一下递归当一个函数调用自身就叫做递归。function foo () { foo();}上面这个操作就叫做递归但是注意了这里没有结束条件是死递归所以会报栈溢出错误的写代码时千万注意给递归添加结束条件。那么什么是尾递归前面我们知道了尾调用的概念当一个函数尾调用自身就叫做尾递归。function foo () { return foo();}2. 作用那么尾递归相比递归而言有哪些不同呢我们通过下面这个求阶乘的例子来看一下function factorial (num) { if (num 1) return 1; return num * factorial(num - 1);}factorial(5); // 120factorial(10); // 3628800factorial(500000); // Uncaught RangeError: Maximum call stack size exceeded上面是使用递归来计算阶乘的例子操作系统为JS引擎调用栈分配的内存是有大小限制的如果计算的数字足够大超出了内存最大范围就会出现栈溢出错误。这里500000并不是临界值只是我用了一个足够造成栈溢出的数。如果用尾递归来计算阶乘呢use strict;function factorial (num, total) { if (num 1) return total; return factorial(num - 1, num * total);}factorial(5, 1); // 120factorial(10, 1); // 3628800factorial(500000, 1); // 分情况// 注意虽然说这里启用了严格模式但是经测试在Chrome和Firefox下还是会报栈溢出错误并没有进行尾调用优化// Safari浏览器进行了尾调用优化factorial(500000, 1)结果为Infinity因为结果超出了JS可表示的数字范围// 如果在node v6版本下执行需要加--harmony_tailcalls参数node --harmony_tailcalls test.js// node最新版本已经移除了--harmony_tailcalls功能通过尾递归我们把复杂度从O(n)降低到了O(1)如果数据足够大的话会节省很多的计算时间。由此可见尾调用优化对递归操作意义重大所以一些函数式编程语言将其写入了语言规格。避免改写递归函数尾递归的实现往往需要改写递归函数确保最后一步只调用自身。要做到这一点需要把函数内部所有用到的中间变量改写为函数的参数就像上面的factorial()函数改写一样。这样做的缺点就是语义不明显要计算阶乘的函数为什么还要另外传入一个参数叫total解决这个问题的办法有两个1. ES6参数默认值use strict;function factorial (num, total 1) { if (num 1) return total; return factorial(num - 1, num * total);}factorial(5); // 120factorial(10); // 36288002. 用一个符合语义的函数去调用改写后的尾递归函数function tailFactorial (num, total) { if (num 1) return total; return tailFactorial(num - 1, num * total);}function factorial (num) { return tailFactorial(num, 1);}factorial(5); // 120factorial(10); // 3628800上面这种写法其实有点类似于做了一个函数柯里化但不完全符合柯里化的概念。函数柯里化是指把接受多个参数的函数转换为接受一个单一参数(最初函数的第一个参数)的函数并且返回接受余下参数且返回结果的新函数。概念看着很绕口我们来个例子感受一下// 普通加法函数function add (x, y, z) { return x y z;}add(1, 2, 3); // 6// 改写为柯里化加法函数function add (x) { return function (y) { return function (z) { return x y z; } }}add(1)(2)(3); // 6可以看到柯里化函数通过闭包找到父作用域里的变量最后依次相加输出结果。通过这个例子可能看不出为什么要用柯里化有什么好处这个我们以后再谈这里先引出一个概念。是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数并且返回接受余下的参数且返回结果的新函数的技术。如果用柯里化改写求阶乘的例子// 柯里化函数function curry (fn) { var _fnArgLength fn.length; function wrap (...args) { var _args args; var _argLength _args.length; // 如果传的是所有参数直接返回fn调用 if (_fnArgLength _argLength) { return fn.apply(null, args); } function act (...args) { _args _args.concat(args); if (_args.length _fnArgLength) { return fn.apply(null, _args); } return act; } return act; } return wrap;}// 尾递归函数function tailFactorial (num, total) { if (num 1) return total; return tailFactorial(num - 1, num * total);}// 改写var factorial curry(tailFactorial);factorial(5)(1); // 120factorial(10)(1); // 3628800这是符合柯里化概念的写法在阮一峰老师的文章中是这样写的function currying(fn, n) { return function (m) { return fn.call(this, m, n); };}function tailFactorial(n, total) { if (n 1) return total; return tailFactorial(n - 1, n * total);}const factorial currying(tailFactorial, 1);factorial(5) // 120我个人认为这种写法其实不是柯里化因为并没有将多参数的tailFacrotial改写为接受单参数的形式只是换了一种写法和下面这样写意义是一样的function factorial (num) { return tailFactorial(num, 1);}function tailFactorial (num, total) { if (num 1) return total; return tailFactorial(num - 1, num * total);}factorial(5); // 120factorial(10); // 3628800结束这篇文章我们主要讨论了尾调用优化和柯里化。要注意的是经过测试Chrome和Firefox并没有对尾调用进行优化Safari对尾调用进行了优化。Node高版本也已经去除了通过--harmony_tailcalls参数启用尾调用优化。有任何问题欢迎大家留言讨论另附我的博客网站快来呀~~欢迎关注我的公众号参考链接http://www.ruanyifeng.com/blog/2015/04/tail-call.html https://juejin.im/post/6844903544756125704 https://github.com/lamdu/lamdu/issues/90