尾调用优化是ES6中出现的一个涉及函数调用的特定化有形式相关的特殊要求,该优化解决的问题举一个常见的例子就是我们使用递归时因为调用递归太多次而报错的情况。如果我们使用尾调用的话就不会有这种问题。
首先来了解以下什么是尾调用
尾调用
尾调用顾名思义,是在函数的最后一步调用另一个函数,如下代码:
function f1(){
return f2();
}
f2是在f1的最后一步调用的,所以这是一个尾调用。
要注意是在最后一步使用调用,如果将调用的函数赋给一个变量,或者在调用时有其他操作,又或者是没有return语句,都不是尾调用,看看下面三个例子
//1
function f1(){
var f=f2();
return f;
}
//2
function f1(){
return 1+f2();
}
//上面的代码可以看成先调用f2,然后再执行+1操作,显然不是最后一步调用
//3
function f1(){
f2();
}
//上面的代码可以看成下面的代码
function f1(){
f2();
return undefined;
}
//这么看的话就很明显了,这是在调用f2后,再隐式返回undefined,所以调用f2不是最后一步
上面三种是经常会被混淆为尾调用的情况,需要好好辨认,而要注意的是,最后一步并不指在函数尾部,如下面代码也是尾调用
function f1(flag){
if(flag){
return f2();
}
else{
return f3();
}
}
要了解尾调用的优化,首先要了解以下函数的调用机制。
函数的调用机制
在调用函数时,会形成一个调用的记录,一般会称为调用帧或者栈帧,我更倾向于栈帧,因为它是以栈结构来使用的,通过下面的代码来理解以下函数的调用
function f1(){
f2();
}
function f2(){
f3();
}
f1();
上面的调用过程如下图,
首先调用了f1,将其压入栈中,然后在f1中调用了f2,将f2压入栈中,相同地将f3压入栈中,f3函数执行完后,将f3弹出,接着将f2弹出,最后将f1弹出。
可以注意到,必须在函数内部调用的函数执行完后,当前函数才会被弹出栈帧,现在这里只有三层调用,所以还没问题,但是如果调用次数过多,引擎是不会允许无限调用的,会有一个限制来界定栈的深度,而超过该深度的调用,就会引发“栈溢出”的错误。(限制不由规范控制,依赖于具体实现,并且根据浏览器和设备的不同而不同)
尾调用就算用来解决这一问题的,接下来我们就最容易出现栈溢出的递归来讲讲尾调用如何避免该问题,即尾递归
尾递归
首先我们通过下面这个计算斐波拉契数列的方法来看看尾递归
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Fibonacci(10) // 89
Fibonacci(100) // 堆栈溢出
Fibonacci(500) // 堆栈溢出
这个方法并没有经过尾调用的优化,在数值过大时会有“栈溢出”的问题,像上面传入100时就报错了。可以通过下面写法来实现尾调用优化。
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity
为什么尾递归就可以不发生“栈溢出”,为什么在最后一步调用函数就不会发送“栈溢出”,这是因为支持TCO的引擎能意识到调用是否“位于尾部”,如果位于尾部的话,就不会在创建一个新的栈帧,而是重用当前的栈帧,即不会把当前调用的函数压入栈中,既然没有压入更多的函数调用,自然就不会有“栈溢出”的情况了。
尾调用优化的注意点
需要注意的是,尾调用优化是有条件的,即在严格模式下才能使用,因为非严格模式中有arguments和caller两个变量可以跟踪函数的调用栈,所以不能在非严格模式下使用。
function restricted() {
'use strict';
restricted.caller; // Uncaught TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
restricted.arguments;
}
restricted();
参考自阮一峰的《ECMAScript6入门》
Kyle Simpson的《你不知道的JavaScript 中卷》
ES6学习笔记目录(持续更新中)