JavaScript 函数声明和求值顺序

IT技术 javascript function-declaration
2021-01-29 23:45:51

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

// 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个回答

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

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

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

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


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

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

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


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

第一阶段:编译。编译器看到您声明了一个变量someFunction并创建了它。和以前一样,它的值是未定义的。

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


案例 4 实际上是案例 2 的另一个版本,其中加入了一些案例 3。此时someFunction传递给 setTimeout 它已经存在,因为它被声明了。


补充说明:

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

@slebetman 对示例 3 的解释,您提到 setTimeout 中的匿名函数为 someFunction 变量创建了一个闭包,并且此时 someFunction 仍未定义 - 这是有道理的。似乎示例 3 不返回 undefined 的唯一原因是 setTimeout 函数(10 毫秒的延迟允许 JavaScript 执行对 someFunction 的下一个赋值语句,从而使其定义)对吗?
2021-03-18 23:45:51
@Matt:从技术上讲,闭包不涉及范围,而是涉及堆栈框架(也称为活动记录)。闭包是堆栈帧之间共享的变量。堆栈帧将对象的范围限定为类。换句话说,作用域是程序员在代码结构中所感知的。堆栈帧是在运行时在内存中创建的。它不是真的那样,但足够接近。在考虑运行时行为时,基于作用域的理解有时是不够的。
2021-03-19 23:45:51
这是一个非常好的答案。
2021-03-21 23:45:51
这个答案让我希望我可以对同一个答案多次投票。真是一个很好的答案。谢谢
2021-03-22 23:45:51
@Matt:我已经在其他地方(多次)解释过这个问题。我最喜欢的一些解释:stackoverflow.com/questions/3572480/...
2021-04-09 23:45:51

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

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

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

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

  • 第四个例子有第二个和第三个工作原因

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

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

+1 表示功能特殊。然而,仅仅因为它可以工作并不意味着它应该完成。使用前请务必声明。
2021-03-17 23:45:51
@jnylen:使用function关键字声明函数并不完全等同于将匿名函数分配给变量。声明为的函数function foo()被“提升”到当前作用域的开头,而变量赋值发生在它们被写入的地方。
2021-03-23 23:45:51
@mway:就我而言,我将“类”中的代码组织成多个部分:私有变量、事件处理程序、私有函数,然后是公共函数。我需要我的事件处理程序之一来调用我的私有函数之一。对我来说,以这种方式组织代码胜过按词法对声明进行排序。
2021-03-23 23:45:51
但是someFunction2setTimeout()执行调用时还没有分配......?
2021-04-04 23:45:51

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

function name (arguments) {code}

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

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

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

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

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