了解javascript

时间:2016-09-12 21:44:05

标签: javascript recursion

我对理解“事件队列”和“调用堆栈”概念的好奇心在我解决这个问题时开始:

var list = readHugeList();

var nextListItem = function() {
    var item = list.pop();

    if (item) {
        // process the list item...
        nextListItem();
    }
};

如果数组列表太大,以下递归代码将导致堆栈溢出。你如何解决这个问题并仍保留递归模式?

提到的解决方案是:

var list = readHugeList();

var nextListItem = function() {
    var item = list.pop();

    if (item) {
        // process the list item...
        setTimeout( nextListItem, 0);
    }
};

解决方案:

  

由于事件循环处理了堆栈溢出,因此消除了堆栈溢出   递归,而不是调用堆栈。当nextListItem运行时,如果item不是   null,超时函数(nextListItem)被推送到事件队列   并且函数退出,从而使调用堆栈清晰。当。。。的时候   事件队列运行其超时事件,处理下一个项目并且a   timer设置为再次调用nextListItem。因此,该方法是   从头到尾处理没有直接递归调用,所以   无论迭代次数多少,调用堆栈都会保持清晰。

现在我的问题:

Q1)“事件队列”和“调用堆栈”之间有什么区别

Q2)我不明白答案。有人可以详细解释我吗?

Q3)当我在javascript中执行函数或调用变量或对象时。流程如何? 调用堆栈中的内容是什么?(假设我执行setTimeout ..它是去callstack还是事件队列?)

这些概念非常不清楚。我用谷歌搜索,但大部分结果都不是我期望理解的。

请帮忙!

2 个答案:

答案 0 :(得分:14)

答案1& 3

事件队列和调用堆栈之间存在很大差异。事实上,它们几乎没有任何共同之处。

调用堆栈(简单概述):

当你执行一个函数时,它所使用的所有内容都被认为是在堆栈上的 ,这与你在那里引用的调用堆栈相同。非常简化,它是功能执行的临​​时内存。或者换句话说



function foo() {
  console.log("-> start [foo]");
  console.log("<- end   [foo]");
}

foo();
&#13;
&#13;
&#13;

当被调用时,它将被赋予一个小沙箱来与堆栈中的一起玩。当函数结束时,使用的临时内存将被擦除并可用于其他内容。因此,所使用的资源(除非在系统的某个地方给出)将只持续该函数持续的时间。

现在,如果您有嵌套函数

&#13;
&#13;
function foo() {
  console.log("-> start [foo]");
  console.log("<- end   [foo]");
}

function bar() {
  console.log("-> start [bar]");
  foo()
  console.log("<- end   [bar]");
}

bar();
&#13;
&#13;
&#13;

