ES6 规范中添加了对尾调用优化的支持,虽然目前来看支持情况还不是很好,但了解其原理还是很有必要的,有助于我们编写高效的代码,一旦哪天引擎支持该优化了,将会收获性能上的提升。 讨论尾调用前,先看函数正常调用时其形成的堆栈(stack frame)情况。 函数的调用及调用堆栈先看一个概念:调用堆栈(call stack),或叫调用帧(stack frame)。 我理解的调用堆栈:因为函数调用后,流程会进入到被调用函数体,此时需要传入一些入参,这些入参需要存放到一个位置,另外当函数调用结束后,执行流程需要返回到被调用的地方,这个返回的地方也需要被记录下来。所以会为这次函数调用分配一个存储区域,存放调用时传入的入参,调用结束后返回的地址。这个存储区域保存的都是跟本次函数调用相关的数据,所以,它是该函数的调用堆栈。 考察下面的示例代码: function g(x) { return x; // (C) } function f(a) { let b = a + 1; return g(b); // (B) } console.log(f(2)); // (A) 下面模拟 JaavScript 引擎在执行上面代码时的流程,
尾调用如果一个函数最后一步是调用另一个函数,则这个调用为尾调用。比如: function g(x) { return x; } function f(y) { var a = y + 1; return g(a); } 但下面这个就不算尾调用了: function g(x) { return x; } function f(y) { var a = y + 1; return g(a) + 1; } 因为这里 function g(x) { return x; } function f(y) { var a = y + 1; var tmp = g(a); var result = tmp + 1; return result; } 经过改造后就能很明显看出,其中对 尾调用的判定函数的调用形式首先,JavaScript 中调用函数是多样的,只要这是另一函数中最后一步操作,都可称作尾调用。比如以下的函数调用形式:
表达式中的尾调用因为剪头函数的函数体可以是表达式,所以表达式最终的步骤是什么决定了该剪头函数中是否包含尾调用。 能够形成尾调用的表达式有如下这些,
const a = x => x ? f() : g(); 其中
const a = () => f() || g(); 上面示例中, const a = () => { let fResult = f(); // not a tail call if (fResult) { return fResult; } else { return g(); // tail call } };
所以从上面的转换中看出,对于
const a = () => f() && g(); 与逻辑或表达式雷同,这里需要对 const a = () => { let fResult = f(); // not a tail call if (!fResult) { return fResult; } else { return g(); // tail call } };
const a = () => (f() , g()); 这个就比较好理解了,逗号表达式是依次执行的,整个表达式返回结果为最后一个表达式。所以这里 需要注意的是,单独的函数调用并不是尾调用,比如下面这样: function foo() { bar(); // this is not a tail call in JS } 这里 function foo() { bar(); return undefined; } 像这种情况其实并不能简单地通过加一个 尾调用优化回到最开始的那个示例: function g(x) { return x; // (C) } function f(a) { let b = a + 1; return g(b); // (B) } console.log(f(2)); // (A) 这里 如果仔细看前面调用过程的分析,会发现,在 所以,完全可以省掉 上面优化后的执行场景下,其调用堆栈的分配则变成了:
最最开始不同之处在于,在创建 其好处显而易见,在函数连续调用过程中,堆栈数没有增加。假如不止一次尾调用, 利用这个特性,我们可以将一些不是尾调用的函数想办法改成尾调用,达到优化调用堆栈的目的,这便是尾调用优化。 尾递归调用如果函数的尾调用是对自己的调用,便形成了递归调用,同时还是归调用,所以合称 尾递归调用。相比于传统的递归,调用堆栈极速增加的不同,尾递归调用的调用堆栈是恒定的,这由前面的分析可知。 所以尾调用优化特别适合用于递归的情况,收益会很大。 计算阶乘就是典型的递归场景: function factorial(x) { if (x <= 0) { return 1; } else { return x * factorial(x-1); // (A) } } 但上面这样的实现并不是尾调用。不过可以通过添加一个额外的方法来达到优化成尾调用的目的。 function factorial(n) { return facRec(n, 1); } function facRec(x, acc) { if (x <= 1) { return acc; } else { return facRec(x-1, x*acc); // (A) } } 此时 其他示例利用尾递归调用实现循环语句。 function forEach(arr, callback, start = 0) { if (0 <= start && start < arr.length) { callback(arr[start], start, arr); return forEach(arr, callback, start+1); // tail call } } forEach([‘a‘, ‘b‘], (elem, i) => console.log(`${i}. ${elem}`)); // Output: // 0. a // 1. b function findIndex(arr, predicate, start = 0) { if (0 <= start && start < arr.length) { if (predicate(arr[start])) { return start; } return findIndex(arr, predicate, start+1); // tail call } } findIndex([‘a‘, ‘b‘], x => x === ‘b‘); // 1 相关资源 |
原文地址:https://www.cnblogs.com/Wayou/p/tail_call_optimization.html