蹦床函数咋就能实现尾调用优化的效果

174 阅读2分钟

一切的起源来自以下递归代码

function sum(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1);
  } else {
    return x;
  }
}

sum(1, 100000)
// Uncaught RangeError: Maximum call stack size exceeded(…)

众所周知,递归调用函数多次,浏览器会报错栈溢出。

这里纠正一个说法,栈溢出不是只在自己调自己的情况下才会出现的。栈溢出本质是栈空间被填满了,也就是说不断的调用新的函数,创建新的栈帧,且旧的栈帧没有被清除掉,达到了浏览器设定的峰值就会抛出的错误。

也就是说不论是相同的函数还是不同的函数递归调用,只要不断的创建新的栈帧,并且让浏览器无法清除旧的栈帧就可以达到让浏览器抛出栈溢出错误的目的。

以下是我瞎写的测试代码,以佐证我上述观点,有兴趣的可以试下。

    const j = 0;
    window[`fn${j}`] = (j) => {
      console.log(j);
      if (j < 10000) {
        ++j;
        window[`fn${j}`] = window[`fn${--j}`];
        window[`fn${j}`](j);
      }
    };
    fn0(j);

最近在读阮一峰老师写的es6入门,看到了尾调用相关的优化。读到了蹦床函数这样一段代码,所以才有了疑惑,为什么这样写就可以避免栈溢出?

下面是阮一峰老师写的蹦床函数,我运行了一遍,发现如果我去掉bind函数的使用,就是会报出栈溢出的错误,由此可见蹦床函数实现尾调用优化的关键在于bind。

function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}
function sum(x, y) {
  if (y > 0) {
    return sum.bind(null, x + 1, y - 1);
  } else {
    return x;
  }
}
trampoline(sum(1, 100000))
// 100001

下面截图中可以看到不使用bind情况下,x当前为8,初始为1,代表我调用了8次sum函数,call stack中就会存在8个sum函数的栈帧。也就是说我如果继续执行下去,就会不断的加入新的栈帧,达到浏览器设置的阈值之后就会报出栈溢出的错误。

image.png

而使用bind的情况下,调用了13次sum函数,但是call stack中却只有一个sum函数的栈帧。

image.png

这是什么情况呢,原来是因为使用bind时会创建一个新的函数,并且传入新函数的x,y的值跟父函数也没有任何关系。所以在新的子函数调用之前,父函数已经完成他的使命被销毁掉了,也就保证了同一时间栈空间里只会存在一个栈帧在运行函数,所以不会触发浏览器的栈溢出错误,实现尾调用优化。

以上是本人查阅资料并结合自身理解得出的结论,如有错误,欢迎指正!