JavaScript函数声明和评估顺序


80

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

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

Answers:


182

这既不是范围问题,也不是关闭问题。问题在于声明表达式之间的理解。

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已定义了变量,因此可以创建它。默认情况下,所有创建的变量的值均为undefined。请注意,编译器此时无法分配值,因为这些值可能需要解释器执行一些代码才能返回要分配的值。并且在这个阶段我们还没有执行代码。

阶段2:执行。解释器看到您要将变量传递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并创建了它。和以前一样,它的值是不确定的。

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


情况4实际上是情况2的另一种版本,其中包含了情况3的一点点。someFunction由于已将其声明给setTimeout,因此它已经存在。


附加说明:

您可能想知道为什么setTimeout(someFunction, 10)不在someFunction的本地副本和传递给setTimeout的本地副本之间创建闭包。答案是,JavaScript中的函数参数始终是数值(如果它们是数字或字符串)或其他所有引用的传递。因此,setTimeout实际上并不会获取传递给它的变量some​​Function(这将意味着正在创建一个闭包),而只会获取someFunction引用的对象(在这种情况下为函数)。这是JavaScript中打破闭合(例如在循环中)使用最广泛的机制。


7
那是一个非常好的答案。
马特·布里格斯

1
@Matt:我已经在其他地方(几次)对此进行了解释。我的一些最喜欢的解释:stackoverflow.com/questions/3572480/...
slebetman


3
@Matt:从技术上讲,闭包不涉及作用域,而是涉及堆栈框架(也称为激活记录)。闭包是在堆栈框架之间共享的变量。堆栈框架将确定对象要分类的范围。换句话说,范围是程序员在代码结构中所感知的。堆栈框架是在运行时在内存中创建的。并不是真的那样,但是足够接近。考虑运行时行为时,基于范围的理解有时是不够的。
slebetman

3
@slebetman为您解释示例3,您提到setTimeout中的匿名函数创建了someFunction变量的闭包,并且在这一点上,someFunction仍未定义-这很有意义。似乎示例3不返回undefined的唯一原因是因为setTimeout函数(10毫秒的延迟使JavaScript可以执行someFunction的下一个赋值语句,从而使之定义),对吗?
2013年

2

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

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

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

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

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


1

因为someFunction1setTimeout()执行调用时尚未分配。

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


但是执行someFunction2调用时尚未分配setTimeout()...?
我们都是莫妮卡(Monica),2010年

1
@jnylen:使用function关键字声明函数并不完全等同于将匿名函数分配给变量。声明为的函数function foo()“悬挂”在当前作用域的开头,而变量分配发生在它们的写入点。
Chuck 2010年

1
+1为特殊功能。但是,仅仅因为它可以工作并不意味着就应该这样做。使用前请务必声明。
mway 2010年

@mway:在我的情况下,我将我的代码组织在“类”中,分为以下几个部分:私有变量,事件处理程序,私有函数,然后是公共函数。我需要我的事件处理程序之一来调用我的私有函数之一。对我而言,以这种方式组织代码胜于按词法对声明进行排序。
我们都是莫妮卡(Monica),2010年

1

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

function name (arguments) {code}

避免使用var声明它们。这只是草率,并导致问题。如果您习惯在使用前先声明所有内容,那么大多数问题将很快消失。声明变量时,我会立即使用有效值对其进行初始化,以确保未定义任何变量。我也倾向于包含在函数使用全局变量之前检查其有效值的代码。这是防止错误的附加保障。

所有这些工作原理的技术细节都类似于手榴弹在玩时的工作原理。我的简单建议是一开始不要玩手榴弹。

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

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.