以下是调用该函数时发生的情况:

    执行
  1. bar - 为堆栈分配内存。
  2. bar打印&#34;开始&#34;
  3. 执行
  4. foo - 为堆栈分配内存。 NB! bar仍在运行,其内存也在那里。
  5. foo打印&#34;开始&#34;
  6. foo打印&#34;结束&#34;
  7. foo完成执行并从堆栈中清除其内存。
  8. bar打印&#34;结束&#34;
  9. bar完成执行并从堆栈中清除其内存。
  10. 因此,执行顺序为bar - &gt; foo,但决议是最后一次,先退出(LIFO)foo完成 - &gt; bar完成。

    这就是使它成为&#34;堆栈的原因。

    这里需要注意的重要一点是,函数使用的资源只有在完成执行时才会释放。当它内部的所有函数和它们内部的函数完成执行时,它完成执行。所以你可以有一个非常深的调用堆栈,如a - &gt; b - &gt; c - &gt; d - &gt; e如果a中有任何大型资源,您需要be才能完成发布。

    在递归中,函数调用自身,它仍然在堆栈上创建条目。因此,如果a一直在调用自己,那么最终会调用a - &gt;的调用堆栈。 a - &gt; a - &gt; a等。

    这是一个非常简短的插图

    &#13;
    &#13;
    // a very naive recursive count down function
    function recursiveCountDown(count) {
      //show that we started
      console.log("-> start recursiveCountDown [" + count + "]");
      
      if (count !== 0) {//exit condition
        //take one off the count and recursively call again
        recursiveCountDown(count -1);
        console.log("<- end recursiveCountDown [" + count + "]"); // show where we stopped. This will terminate this stack but only after the line above finished executing;
      } else {
        console.log("<<<- it's the final recursiveCountDown! [" + count + "]"); // show where we stopped
      }
    }
    
    console.log("--shallow call stack--")
    recursiveCountDown(2);
    
    console.log("--deep call stack--")
    recursiveCountDown(10);
    &#13;
    &#13;
    &#13;

    这是一个非常简单且非常有缺陷的递归函数,但它仅用于演示在这种情况下会发生什么。

    事件队列

    JavaScript在事件队列中运行(或者也是#34;事件循环&#34;),简单来说,等待&#34;活动&#34; (事件),处理它们然后再等待。

    如果有多个事件,它将按顺序处理它们 - 先进先出(FIFO),因此是一个队列。所以,如果我们重新编写上述函数:

    &#13;
    &#13;
    function foo() {
      console.log("-> start [foo]");
      console.log("<- end   [foo]");
    }
    
    function bar() {
      console.log("-> start [bar]");
      console.log("<- end   [bar]");
    }
    
    
    function baz() {
      console.log("-> start [baz]");
      
      setTimeout(foo, 0);
      setTimeout(bar, 0);
      
      console.log("<- end   [baz]");
    }
    
    baz();
    &#13;
    &#13;
    &#13;

    这是如何发挥作用的。

    1. baz已执行。内存分配在堆栈上。
    2. foo通过安排运行&#34; next&#34;。
    3. 而延迟
    4. bar通过安排运行&#34; next&#34;。
    5. 而延迟
    6. baz完成。堆栈已清除。
    7. 事件循环选择队列中的下一个项目 - 这是foo
    8. foo被执行。内存分配在堆栈上。
    9. foo完成。堆栈已清除。
    10. 事件循环选择队列中的下一个项目 - 这是bar
    11. bar被执行。内存分配在堆栈上。
    12. bar完成。堆栈已清除。
    13. 正如您所希望的那样,堆栈仍在使用中。您调用的任何函数将始终生成堆栈条目。事件队列是一个单独的机制。

      通过这种方式,您可以减少内存开销,因为您不必等待任何其他功能来释放分配的资源。另一方面,你不能依赖任何完成的功能。

      我希望本节也能回答你的问题。

      答案2

      如何推迟到队列帮助?

      我希望上面的解释会更清楚,但它确保解释是有道理的:

      堆栈的深度有一个限制。如果你考虑一下,它应该是显而易见的 - 只有大量的内存可用于大概是临时存储。达到最大调用深度后,JavaScript将抛出RangeError: Maximum call stack size exceeded错误。

      如果您查看我上面给出的recursiveCountDown示例,很容易造成错误 - 如果您致电recursiveCountDown(100000),您将获得RangeError

      通过将所有其他执行放在队列中,您可以避免填满堆栈,从而避免使用RangeError。所以让我们重新编写函数

      &#13;
      &#13;
      // still naive but a bit improved recursive count down function
      function betterRecursiveCountDown(count) {
        console.log("-> start recursiveCountDown [" + count + "]");
        
        if (count !== 0) {
          //setTimeout takes more than two parameters - anything after the second one will be passed to the function when it gets executed
          setTimeout(betterRecursiveCountDown, 0, count - 1);
          console.log("<- end recursiveCountDown [" + count + "]");
        } else {
          console.log("<<<- it's the final recursiveCountDown! [" + count + "]"); // show where we stopped
        }
      }
      
      betterRecursiveCountDown(10);
      &#13;
      &#13;
      &#13;

答案 1 :(得分:1)

使用call stack的主要原因用于了解当前函数结束后的去向。 但大多数语言的大小限制为call stack,因此如果重复调用函数直到函数未完成,call stack的大小就会溢出。

setTimeout的大多数工具都有queue用于保存工作。 并在空闲时间执行它们。

首先nextListItem在自身未完成之前调用自我。 所以call stack会很长,直到项目列表结束。

第二个nextListItem在完成后自称为自我,call stack也是明确的。 因此,call stack在空闲时从nextListItem调用setTimeout时将从空开始。

  1. call stack用于函数调用历史记录。 event queue用于保存setTimeout作业。

  2. 见上解释。

  3. javascript只是不断执行你的陈述。但是在函数完成后调用此函数返回的位置将保存。 call stack用于保存被调用函数的历史记录。