Javascript 闭包 - 变量范围问题

发布于 2024-09-10 11:27:48 字数 1141 浏览 7 评论 0原文

我正在阅读 Mozilla 开发人员的关于闭包的网站,我注意到在他们的常见错误示例中,他们有这样的代码:

<p id="help">Helpful notes will appear here</p>  
<p>E-mail: <input type="text" id="email" name="email"></p>  
<p>Name: <input type="text" id="name" name="name"></p>  
<p>Age: <input type="text" id="age" name="age"></p>  

他们

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

说对于 onFocus 事件,代码只会显示最后一项的帮助,因为所有分配给 onFocus 事件的匿名函数在“item”变量周围有一个闭包,这是有意义的,因为在 JavaScript 中变量没有块作用域。解决方案是使用“let item = ...”来代替,因为这样它就有了块作用域。

但是,我想知道为什么你不能在 for 循环上方声明“var item”?然后它具有 setupHelp() 的范围,并且每次迭代都为它分配一个不同的值,然后该值将被捕获为闭包中的当前值......对吗?

I'm reading the Mozilla developer's site on closures, and I noticed in their example for common mistakes, they had this code:

<p id="help">Helpful notes will appear here</p>  
<p>E-mail: <input type="text" id="email" name="email"></p>  
<p>Name: <input type="text" id="name" name="name"></p>  
<p>Age: <input type="text" id="age" name="age"></p>  

and

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

and they said that for the onFocus event, the code would only show help for the last item because all of the anonymous functions assigned to the onFocus event have a closure around the 'item' variable, which makes sense because in JavaScript variables do not have block scope. The solution was to use 'let item = ...' instead, for then it has block scope.

However, what I wonder is why couldn't you declare 'var item' right above the for loop? Then it has the scope of setupHelp(), and each iteration you are assigning it a different value, which would then be captured as its current value in the closure... right?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

旧竹 2024-09-17 11:27:48

这是因为在评估 item.help 时,循环将完全完成。相反,您可以使用闭包来做到这一点:

for (var i = 0; i < helpText.length; i++) {
   document.getElementById(helpText[i].id).onfocus = function(item) {
           return function() {showHelp(item.help);};
         }(helpText[i]);
}

JavaScript 没有块作用域,但它有函数作用域。通过创建闭包,我们永久捕获对 helpText[i] 的引用。

Its because at the time item.help is evaluated, the loop would have completed in its entirety. Instead, you can do this with a closure:

for (var i = 0; i < helpText.length; i++) {
   document.getElementById(helpText[i].id).onfocus = function(item) {
           return function() {showHelp(item.help);};
         }(helpText[i]);
}

JavaScript doesn't have block scope but it does have function-scope. By creating a closure, we are capturing the reference to helpText[i] permanently.

一身骄傲 2024-09-17 11:27:48

闭包是一个函数以及该函数的作用域环境。

它有助于理解 Javascript 在这种情况下如何实现作用域。事实上,它只是一系列嵌套的字典。考虑以下代码:

var global1 = "foo";

function myFunc() {
    var x = 0;
    global1 = "bar";
}

myFunc();

当程序开始运行时,您有一个作用域字典,即全局字典,其中可能定义了许多内容:

{ global1: "foo", myFunc:<function code> }

假设您调用 myFunc,它有一个局部变量 x。为此函数的执行创建一个新作用域。该函数的局部作用域如下所示:

{ x: 0 }

它还包含对其父作用域的引用。所以函数的整个作用域如下所示:

{ x: 0, parentScope: { global1: "foo", myFunc:<function code> } }

这允许 myFunc 修改 global1。在 Javascript 中,每当您尝试为变量赋值时,它都会首先检查变量名称的本地范围。如果没有找到,它会检查父作用域以及该作用域的父作用域等,直到找到该变量。

