如何在JavaScript中了解蹦床?
这里是代码:
function repeat(operation, num) { return function() { if (num <= 0) return operation() return repeat(operation, --num) } } function trampoline(fn) { while(fn && typeof fn === 'function') { fn = fn() } } module.exports = function(operation, num) { trampoline(function() { return repeat(operation, num) }) }
我已经读过蹦床是用来处理溢出问题的,所以这个函数不会只是保持自己的调用和堆栈。
但是这个片段的function如何呢? 特别是trampoline
function? 它究竟做了什么,它是如何完成它的目标?
感谢您的任何帮助 :)
while
循环将继续运行,直到条件是错误的。
fn && typeof fn === 'function'
如果fn
本身是伪造的,或者如果fn
是函数以外的其他东西,那么fn && typeof fn === 'function'
将是falsy。
上半场实际上是多余的,因为谬误值也不是function。
蹦床只是一种优化recursion的技巧,并且在不支持JavaScript ES5实现和C#等tail call optimization
语言中防止堆栈溢出exception。 但是,ES6可能会支持尾部呼叫优化。
常规recursion的问题是每个recursion调用都会将堆栈框架添加到调用堆栈,您可以将其视为一个调用金字塔 。 这是recursion地调用阶乘函数的可视化:
(factorial 3) (* 3 (factorial 2)) (* 3 (* 2 (factorial 1))) (* 3 (* 2 (* 1 (factorial 0)))) (* 3 (* 2 (* 1 1))) (* 3 (* 2 1)) (* 3 2) 6
这里是每个垂直破折号是堆栈帧的堆栈的可视化:
---|--- ---| |--- ---| |--- --- ---
问题是堆栈的大小有限,堆叠这些堆栈帧可能会溢出堆栈。 根据堆栈大小,更大的因子的计算会溢出堆栈。 这就是为什么在C#,Javascript等常规recursion可能被认为是危险的 。
一个最佳的执行模式就像是一个蹦床,而不是一个金字塔,每个recursion调用都是在原地执行的,并且不会叠加在调用堆栈上。 支持尾部呼叫优化的语言中的执行可能如下所示:
(fact 3) (fact-tail 3 1) (fact-tail 2 3) (fact-tail 1 6) (fact-tail 0 6) 6
你可以像弹跳的蹦床一样可视化堆栈:
---|--- ---|--- ---|--- --- --- ---
这显然是更好的,因为堆栈总是只有一个框架,从可视化,你也可以看到为什么它被称为蹦床。 这可以防止堆栈溢出。
由于我们没有在Javascript中使用tail call optimization
,所以我们需要找出一种方法将常规recursion转化为优化的版本,并以蹦床方式执行。
一个明显的方法是摆脱recursion ,并重写代码迭代。
当不可能的时候,我们需要一些更复杂的代码,而不是直接执行recursion步骤,我们将使用higher order functions
来返回一个包装函数,而不是直接执行recursion步骤,并让另一个函数来控制执行。
在你的例子中, repeat函数用函数包装了规则的recursion调用,并且它返回该函数而不是执行recursion调用:
function repeat(operation, num) { return function() { if (num <= 0) return operation() return repeat(operation, --num) } }
返回的函数是recursion执行的下一步,蹦床是一种在while循环中以受控和反复的方式执行这些步骤的机制:
function trampoline(fn) { while(fn && typeof fn === 'function') { fn = fn() } }
所以蹦床function的唯一目的是以迭代的方式控制执行,并确保堆栈在任何给定的时间只有一个堆栈帧。
使用蹦床显然比简单的recursion性能低,因为你正在“阻塞”正常的recursionstream程,但它更安全。
其他答复描述了蹦床是如何工作的。 给定的实现有两个缺点,其中一个甚至是有害的:
- 蹦床协议仅依赖于function。 如果recursion操作的结果也是函数呢?
- 您必须在整个调用代码中使用带有蹦床function的recursion函数。 这是一个应该隐藏的实现细节。
从本质上说,蹦床技术在一种热切评估的语言中处理懒惰的评估。 这是避免上述缺点的方法:
// a tag to uniquely identify thunks (zero-argument functions) const $thunk = Symbol.for("thunk"); // eagerly evaluate a lazy function until the final result const eager = f => (...args) => { let g = f(...args); while (g && g[$thunk]) g = g(); return g; }; // lift a normal binary function into the lazy context const lazy2 = f => (x, y) => { const thunk = () => f(x, y); return (thunk[$thunk] = true, thunk); }; // the stack-safe iterative function in recursive style const repeat = n => f => x => { const aux = lazy2((n, x) => n === 0 ? x : aux(n - 1, f(x))); return eager(aux) (n, x); }; const inc = x => x + 1; // and run... console.log(repeat(1e6) (inc) (0)); // 1000000