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式是:
- 任何符号的右边(或
:
对象文字)。 - 括号
()
任何内容。 - 函数参数(实际上已经被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实际上并没有得到传递给它的variablessomeFunction(这意味着要创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,然后把你的其他代码放到最后。 按照这些经验法则,你不会出错。 甚至可以把你的声明放在网页的头部和你的其他代码中,以确保执行这些规则。