闭包实际上是一个函数加上一个指向该函数作用域的指针(其中包含指向其父作用域的指针,等等)。因此,在您的示例中,在 for 循环执行完毕后,作用域可能如下所示:

setupHelpScope = {
  helpText:<...>,
  i: 3, 
  item: {'id': 'age', 'help': 'Your age (you must be over 16)'},
  parentScope: <...>
}

您创建的每个闭包都将指向此单个作用域对象。如果我们要列出您创建的每个闭包,它看起来会像这样:

[anonymousFunction1, setupHelpScope]
[anonymousFunction2, setupHelpScope]
[anonymousFunction3, setupHelpScope]

当这些函数中的任何一个执行时,它都会使用它传递的作用域对象 - 在这种情况下,每个函数都有相同的作用域对象!每个变量都会查看相同的 item 变量并看到相同的值,这是 for 循环设置的最后一个值。

要回答您的问题,无论您在 for 循环上方还是在循环内部添加 var item 都没有关系。由于 for 循环不会创建自己的作用域,因此 item 将存储在当前函数的作用域字典中,即 setupHelpScope。在 for 循环内生成的封装将始终指向 setupHelpScope

一些重要的注意事项:

  • 发生这种行为的原因是,在 Javascript 中,for 循环没有自己的作用域 - 它们只是使用封闭函数的作用域。对于 ifwhileswitch 等也是如此。另一方面,如果这是 C#,则新的作用域对象将是为每个循环创建,每个闭包将包含一个指向其自己唯一范围的指针。
  • 请注意,如果 anonymousFunction1 修改其范围内的变量,它也会修改其他匿名函数的该变量。这可能会导致一些非常奇怪的互动。
  • 作用域只是对象,就像您用来编程的对象一样。具体来说,它们是字典。 JS 虚拟机像其他任何东西一样管理它们从内存中的删除 - 使用垃圾收集器。因此,过度使用闭包可能会造成真正的内存膨胀。由于闭包包含一个指向作用域对象的指针(该对象又包含一个指向其父作用域对象的指针等等),因此整个作用域链无法被垃圾收集,并且必须保留在内存中。

进一步阅读:

A closure is a function and the scoped environment of that function.

It helps to understand how Javascript implements scope in this case. It is, in fact, just a series of nested dictionaries. Consider this code:

var global1 = "foo";

function myFunc() {
    var x = 0;
    global1 = "bar";
}

myFunc();

When the program starts running, you have a single scope dictionary, the global dictionary, which might have a number of things defined in it:

{ global1: "foo", myFunc:<function code> }

Say you call myFunc, which has a local variable x. A new scope is created for this function's execution. The function's local scope looks like this:

{ x: 0 }

It also contains a reference to its parent scope. So the entire scope of the function looks like this:

{ x: 0, parentScope: { global1: "foo", myFunc:<function code> } }

This allows myFunc to modify global1. In Javascript, whenever you attempt to assign a value to a variable, it first checks the local scope for the variable name. If it isn't found, it checks the parentScope, and that scope's parentScope, etc. until the variable is found.

A closure is literally a function plus a pointer to that function's scope (which contains a pointer to its parent scope, and so on). So, in your example, after the for loop has finished executing, the scope might look like this:

setupHelpScope = {
  helpText:<...>,
  i: 3, 
  item: {'id': 'age', 'help': 'Your age (you must be over 16)'},
  parentScope: <...>
}

Every closure you create will point to this single scope object. If we were to list every closure that you created, it would look something like this:

[anonymousFunction1, setupHelpScope]
[anonymousFunction2, setupHelpScope]
[anonymousFunction3, setupHelpScope]

When any of these functions executes, it uses the scope object that it was passed - in this case, it's the same scope object for each function! Each one will look at the same item variable and see the same value, which is the last one set by your for loop.

To answer your question, it doesn't matter whether you add var item above the for loop or inside it. Because for loops do not create their own scope, item will be stored in the current function's scope dictionary, which is setupHelpScope. Enclosures generated inside the for loop will always point to setupHelpScope.

