JavaScript函数声明和评估顺序

jny*_*len 79 javascript function-declaration

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

// 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'); }
})();
Run Code Online (Sandbox Code Playgroud)

sle*_*man 179

这不是范围问题,也不是封闭问题.问题是,在相互理解的声明表达式.

JavaScript代码,因为即使是Netscape的第一个版本的JavaScript和微软的第一个版本,它也分两个阶段处理:

阶段1:编译 - 在此阶段,代码被编译为语法树(以及字节码或二进制,具体取决于引擎).

阶段2:执行 - 然后解释解析的代码.

函数声明的语法是:

function name (arguments) {code}
Run Code Online (Sandbox Code Playgroud)

参数当然是可选的(代码也是可选的,但重点是什么?).

但JavaScript也允许您使用表达式创建函数.函数表达式的语法类似于函数声明,除了它们是在表达式上下文中编写的.表达式是:

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

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

所以,澄清一下:


// 1
(function() {
setTimeout(someFunction, 10);
var someFunction = function() { alert('here1'); };
})();
Run Code Online (Sandbox Code Playgroud)

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

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


// 2
(function() {
setTimeout(someFunction, 10);
function someFunction() { alert('here2'); }
})();
Run Code Online (Sandbox Code Playgroud)

第1阶段:编译.编译器看到你声明一个名为someFunction的函数,因此它创建它.

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


// 3
(function() {
setTimeout(function() { someFunction(); }, 10);
var someFunction = function() { alert('here3'); };
})();
Run Code Online (Sandbox Code Playgroud)

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

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


案例4实际上是案例2的另一个版本,其中someFunction引入了一些案例3.当传递给setTimeout时,由于它被声明,它已经存在.


补充说明:

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

  • 这是一个非常好的答案. (7认同)
  • @Matt:从技术上讲,闭包不涉及范围而是堆栈帧(也称为激活记录).闭包是堆栈帧之间共享的变量.堆栈框架用于定义对象的类.换句话说,范围是程序员在代码结构中感知的范围.堆栈帧是在运行时在内存中创建的.这不是真的那样,但足够接近.在考虑运行时行为时,基于范围的理解有时是不够的. (3认同)
  • @slebetman对你的例子3的解释,你提到setTimeout中的匿名函数创建了someFunction变量的闭包,此时someFunction仍未定义 - 这是有道理的.似乎示例3没有返回undefined的唯一原因是因为setTimeout函数(10毫秒的延迟允许JavaScript对someFunction执行下一个赋值语句,从而使其定义)对吗? (3认同)
  • 这个答案让我希望我可以对同一个答案多次投票。确实是一个很好的答案。谢谢 (2认同)