JavaScript函数声明和评估顺序

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

// 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'); } })(); 

这既不是范围问题,也不是封闭问题。 问题在于声明expression之间的理解。

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

阶段1:编译 – 在这个阶段,代码被编译成一个语法树(根据引擎字节码或二进制)。

阶段2:执行 – parsing的代码然后被解释。

函数声明的语法是:

 function name (arguments) {code} 

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

但是JavaScript也允许你使用expression式来创build函数。 函数expression式的语法类似于函数声明,只不过它们是在expression式上下文中编写的。 和expression式是:

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

声明不同的expression式在执行阶段而不是编译阶段进行处理。 正因为如此,expression的顺序很重要。

所以,澄清一下:


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

阶段1:编译。 编译器看到variablessomeFunction被定义,所以它创build它。 默认情况下,所有创build的variables的值都是undefined。 请注意,编译器此时不能分配值,因为这些值可能需要解释器执行某些代码才能返回要分配的值。 而在这个阶段,我们还没有执行代码。

阶段2:执行。 解释器看你想把variables的someFunction传给setTimeout。 这样做。 不幸的是, someFunction的当前值是不确定的。


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

阶段1:编译。 编译器看到你正在用名字someFunction声明一个函数,所以它创build它。

阶段2:解释器看到你想传递一些someFunction给setTimeout。 这样做。 someFunction的当前值是其编译的函数声明。


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

阶段1:编译。 编译器看到你已经声明了一个variablessomeFunction并创build它。 和以前一样,它的值是不确定的。

阶段2:执行。 解释器传递一个匿名函数来设置稍后执行的Timeout。 在这个函数中,它看到你正在使用variablessomeFunction所以它创build一个variables的闭包。 此时someFunction的值仍然是未定义的。 然后它看到你将一个函数赋值给某个函数。 此时someFunction的值不再是未定义的。 1/100秒后setTimeout触发器和someFunction被调用。 由于它的价值不再未定义它的作品。


情况4实际上是情况2的另一个版本,其中有一些情况3被抛出。在someFunction被传递给setTimeout的时候,它已经存在了,因为它被声明了。


补充说明:

您可能想知道为什么setTimeout(someFunction, 10)不会在someFunction的本地副本和传递给setTimeout的副本之间创build闭包。 答案就是JavaScript中的函数参数总是被传递,如果它们是数字或string的话,或者通过引用其他的东西。 所以setTimeout实际上并没有得到传递给它的variablessome​​Function(这意味着要创build一个闭包),而只是获得someFunction引用的对象(在这种情况下是一个函数)。 这是JavaScript中用于分解闭包(例如循环)中使用最广泛的机制。

Javascript的范围是基于function的,而不是严格的词汇范围。 那意味着

  • Somefunction1是从封闭函数的开始定义的,但是它的内容是未定义的,直到分配为止。

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

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

  • 第四个例子既有第二个也有第三个原因

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

someFunction3可能看起来像一个类似的情况,但是因为在这种情况下传递一个包装someFunction3()setTimeout()的函数,所以直到稍后调用someFunction3()

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

 function name (arguments) {code} 

避免用var声明它们。 这只是马虎,导致问题。 如果你习惯于在使用之前宣布所有的东西,那么你的大部分问题都会很快消失。 在声明variables的时候,我会立刻用一个有效的值初始化它们,以确保它们都没有被定义。 我也倾向于包含在函数使用它们之前检查全局variables的有效值的代码。 这是防止错误的额外保障。

所有这些工作的技术细节有点类似手榴弹在你玩的时候的工作原理。 我的简单build议是不要用手榴弹玩首先。

在代码开头的一些简单的声明可能会解决这些types的问题中的大多数,但是代码的一些清理可能仍然是必需的。

附加说明:
我进行了一些实验,看来如果按照这里描述的方式声明所有的函数,那么它们的顺序并不重要。如果函数A使用函数B,函数B就不必在声明之前声明functionA.

所以,先声明所有的函数,然后声明你的全局variables,然后把你的其他代码放到最后。 按照这些经验法则,你不会出错。 甚至可以把你的声明放在网页的头部和你的其他代码中,以确保执行这些规则。