【问题标题】:JavaScript function declaration and evaluation orderJavaScript 函数声明和求值顺序
【发布时间】:2011-04-22 16:36:29
【问题描述】:

为什么这些示例中的第一个不起作用,而其他所有示例都起作用?

// 1 - does not work
(function() {
setTimeout(someFunction1, 10);
var someFunction1 = function() { alert('here1'); };
})();

// 2
(function() {
setTimeout(someFunction2, 10);
function someFunction2() { alert('here2'); }
})();

// 3
(function() {
setTimeout(function() { someFunction3(); }, 10);
var someFunction3 = function() { alert('here3'); };
})();

// 4
(function() {
setTimeout(function() { someFunction4(); }, 10);
function someFunction4() { alert('here4'); }
})();

【问题讨论】:

    标签: javascript function-declaration


    【解决方案1】:

    这既不是作用域问题,也不是闭包问题。问题在于声明表达式之间的理解。

    JavaScript 代码,因为即使是 Netscape 的第一个 JavaScript 版本和 Microsoft 的第一个副本,也是分两个阶段处理的:

    第 1 阶段:编译 - 在此阶段,代码被编译成语法树(以及字节码或二进制文件,具体取决于引擎)。

    第 2 阶段:执行 - 然后解释解析的代码。

    函数声明的语法是:

    function name (arguments) {code}
    

    参数当然是可选的(代码也是可选的,但这有什么意义呢?)。

    但 JavaScript 也允许您使用 表达式 创建函数。函数表达式的语法类似于函数声明,只是它们是在表达式上下文中编写的。表达式是:

    1. = 符号右侧的任何内容(或对象文字上的 :)。
    2. 括号中的任何内容()
    3. 函数的参数(这实际上已经被 2 覆盖)。

    表达式声明不同,是在执行阶段而不是编译阶段处理的。因此,表达式的顺序很重要。

    所以,澄清一下:


    // 1
    (function() {
    setTimeout(someFunction, 10);
    var someFunction = function() { alert('here1'); };
    })();
    

    第一阶段:编译。编译器看到变量someFunction 已定义,因此它创建它。默认情况下,所有创建的变量都具有未定义的值。请注意,此时编译器还不能赋值,因为这些值可能需要解释器执行一些代码才能返回要赋值的值。在这个阶段,我们还没有执行代码。

    第二阶段:执行。解释器看到您想将变量someFunction 传递给setTimeout。它确实如此。不幸的是,someFunction 的当前值未定义。


    // 2
    (function() {
    setTimeout(someFunction, 10);
    function someFunction() { alert('here2'); }
    })();
    

    第一阶段:编译。编译器看到您正在声明一个名为 someFunction 的函数,因此它会创建它。

    第 2 阶段:解释器看到您想将 someFunction 传递给 setTimeout。它确实如此。 someFunction 的当前值是它的编译函数声明。


    // 3
    (function() {
    setTimeout(function() { someFunction(); }, 10);
    var someFunction = function() { alert('here3'); };
    })();
    

    第一阶段:编译。编译器看到你声明了一个变量someFunction 并创建它。和以前一样,它的值是未定义的。

    第二阶段:执行。解释器将匿名函数传递给 setTimeout 以便稍后执行。在此函数中,它看到您正在使用变量someFunction,因此它为该变量创建了一个闭包。此时someFunction 的值仍未定义。然后它会看到您将函数分配给someFunction。此时someFunction 的值不再未定义。 1/100 秒后触发 setTimeout 并调用 someFunction。由于它的值不再是未定义的,它可以工作。


    案例 4 实际上是案例 2 的另一个版本,其中包含了一些案例 3。在 someFunction 被传递给 setTimeout 时,它已经存在,因为它被声明了。


    补充说明:

    您可能想知道为什么setTimeout(someFunction, 10) 没有在 someFunction 的本地副本和传递给 setTimeout 的副本之间创建闭包。答案是 JavaScript 中的函数参数总是,总是如果它们是数字或字符串,则按值传递,或者通过引用传递其他所有参数。所以 setTimeout 实际上并没有得到传递给它的变量 someFunction (这意味着要创建一个闭包),而是只获取 someFunction 引用的对象(在这种情况下是一个函数)。这是 JavaScript 中用于打破闭包(例如在循环中)的最广泛使用的机制。

    【讨论】:

    • 这是一个非常棒的答案。
    • 这个答案让我希望我能对同一个答案多次投票。真是一个很棒的答案。谢谢
    • @Matt:我已经在其他地方(多次)在 SO 上解释过这一点。我最喜欢的一些解释:stackoverflow.com/questions/3572480/…
    • @Matt:从技术上讲,闭包不涉及范围,而是涉及堆栈帧(也称为激活记录)。闭包是堆栈帧之间共享的变量。堆栈框架用于确定对象对类的范围。换句话说,范围是程序员在代码结构中所感知的。堆栈帧是在运行时在内存中创建的。它不是真的那样,但足够接近。在考虑运行时行为时,基于范围的理解有时是不够的。
    • @slebetman 为您对示例 3 的解释,您提到 setTimeout 中的匿名函数为 someFunction 变量创建了一个闭包,此时, someFunction 仍未定义 - 这是有道理的。似乎示例 3 没有返回 undefined 的唯一原因是因为 setTimeout 函数(10 毫秒的延迟允许 JavaScript 执行下一个赋值语句到 someFunction,从而使其定义)对吗?
    【解决方案2】:

    Javascript 的作用域是基于函数的,而不是严格的词法作用域。这意味着

    • Somefunction1 从封闭函数的开头定义,但它的内容在分配之前是未定义的。

    • 在第二个示例中,赋值是声明的一部分,因此它“移动”到顶部。

    • 在第三个例子中,定义匿名内部闭包时变量存在,但直到10秒后才使用,此时值已被赋值。

    • 第四个示例同时具有第二个和第三个工作原因

    【讨论】:

      【解决方案3】:

      因为在执行对setTimeout() 的调用时尚未分配someFunction1

      someFunction3 可能看起来像一个类似的情况,但由于在这种情况下您将包装someFunction3() 的函数传递给setTimeout(),因此直到稍后才会评估对someFunction3() 的调用。

      【讨论】:

      • 但是someFunction2在调用setTimeout()时还没有被分配...?
      • @jnylen:使用 function 关键字声明函数并不完全等同于将匿名函数分配给变量。声明为 function foo() 的函数被“提升”到当前作用域的开头,而变量赋值发生在它们被写入的位置。
      • +1 表示特殊功能。然而,仅仅因为它可以工作并不意味着它应该被完成。使用前务必声明。
      • @mway:在我的例子中,我将“类”中的代码组织成几个部分:私有变量、事件处理程序、私有函数,然后是公共函数。我需要一个事件处理程序来调用我的一个私有函数。对我来说,以这种方式组织代码胜过按词法排序声明。
      【解决方案4】:

      这听起来像是遵循良好程序以避免麻烦的基本案例。在使用变量和函数之前声明它们并声明如下函数:

      function name (arguments) {code}
      

      避免使用 var 声明它们。这只是草率并导致问题。如果你养成在使用前声明一切的习惯,你的大部分问题都会很快消失。在声明变量时,我会立即用一个有效值初始化它们,以确保它们都不是未定义的。我还倾向于包含在函数使用全局变量之前检查它们的有效值的代码。这是防止错误的额外保护措施。

      所有这些工作原理的技术细节有点像你玩手榴弹时如何工作的物理原理。我的简单建议是首先不要玩手榴弹。

      在代码开头的一些简单声明可能会解决大多数此类问题,但可能仍然需要对代码进行一些清理。

      补充说明:
      我进行了一些实验,似乎如果您以此处描述的方式声明所有函数,它们的顺序并不重要。如果函数 A 使用函数 B,则函数 B 不必在之前声明函数A。

      因此,首先声明所有函数,然后声明全局变量,然后将其他代码放在最后。遵循这些经验法则,你就不会出错。最好将您的声明放在网页的头部,将您的其他代码放在正文中,以确保这些规则的执行。

      【讨论】:

        猜你喜欢
        • 2021-09-19
        • 1970-01-01
        • 1970-01-01
        • 2015-12-03
        • 2010-12-24
        • 2016-03-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多