86

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

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

4 回答 4

192

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

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

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

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


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

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

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


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

阶段 1:编译。编译器看到你已经声明了一个变量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 中用于打破闭包(例如在循环中)的最广泛使用的机制。

于 2010-10-08T03:57:10.150 回答
2

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

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

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

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

  • 第四个示例同时具有第二个和第三个工作理由

于 2010-10-08T03:12:49.273 回答
1

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

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

于 2010-10-08T03:07:56.550 回答
1

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

function name (arguments) {code}

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

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

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

Additional Note:
I ran a few experiments and it seems that if you declare all of your functions in the manner described here, it doesn't really matter what order they are in. If function A uses function B, function B does not have to be declared before function A.

So, declare all of your functions first, your global variables next, and then put your other code last. Follow these rules of thumb and you can't go wrong. It might even be best to put your declarations in the head of the web page and your other code in the body to ensure enforcement of these rules.

于 2012-07-24T23:48:42.173 回答