【问题标题】:about JS functioning关于 JS 的功能
【发布时间】:2022-01-11 01:17:21
【问题描述】:

这里是一个带有递归的字符串反转函数的例子。

我想了解原因:

(1) 如果我像这样切换返回线内部操作:

return word.charAt(0) + reversed(word.substring(1))

我再次得到字符串但没有反转。

(2) 'word.charAt()' 字母是通过嵌套调用保存的(总体而言,如果它与常规函数相同),记忆方面我在这里错过了什么,是什么让它们通过机智和他们要去哪里。

我想解释一下内存概念在 js 中是如何工作的,以便更好地理解我编码时的关系(我认为尤其是在我的案例中函数的输入和输出)。

var reverseArray = reversedItems(arr)

function reverseAll(items) {
  return items.map(function reversed(word) {
    if (word === "") return ""
    return reversed(word.substring(1)) + word.charAt(0)
  })
}

【问题讨论】:

  • 我认为您需要阅读的概念不是“内存”,而是“可变范围”。也许这会对您有所帮助:What is the scope of variables in JavaScript?.
  • 我建议用铅笔和纸“玩电脑”,用一根短线描出每一步。使用调试器也是一种选择,但最终会更加混乱。

标签: javascript recursion memory


【解决方案1】:

在关注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 个绑定wordrw 的内存分配。用于计算 +w.substr(1) 的内存分配也会被运行时的自动垃圾收集器重新捕获。

ECMAScript 6 中,尾调用消除是 added to the specification,但几乎所有流行的运行时都不支持它,而且这种情况不太可能改变。然而,这并不意味着我们只能使用命令式while 循环来编写递归程序。有various techniques 使递归程序即使在 JavaScript 中也是安全的,即使在不支持这种优化的运行时也是如此。

考虑使用looprecur 实现reverse -

const reverse = word =>
  loop
    ( (r = "", w = word) =>
        w == ""
          ? r
          : recur(w[0] + r, w.substr(1))
    )

非递归 looprecur 函数是通用的,允许我们使用它们编写大多数不会导致堆栈溢出的递归程序 -

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 个绑定,以及一些立即被垃圾收集的值的少量开销,例如 +substrrecur -

展开下面的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 以深入探讨该主题。

【讨论】:

  • 他点了一道主菜,你送了整顿饭!
  • 为人民计算 ^_^
  • 惊人的答案,非常感谢你,你很棒,今天过得愉快。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-17
  • 1970-01-01
  • 2016-04-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多