JavaScript函数声明和评估顺序

89
为什么这些例子中的第一个无法运行,而其他所有例子却可以?
// 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个回答

198

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

JavaScript代码自从Netscape的第一个版本以及Microsoft的第一份副本以来,都经过两个阶段的处理:

阶段1:编译 - 在此阶段,代码被编译成语法树(并且根据引擎可能还生成字节码或二进制码)。

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

函数声明的语法如下:

function name (arguments) {code}

参数是可选的(当然代码也是可选的,但那有什么意义呢?)。

但是JavaScript也允许你使用表达式来创建函数。函数表达式的语法与函数声明类似,只是它们在表达式上下文中编写。而表达式可以是:

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

表达式声明不同,它们在执行阶段而不是编译阶段进行处理。因此,表达式的顺序很重要。

因此,为了澄清:


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

第一阶段:编译。编译器发现变量someFunction被定义,因此创建了它。默认情况下,所有创建的变量都具有未定义的值。请注意,在这个阶段编译器不能分配任何值,因为这些值可能需要解释器执行一些代码才能返回一个值来进行分配。而在这个阶段我们还没有执行代码。

第二阶段:执行。解释器看到您想将变量someFunction传递给setTimeout。于是它这样做了。不幸的是,someFunction当前的值是undefined。


// 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)不会在本地副本和传递给setTimeout的副本之间创建闭包。答案是,在JavaScript中,如果数字或字符串为函数参数,则始终按值传递,对于其他所有内容则按引用传递。因此,setTimeout实际上并没有得到传递给它的变量someFunction(这将意味着创建一个闭包),而是只得到了someFunction所引用的对象(在这种情况下是一个函数)。这是JavaScript中在发明“let”关键字之前最广泛使用的打破闭包的机制(例如在循环中)。


10
那是一个非常棒的回答。 - Matt Briggs
2
这个答案让我希望我可以在同一个答案上投多次赞。 真的是一个很棒的答案。谢谢。 - ArtBIT
1
@Matt:我在 Stack Overflow 的其他地方已经解释过这个问题(多次)。以下是我喜欢的一些说明链接:https://dev59.com/k3A65IYBdhLWcg3w5zDB#3572616 - slebetman
4
@Matt:从技术上讲,闭包涉及堆栈帧(也称为激活记录),而非作用域。闭包是在堆栈帧之间共享的变量。堆栈帧类似于类中的对象,作用域类似于程序员在代码结构中所感知到的内容。换句话说,作用域是代码结构中程序员所看到的,而堆栈帧则是在运行时在内存中创建的。虽然实际情况并非如此,但这种近似足以满足我们对于运行时行为的理解。 - slebetman
3
@slebetman在解释示例3时提到,setTimeout中的匿名函数创建了一个闭包来访问someFunction变量,并且此时someFunction仍未定义,这是有道理的。看起来,示例3之所以不返回undefined,似乎只是因为setTimeout函数的延迟(10毫秒的延迟允许JavaScript执行下一个对someFunction的赋值语句,从而使其被定义)是吗? - wmock
显示剩余13条评论

2

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

  • 某些函数1从封闭函数开始就已经定义,但其内容在分配之前未定义。

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

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

  • 第四个示例同时具有第二个和第三个原因来工作。


1
这听起来像是遵循良好程序以避免麻烦的基本案例。在使用变量和函数之前声明它们,并像这样声明函数:
function name (arguments) {code}

避免使用var进行声明,这只是一种懒惰的做法,会导致问题。如果你养成在使用变量之前先声明所有变量的习惯,大部分问题都会迎刃而解。在声明变量时,我会立即初始化它们为有效值,以确保它们不是未定义的。我还倾向于在函数使用全局变量之前包含检查有效值的代码。这是对错误的额外保障。
所有这些工作的技术细节有点像你玩手榴弹时手榴弹的物理原理。我的简单建议是首先不要玩手榴弹。
在代码开头进行一些简单的声明可能会解决大部分这类问题,但仍可能需要清理代码。
附加说明: 我进行了一些实验,发现如果按照这里描述的方式声明所有函数,它们的顺序并不重要。如果函数A使用函数B,则函数B在函数A之前不必声明。

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


1

由于在调用 setTimeout() 时,someFunction1 尚未被分配。

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


дҪҶжҳҜеҪ“setTimeout()иў«жү§иЎҢж—¶пјҢsomeFunction2иҝҳжІЎжңүиў«еҲҶй…Қеҗ—пјҹ - We Are All Monica
1
@jnylen:使用function关键字声明函数并不完全等同于将匿名函数分配给变量。使用function foo()声明的函数会被“提升”到当前作用域的开头,而变量赋值发生在它们编写的位置。 - Chuck
1
函数的特殊性使得+1。然而,仅仅因为它“可以”工作并不意味着应该这样做。在使用之前始终要进行声明。 - mway
@mway:在我的情况下,我将我的代码组织在一个“类”中的各个部分内部:私有变量、事件处理程序、私有函数,然后是公共函数。我需要其中一个事件处理程序调用其中一个私有函数。对我来说,以这种方式保持代码有组织性比按字典顺序排序声明更为重要。 - We Are All Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接