javascript专题系列--尾调用和尾递归
最近在看《冴羽的博客》,讲真,确实受益匪浅,已经看了javascript 深入系列和专题系列的大部分文章,可是现在才想起来做笔记。所以虽然很多以前面试被问得一脸懵逼的问题都被“一语惊醒梦中人”过,注意这里我说的是“过”。是的,这些知道点,当时看的时候跟着大佬的思维,确实当时感觉“哦~ 原来是这样”,但是,看了下篇把上篇的知识忘了还是让我感觉自己太挫了。
于是,决定写点笔记来加深一点印象吧!
今天看到了“javascript专题之递归” (https://github.com/mqyqingfeng/Blog/issues/49) 这篇,所以就从这里开始吧!
在这里我再一次看到了尾调用和尾递归这俩个的关键字,距离前一次看到好像还是很多年前的事了,但是当时完全不明所以啊...,但是,今天,结合了之前的执行上下文 和 执行上下文栈的知识,总算明白这到底是个啥了!
尾调用,是指函数内部的最后一个动作是函数调用。该调用的返回值,直接返回给函数。
尾递归,函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
至于为什么很多场景要提倡使用尾调用,为什么使用尾调用和尾递归会有更好的性能呢?,就涉及到了执行上下文栈 的知识。https://github.com/mqyqingfeng/Blog/issues/4:当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。 所以我理解是:也正是因为如此执行上下文被弹出,从而释放了内存,所以性能就得到了提升吧!
尾调用:
// 尾调用 function f(x) { return g(x); }
注意:
// 非尾调用 function f(x) { return g(x) + 1; }
因为g(x)的返回值还需要跟1进行计算后,f(x)才会返回值。
两者仅仅是 + 1这一点点的区别,有什么不一样呢?答案就是执行上下文栈的变化不一样。
为了模拟执行上下文栈的行为,让我们定义执行上下文栈是一个数组:
ECStack = [];
尾调用的执行过程:
// 模拟的伪代码: ECStack.push(<f> functionContext); ECStack.pop(); ECStack.push(<g> functionContext); ESStack.pop();
非尾调用的执行过程:
ECStack.push(<f> functionContext); ECStack.push(<g> functionContext); ECStack.pop(); ECStack.pop();
由此可以看到尾调用和非尾调用都是需要调用f()函数和g()函数,但是,尾调用,当调用了f()之后,马上就释放了f()的执行上下文,执行g()时候,就只剩下g()的执行上下文了,而非尾调用是先把两个函数先后压入执行上下文栈,待最后一个函数执行完才把两个函数的执行上下文弹出(在没有执行完最后一个函数之前,前面的函数执行上下文一直占据着内存)。
阶乘函数:
// 非尾调用写法: function factorial(n) { if (n == 1) return n; return n * factorial(n - 1); } factorial(5) // --> 5 * 4 * 3 * 2 * 1 = 120 // 尾调用优化写法: function factorial(n, res) { if (n == 1) return res; return factorial(n - 1, n * res) }