在关注reverseAll 之前,您应该单独了解reversed 功能-
function reverse(word) {
if (word === "") return ""
return reverse(word.substring(1)) + word.charAt(0)
}
console.log(reverse("hello world"))
从reverse("hello_world") 开始,我们可以轻松跟踪评估。只要输入word 非空,就会打开一个新的堆栈帧,并递归调用子问题reverse(word.substring(1))。 ... + word.charAt(0) 部分保留在调用框架中,仅在后代框架返回后恢复 -
reverse("hello world") =
reverse("ello world") + "h" =
reverse("llo world") + "e" + "h" =
reverse("lo world") + "l" + "e" + "h" =
reverse("o world") + "l" + "l" + "e" + "h" =
reverse(" world") + "o" + "l" + "l" + "e" + "h" =
reverse("world") + " " + "o" + "l" + "l" + "e" + "h" =
reverse("orld") + "w" + " " + "o" + "l" + "l" + "e" + "h" =
reverse("rld") + "o" + "w" + " " + "o" + "l" + "l" + "e" + "h" =
reverse("ld") + "r" + "o" + "w" + " " + "o" + "l" + "l" + "e" + "h" =
reverse("d") + "l" + "r" + "o" + "w" + " " + "o" + "l" + "l" + "e" + "h" =
reverse("") + "d" + "l" + "r" + "o" + "w" + " " + "o" + "l" + "l" + "e" + "h" =
这里我们遇到了基本情况,递归停止并返回空字符串。现在所有打开的堆栈帧都崩溃了,从最深的帧开始,将其值返回给它的调用者 -
"" + "d" + "l" + "r" + "o" + "w" + " " + "o" + "l" + "l" + "e" + "h" =
"d" + "l" + "r" + "o" + "w" + " " + "o" + "l" + "l" + "e" + "h" =
"dl" + "r" + "o" + "w" + " " + "o" + "l" + "l" + "e" + "h" =
"dlr" + "o" + "w" + " " + "o" + "l" + "l" + "e" + "h" =
"dlro" + "w" + " " + "o" + "l" + "l" + "e" + "h" =
"dlrow" + " " + "o" + "l" + "l" + "e" + "h" =
"dlrow " + "o" + "l" + "l" + "e" + "h" =
"dlrow o" + "l" + "l" + "e" + "h" =
"dlrow ol" + "l" + "e" + "h" =
"dlrow oll" + "e" + "h" =
"dlrow olle" + "h" =
最后我们可以关闭对reverse的最外层调用并返回结果-
"dlrow olleh"
在此程序中,堆栈 用于对操作进行排序并按预期顺序组合结果值。如果输入word 非常大,您会遇到堆栈溢出,因为会打开太多的帧,并且您基本上会打破此类计算的 JavaScript 运行时限制。内存或堆仅用于所有中间字符串分配。
上述程序中不断增长的堆栈演示了一个递归过程。这是任何不使用尾调用的递归程序的特征。尾调用只是函数中的 last 调用,直接返回给它的调用者 -
function reverse(word) {
function loop(r, w) {
if (w == "") return r
return loop(w[0] + r, w.substr(1)) // <- loop is the last called
}
return loop("", word)
}
console.log(reverse("hello world"))
这演示了一个线性迭代过程,之所以这么称呼是因为递归函数创建的过程像一条线一样保持平直-
reverse("hello world") =
loop("", "hello world") =
loop("h", "ello world") =
loop("eh", "llo world") =
loop("leh", "lo world") =
loop("lleh", "o world") =
loop("olleh", " world") =
loop(" olleh", "world") =
loop("w olleh", "orld") =
loop("ow olleh", "rld") =
loop("row olleh", "ld") =
loop("lrow olleh", "d") =
loop("dlrow olleh", "") =
"dlrow olleh"
一些语言有尾调用优化,这意味着像上面这样的递归函数可以避免堆栈溢出问题。编译器或运行时有效地将递归调用转换为循环 -
function reverse(word) {
function loop(r, w) {
while (true) {
if (w == "") return r
r = w[0] + r
w = w.substr(1)
}
}
return loop("", word)
}
console.log(reverse("hello world"))
仅使用 2 帧 和 3 个绑定、word、r 和 w 的内存分配。用于计算 + 和 w.substr(1) 的内存分配也会被运行时的自动垃圾收集器重新捕获。
在 ECMAScript 6 中,尾调用消除是 added to the specification,但几乎所有流行的运行时都不支持它,而且这种情况不太可能改变。然而,这并不意味着我们只能使用命令式while 循环来编写递归程序。有various techniques 使递归程序即使在 JavaScript 中也是安全的,即使在不支持这种优化的运行时也是如此。
考虑使用loop 和recur 实现reverse -
const reverse = word =>
loop
( (r = "", w = word) =>
w == ""
? r
: recur(w[0] + r, w.substr(1))
)
非递归 loop 和 recur 函数是通用的,允许我们使用它们编写大多数不会导致堆栈溢出的递归程序 -
const recur = (...values) =>
({ recur, values })
const loop = run =>
{ let r = run ()
while (r && r.recur === recur)
r = run (...r.values)
return r
}
console.log(reverse("hello world"))
这与上面的while 循环具有非常相似的性能。只有 2 个堆栈帧 和 3 个绑定,以及一些立即被垃圾收集的值的少量开销,例如 +、substr 和 recur -
展开下面的sn-p,在自己的浏览器中验证结果-
const recur = (...values) =>
({ recur, values })
const loop = run =>
{ let r = run ()
while (r && r.recur === recur)
r = run (...r.values)
return r
}
const reverse = word =>
loop
( (r = "", w = word) =>
w == ""
? r
: recur(w[0] + r, w.substr(1))
)
console.log(reverse("hello world"))
"dlrow olleh"
事实上,任何递归程序,尾调用与否,都可以使用各种技术实现堆栈安全。如果您对这类事情感兴趣,请参阅 this related Q&A 以深入探讨该主题。