在 ECMAScript 2017 (ES8) 中有一项重要的优化技术,叫做尾调用优化。它可以使一些函数在调用时不会增加新的调用帧,大大减少了函数调用栈的深度,有效优化了函数的性能。
什么是尾调用优化
尾调用是指一个函数的最后一个动作是调用另一个函数。尾调用转化是指编译器或者解释器把这个函数调用转化为 goto 命令,而不是通过保存当前函数的调用帧和返回地址实现函数调用和返回。这意味着,对于一个包含多个尾调用的函数,编译器只需要保留一个调用帧和返回地址,而不是每个调用都需要保存,从而减少了函数调用栈的深度。
尾递归
尾递归是一种特殊的尾调用,在递归函数中使用尾递归可以避免爆栈的情况,从而降低内存的使用。这是因为在尾递归中,函数返回时不需要保留当前函数的调用帧和返回地址,因此可以有效地节省内存。
如何使用尾调用优化
在 ES6 中,尾调用优化只能由 JavaScript 引擎自行实现,而在 ES7 中,尾调用优化被正式纳入 ECMAScript 规范,并定义了相关的语法,使得程序员可以直接声明一个函数是否需要采用尾调用优化。
如果想要在函数中使用尾调用优化,可以通过在函数定义的末尾添加 return
关键字来告诉编译器该函数存在尾调用优化的可能。
例如下面的代码片段中使用了尾递归来计算一个数字的阶乘:
function factorial(n, acc = 1) { if (n === 0) return acc; return factorial(n - 1, n * acc); }
示例
下面我们来看一个例子,假设我们要计算一个数组中所有数字的平方和。我们可以编写下面的代码:
function sumOfSquares(arr) { if (arr.length === 0) return 0; const [head, ...tail] = arr; const squared = head * head; return squared + sumOfSquares(tail); }
这个函数使用递归的方式来计算数组中所有数字的平方和,每次递归调用时传入一个缩短了一项的数组,并计算当前项的平方,最后将结果累加起来。
然而,由于递归调用在每个调用帧中都会保存一份当前的状态,容易出现栈溢出的情况,导致程序无法继续执行。为了避免这种情况的发生,我们可以使用尾递归来改写这个函数,代码如下:
function sumOfSquares(arr, acc = 0) { if (arr.length === 0) return acc; const [head, ...tail] = arr; const squared = head * head; return sumOfSquares(tail, acc + squared); }
在这个改写后的函数中,我们使用了一个额外的参数 acc
来存储当前的状态,避免了递归调用时保存调用帧导致的性能问题。
总结
尾调用优化是 ECMAScript 2017 规范中的一个重要优化技术,可以优化函数调用栈深度,提升函数的性能。在函数中使用尾递归可以避免递归调用时出现栈溢出的情况。然而,尾调用优化并不是所有 JavaScript 引擎都支持的功能,因此需要谨慎使用。
来源:JavaScript中文网 ,转载请注明来源 https://www.javascriptcn.com/post/64f16594f6b2d6eab3b3b432