Some important notes:

  • This behavior occurs because, in Javascript, for loops do not have their own scope - they just use the enclosing function's scope. This is also true of if, while, switch, etc. If this were C#, on the other hand, a new scope object would be created for each loop, and each closure would contain a pointer to its own unique scope.
  • Notice that if anonymousFunction1 modifies a variable in its scope, it modifies that variable for the other anonymous functions. This can lead to some really bizarre interactions.
  • Scopes are just objects, like the ones you program with. Specifically, they're dictionaries. The JS virtual machine manages their deletion from memory just like anything else - with the garbage collector. For this reason, overuse of closures can create a real memory bloat. Since a closure contains a pointer to a scope object (which in turn contains a pointer to its parent scope object and on and on), the entire scope chain cannot be garbage collected, and has to stick around in memory.

Further reading:

谈场末日恋爱 2024-09-17 11:27:48

我意识到最初的问题已有五年历史了...但是您也可以将不同/特殊范围绑定到您分配给每个元素的回调函数:

// Function only exists once in memory
function doOnFocus() {
   // ...but you make the assumption that it'll be called with
   //    the right "this" (context)
   var item = helpText[this.index];
   showHelp(item.help);
};

for (var i = 0; i < helpText.length; i++) {
   // Create the special context that the callback function
   // will be called with. This context will have an attr "i"
   // whose value is the current value of "i" in this loop in
   // each iteration
   var context = {index: i};

   document.getElementById(helpText[i].id).onfocus = doOnFocus.bind(context);
}

如果您想要单行(或关闭)到它):

// Kind of messy...
for (var i = 0; i < helpText.length; i++) {
   document.getElementById(helpText[i].id).onfocus = function(){
      showHelp(helpText[this.index].help);
   }.bind({index: i});
}

或者更好的是,您可以使用 EcmaScript 5.1 的 array.prototype.forEach,它可以为您解决范围问题。

helpText.forEach(function(help){
   document.getElementById(help.id).onfocus = function(){
      showHelp(help);
   };
});

I realize the original question is five years old... But you could also just bind a different/special scope to the callback function you assign to each element:

// Function only exists once in memory
function doOnFocus() {
   // ...but you make the assumption that it'll be called with
   //    the right "this" (context)
   var item = helpText[this.index];
   showHelp(item.help);
};

for (var i = 0; i < helpText.length; i++) {
   // Create the special context that the callback function
   // will be called with. This context will have an attr "i"
   // whose value is the current value of "i" in this loop in
   // each iteration
   var context = {index: i};

   document.getElementById(helpText[i].id).onfocus = doOnFocus.bind(context);
}

If you want a one-liner (or close to it):

// Kind of messy...
for (var i = 0; i < helpText.length; i++) {
   document.getElementById(helpText[i].id).onfocus = function(){
      showHelp(helpText[this.index].help);
   }.bind({index: i});
}

Or better yet, you can use EcmaScript 5.1's array.prototype.forEach, which fixes the scope problem for you.

helpText.forEach(function(help){
   document.getElementById(help.id).onfocus = function(){
      showHelp(help);
   };
});
最初的梦 2024-09-17 11:27:48

新作用域function 块中创建(以及with,但不要使用它)。像 for 这样的循环不会创建新的作用域。

因此,即使您在循环外声明变量,也会遇到完全相同的问题。

New scopes are only created in function blocks (and with, but don't use that). Loops like for don't create new scopes.

So even if you declared the variable outside the loop, you would run into the exact same problem.

失眠症患者 2024-09-17 11:27:48

即使它是在 for 循环之外声明的,每个匿名函数仍然会引用同一个变量,因此在循环之后,它们仍然会指向 item 的最终值。

Even if it's declared outside of the for loop, each of the anonymous functions will still be referring to the same variable, so after the loop, they'll all still point to the final value of item.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文