-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
JavaScript专题之递归 #49
Comments
阶乘函数优化那节,factorial中的factorial多打了个2,newFactorial(5) // 24这个也手误了,newFactorial函数用的也不是你链接中的curry,而是partial |
@SilenceZeng 非常感谢指出~ 已经修改,以后写文章的时候会更加严谨一些~ |
终于明白了尾递归,没白来 |
如下场景:
在chrome中跑了下,那个控制台 -> Sources -> Call Stack中观察到,f(x) 和g(x)都在stack中,并没有出现如下这种情况啊
|
@coderLius 怎么观察的,我怎么看不到? |
@coderLius 很抱歉之前没有看到这个问题,V8 并没有部署尾递归优化,所以其实从 Call Stack 中看不到期望的效果 |
@jasonzhangdong 在这里 |
看了你这张截图,然后试着把数据跑出来,看着数据的变化,感觉更能理解这儿的this,执行上下文了。 |
@qujsh 感谢建议~ 以后可以写一个工具系列~ 哈哈 |
@mqyqingfeng 如果真的能够写一个算法系列,那真是大大的好啊! |
@hazxy 算法系列真是任重而道远呐~ |
执行上下文栈和函数调用栈是一个东西吧 |
@mqyqingfeng function factorial(n, res) {
if (n == 1) return res;
return factorial(n - 1, n * res)
}
function factorial2(n) {
if (n == 1) return n;
return n * factorial(n - 1)
}
console.time('尾递归');
factorial(10000, 1)
console.timeEnd('尾递归');
console.time('正常递归');
factorial2(10000)
console.timeEnd('正常递归'); 在chrome下,尾递归的性能通常较好,偶尔性能劣于正常的递归; |
@Tan90Qian 代码写错了哈…… function factorial(n, res) {
if (n == 1) return res;
return factorial(n - 1, n * res)
}
function factorial2(n) {
if (n == 1) return n;
// 这里应该是 factorial2
return n * factorial2(n - 1)
}
console.time('尾递归');
factorial(10000, 1)
console.timeEnd('尾递归');
console.time('正常递归');
factorial2(10000)
console.timeEnd('正常递归'); |
|
@cikeyin 这不是尾递归,这是闭包.朋友,基础很重要啊!!! |
@cikeyin 按照我的理解,这两个地方执行栈都没问题。一个是没有用尾调优化的栈,一个是使用了尾调用优化的栈(现在实际在chrome下跑也暂时还看不到尾调用优化的结果)。可能之前博主在写执行上下文栈的时候更多的重心是在执行上下文栈这块,现在写递归就顺带写了执行栈的优化。 |
关于尾调用刚开始还以为作者说错了 所以又仔细看了看阮老师的http://es6.ruanyifeng.com/#docs/function#%E5%B0%BE%E8%B0%83%E7%94%A8%E4%BC%98%E5%8C%96 |
以前看阮一峰老师的ES6,看到尾调用这边云里雾里,后来看了冴羽大神写的执行环境栈运行原理,回过头看尾调用终于看懂了。 |
|
尾递归学习总结: // 求和
function sum(n, res = 0) {
if (n < 1) return res;
return sum(n - 1, n + res);
}
sum(5); // 15
// 斐波拉契数
function fibonacci(n, sum1 = 1, sum2 = 1) {
if (n <= 2) return sum2;
return fibonacci(n - 1, sum2, sum1 + sum2);
}
fibonacci(5); // 5
// 阶乘
function factorial(n, res = 1) {
if (n <= 1) return res;
return factorial(n - 1, n * res);
} |
ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。博主讲的应该是在严格模式下 |
return g(x) 的时候 f(x)已经结束了,所以f(x)push之后就pop了 |
function f(x){
return g(x);
}
// 非尾调用
function f(x){
return g(x) + 1;
} 谁能帮我解释下这俩的执行上下文栈有啥区别么 |
@tancgo 我理解的出入栈如下:
|
@tancgo 可以看下这个文章开头部分的解释 |
函数调用会产生调用栈 call stack,用于保存函数调用的一些信息,比如局部变量等。非尾调用g(x) + 1,需要留存住这个 + 1,但是尾调用g(x),不需要保留任何信息,直接用当前call stack 取代之前call stack 节省了空间。言语很拙劣,不知道讲清楚了没,😂。 |
关于尾递归,推荐一下文章:https://site.douban.com/196781/widget/notes/12161495/note/262014367/ 虽然有点老... |
个人感觉递归最重要的一点是,函数的名字要具有语义化 |
定义
程序调用自身的编程技巧称为递归(recursion)。
阶乘
以阶乘为例:
示意图(图片来自 wwww.penjee.com):
斐波那契数列
在《JavaScript专题之函数记忆》中讲到过的斐波那契数列也使用了递归:
递归条件
从这两个例子中,我们可以看出:
构成递归需具备边界条件、递归前进段和递归返回段,当边界条件不满足时,递归前进,当边界条件满足时,递归返回。阶乘中的
n == 1
和 斐波那契数列中的n < 2
都是边界条件。总结一下递归的特点:
了解这些特点可以帮助我们更好的编写递归函数。
执行上下文栈
在《JavaScript深入之执行上下文栈》中,我们知道:
当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。
试着对阶乘函数分析执行的过程,我们会发现,JavaScript 会不停的创建执行上下文压入执行上下文栈,对于内存而言,维护这么多的执行上下文也是一笔不小的开销呐!那么,我们该如何优化呢?
答案就是尾调用。
尾调用
尾调用,是指函数内部的最后一个动作是函数调用。该调用的返回值,直接返回给函数。
举个例子:
然而
并不是尾调用,因为 g(x) 的返回值还需要跟 1 进行计算后,f(x)才会返回值。
两者又有什么区别呢?答案就是执行上下文栈的变化不一样。
为了模拟执行上下文栈的行为,让我们定义执行上下文栈是一个数组:
我们模拟下第一个尾调用函数执行时的执行上下文栈变化:
我们再来模拟一下第二个非尾调用函数执行时的执行上下文栈变化:
也就说尾调用函数执行时,虽然也调用了一个函数,但是因为原来的的函数执行完毕,执行上下文会被弹出,执行上下文栈中相当于只多压入了一个执行上下文。然而非尾调用函数,就会创建多个执行上下文压入执行上下文栈。
函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
所以我们只用把阶乘函数改造成一个尾递归形式,就可以避免创建那么多的执行上下文。但是我们该怎么做呢?
阶乘函数优化
我们需要做的就是把所有用到的内部变量改写成函数的参数,以阶乘函数为例:
然而这个很奇怪呐……我们计算 4 的阶乘,结果函数要传入 4 和 1,我就不能只传入一个 4 吗?
这个时候就要用到我们在《JavaScript专题之偏函数》中编写的 partial 函数了:
应用
如果你看过 JavaScript 专题系列的文章,你会发现递归有着很多的应用。
作为专题系列的第十八篇,我们来盘点下之前的文章中都有哪些涉及到了递归:
1.《JavaScript 专题之数组扁平化》:
2.《JavaScript 专题之深浅拷贝》:
3.JavaScript 专题之从零实现 jQuery 的 extend:
4.《JavaScript 专题之如何判断两个对象相等》:
5.《JavaScript 专题之函数柯里化》:
写在最后
递归的内容远不止这些,比如还有汉诺塔、二叉树遍历等递归场景,本篇就不过多展开,真希望未来能写个算法系列。
专题系列
JavaScript专题系列目录地址:https://github.com/mqyqingfeng/Blog。
JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。
The text was updated successfully, but these errors were encountered: