为什么这些例子中的第一个不起作用,但所有其他例子都不起作用?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| // 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'); }
})(); |
这不是范围问题,也不是封闭问题。问题在于理解和表达式之间的理解。
JavaScript代码,因为即使是Netscape的第一个版本的JavaScript和微软的第一个版本,它也分两个阶段处理:
阶段1:编译 - 在此阶段,代码被编译为语法树(以及字节码或二进制,具体取决于引擎)。
阶段2:执行 - 然后解释解析的代码。
函数声明的语法是:
1
| function name (arguments) {code} |
参数当然是可选的(代码也是可选的,但重点是什么?)。
但JavaScript也允许您使用表达式创建函数。函数表达式的语法类似于函数声明,除了它们是在表达式上下文中编写的。表达式是:
=符号右侧的任何内容(或对象文字上的:)。
括号中的任何内容()。
函数的参数(这实际上已经被2覆盖)。
与声明不同的表达式在执行阶段而不是编译阶段处理。因此,表达的顺序很重要。
所以,澄清一下:
1 2 3 4 5
| // 1
(function() {
setTimeout(someFunction, 10);
var someFunction = function() { alert('here1'); };
})(); |
第1阶段:编译。编译器看到变量someFunction已定义,因此它创建它。默认情况下,创建的所有变量都具有undefined值。请注意,此时编译器无法分配值,因为这些值可能需要解释器执行某些代码以返回要分配的值。在这个阶段,我们还没有执行代码。
第2阶段:执行。解释器看到您想要将变量someFunction传递给setTimeout。它确实如此。不幸的是,someFunction的当前值是未定义的。
1 2 3 4 5
| // 2
(function() {
setTimeout(someFunction, 10);
function someFunction() { alert('here2'); }
})(); |
第1阶段:编译。编译器看到你声明一个名为someFunction的函数,因此它创建它。
阶段2:解释器看到您要将someFunction传递给setTimeout。它确实如此。 someFunction的当前值是其编译的函数声明。
1 2 3 4 5
| // 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实际上并没有传递someFunction传递给它的变量(这意味着创建了一个闭包),而是只获取someFunction引用的对象(在本例中是一个函数)。这是JavaScript中用于破解闭包的最广泛使用的机制(例如在循环中)。
-
这是一个非常好的答案。
-
这可能是对闭包的理解不足,但我一直认为它是对作用域的访问,而不是在一个作用域和另一个作用域之间创建。我还认为它是在范围级别,而不是变量级别。您是否介意对此进行详细阐述,或者将我指向可以阅读的内容的方向?再一次,很好的答案,希望我能两次投票。
-
这个答案让我希望我可以在同一个答案上多次投票。真的是一个很好的答案。谢谢
-
@Matt:我已经在其他地方(好几次)在SO上解释过了。我最喜欢的一些解释:stackoverflow.com/questions/3572480/…
-
@Matt:另外:stackoverflow.com/questions/61088/hidden-features-of-javascript/…
-
@Matt:从技术上讲,闭包不涉及范围而是堆栈帧(也称为激活记录)。闭包是堆栈帧之间共享的变量。堆栈框架用于定义对象的类。换句话说,范围是程序员在代码结构中感知的范围。堆栈帧是在运行时在内存中创建的。这不是真的那样,但足够接近。在考虑运行时行为时,基于范围的理解有时是不够的。
-
@slebetman:这完全有道理,谢谢:)基于你的类比,闭包将是一个私有静态变量(在java术语中),对吧?我想这就是我从javascript和ruby等语言中学习它们而不是真正的函数式语言。
-
谢谢。编辑了问题标题和标签,以反映这不是关于闭包的事实。
-
@slebetman为您解释示例3,您提到setTimeout中的匿名函数创建了someFunction变量的闭包,此时,someFunction仍未定义 - 这是有道理的。似乎示例3没有返回undefined的唯一原因是因为setTimeout函数(10毫秒的延迟允许JavaScript对someFunction执行下一个赋值语句,从而使其定义)对吗?
-
@WillsonMock说的没错
-
你说:name and arguments are of course optional在你的函数definition解释中。现在这令人困惑.. MDN,MSDN,ES3,ES5和我读过的所有书籍都与该陈述相矛盾,并且说在函数声明中只有参数是可选的:function Identifier ( FormalParameterList opt ){ FunctionBody }(所以标识符是必需的)但FunctionExpression:function Identifier opt ( FormalParameterList opt ){ FunctionBody }那么,我错过了什么?
-
@GitaarLAB:没有标识符它仍然是合法的我相信它被编译为表达式而不是声明。如果表达式的结果被丢弃(未使用=运算符分配),则迄今为止没有浏览器会生成错误
-
@GitaarLAB:当我写下我的答案时,我的基础不是基于任何文档或规范,而是基于我在IE,FF,Chrome和Opera上测试过的测试脚本。 当我声明没有标识符的函数时,我测试的所有浏览器都没有生成任何错误消息。
-
@GitaarLAB:刚刚在最新版本的Chrome上测试过它,如果没有给出标识符,它现在似乎出错了。 所以我的答案似乎已经过时了。 我会解决它。
-
@slebetman:谢谢你! 对于这两个阶段,您是否可以参考浏览器引擎文档? 我一直在努力获得有关编译阶段的更多信息,以及在此阶段发生的确切情况,并且没有太多运气找到好的解释。 谢谢!
Javascript的范围是基于功能的,而不是严格的词法范围。 这意味着
-
Somefunction1是从封闭函数的开头定义的,但是在分配之前它的内容是未定义的。
-
在第二个例子中,赋值是声明的一部分,因此它"移动"到顶部。
-
在第三个例子中,当定义了匿名内部闭包时,该变量存在,但是直到10秒之后它才被使用,之后该值已被赋值。
-
第四个例子有第二和第三个原因
-
在你的第一个要点,你的意思是someFunction1?
-
tks,固定.....
这听起来像是遵循良好程序以避免麻烦的基本情况。在使用它们之前声明变量和函数,并声明如下函数:
1
| function name (arguments) {code} |
避免用var声明它们。这只是草率并导致问题。如果你养成了在使用它之前宣布一切的习惯,那么你的大部分问题都会匆匆消失。在声明变量时,我会立即使用有效值初始化它们,以确保它们都不是未定义的。我还倾向于包含在函数使用它们之前检查全局变量的有效值的代码。这是防止错误的额外保护措施。
所有这些工作原理的技术细节有点像手榴弹玩耍时手榴弹的物理特性。我的简单建议是首先不要玩手榴弹。
代码开头的一些简单声明可能会解决大多数这类问题,但可能仍需要对代码进行一些清理。
附加说明:
我运行了几个实验,似乎如果你按照这里描述的方式声明所有函数,它们的顺序并不重要。如果函数A使用函数B,函数B不必在之前声明功能A.
因此,首先声明所有函数,然后声明全局变量,然后将其他代码放在最后。遵循这些经验法则,你不会出错。甚至最好将您的声明放在网页的头部,并将其他代码放在正文中,以确保执行这些规则。
因为在执行对setTimeout()的调用时尚未分配someFunction1。
someFunction3可能看起来像一个类似的情况,但是由于在这种情况下你传递了一个将someFunction3()包装到setTimeout()的函数,所以直到稍后才会对someFunction3()的调用进行求值。
-
但是,当执行对setTimeout()的调用时,尚未分配someFunction2 ...?
-
@jnylen:使用function关键字声明函数并不等同于为变量赋予匿名函数。声明为function foo()的函数被"提升"到当前作用域的开头,而变量赋值则发生在它们被写入的位置。
-
功能+1是特殊的。然而,仅仅因为它可以工作并不意味着它应该被完成。在使用之前始终声明。
-
@mway:就我而言,我把我的代码组织在一个"类"中的部分:私有变量,事件处理程序,私有函数,然后是公共函数。我需要一个事件处理程序来调用我的一个私有函数。对我来说,以这种方式保持代码组织胜过在词汇上排序声明。