JavaScript函数声明和评估顺序

JavaScript function declaration and evaluation order

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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代码,因为即使是Netscape的第一个版本的JavaScript和微软的第一个版本,它也分两个阶段处理:

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

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

函数声明的语法是:

1
function name (arguments) {code}

参数当然是可选的(代码也是可选的,但重点是什么?)。

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

  • =符号右侧的任何内容(或对象文字上的:)。
  • 括号中的任何内容()
  • 函数的参数(这实际上已经被2覆盖)。
  • 与声明不同的表达式在执行阶段而不是编译阶段处理。因此,表达的顺序很重要。

    所以,澄清一下:

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

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

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

    1
    2
    3
    4
    5
    // 2
    (function() {
    setTimeout(someFunction, 10);
    function someFunction() { alert('here2'); }
    })();

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

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

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

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

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

    案例4实际上是案例2的另一个版本,其中引入了一些案例3.在someFunction传递给setTimeout的点上,由于它被声明,它已经存在。

    补充说明:

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


    Javascript的范围是基于功能的,而不是严格的词法范围。 这意味着

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

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

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

    • 第四个例子有第二和第三个原因


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

    1
    function name (arguments) {code}

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

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

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

    附加说明:
    我运行了几个实验,似乎如果你按照这里描述的方式声明所有函数,它们的顺序并不重要。如果函数A使用函数B,函数B不必在之前声明功能A.

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


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

    someFunction3可能看起来像一个类似的情况,但是由于在这种情况下你传递了一个将someFunction3()包装到setTimeout()的函数,所以直到稍后才会对someFunction3()的调用进行求值。