javascript闭包中的环境范围

时间:2013-01-01 21:47:54

标签: javascript closures

遇到Mozila developer network

中描述的闭包常见错误
  

它没有按预期工作。无论你关注什么领域,   将显示有关您年龄的消息。

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

setupHelp();

确实提供了解决方案。这是另一个封闭。

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

function makeHelpCallback(help) {
  return function() {
    showHelp(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 = makeHelpCallback(item.help);
  }
}

setupHelp(); 

MDN给出的解释是因为第二种解决方案创造了3种环境。

任何人都可以给我解释一下,“环境”的含义是什么,我该如何判断其范围?

2 个答案:

答案 0 :(得分:3)

在函数中使用变量名时,首先JavaScript解释器将该变量检查为函数中的局部变量。如果不存在这样的局部变量,则它会遍历到外部范围以查找变量。 (当定义一个函数时,它可以访问作用域链 - 作用域的层次结构,从定义它的外部函数的作用域开始,然后继续执行包含 函数的函数等等。)

这里,匿名onfocus处理函数使用变量标识符itemitem不是函数中的局部变量,因此当执行处理程序时,JavaScript必须遍历下一个外部作用域,它在item中定义setupHelp。您为每个项目定义了一个处理函数;但是,所有这些处理程序函数都在外部作用域中保存了对item相同的引用。每当调用这些函数中的任何一个时,JavaScript都会查找item的相同值,这是for循环完成时所持有的值。

在具有额外闭包的情况下,将item.help的当前值作为参数传递给makeHelpCallback,后者返回处理函数。由于JavaScript按值传递变量,因此每个处理函数现在都有一个单独的外部作用域,其值为help

答案 1 :(得分:2)

考虑一个更简单的例子:

for (var i = 0; i < 10; ++i) {
    document.getElementById('elem' + i).addEventListener('click', function() {
        document.getElementById('elem' + i).style.display = 'none';
    });
}

代码看起来很简单,对吧?看起来如果你单击一个它将隐藏它。

但事情并非如此。事件处理程序在i左右关闭。无论处理程序之外的i发生了什么,都会在处理程序内发生i(为了简化一些事情)。

Here就是这种行为的一个例子。请注意,无论您点击什么div,ELEM #10始终是隐藏的。 (它是10而不是9,因为i在循环条件失败之前最后一次递增。)


诱人的解决方法是试试这个:

for (var i = 0; i < 10; ++i) {
    document.getElementById('elem' + i).addEventListener('click', function() {
        var fix = i;
        document.getElementById('elem' + fix).style.display = 'none';
    });
}​

这不起作用,因为在i已经10之前不会处理分配。 (好吧,从技术上来说,直到点击事件发生才进行处理 - 假设i是10,然而理论上,点击可能在所有事件被绑定之前发生,使i低于当时10。)


你实际上可以使用内联使用的MDN技巧:

for (var i = 0; i < 10; ++i) {
    document.getElementById('elem' + i).addEventListener('click', (function(j) { return function() {
        document.getElementById('elem' + j).style.display = 'none';
    }}(i)));
}​

将变量传递给函数会使其被视为不同的“环境”。函数内部的j在调用函数时被冻结。然后内部闭合使用j。有趣的是,i仍然存在于这两个函数中。根据访问时间,它会介于010之间。

for (var i = 0; i < 10; ++i) {
    document.getElementById('elem' + i).addEventListener('click', (function(j) { 
        alert(i); //whichever value is being bound
        //note that i === j is always true here
        //j is a copy of i though and thus stops changing when i changes in the future 
        return function() {
            alert(i); //always 10
            document.getElementById('elem' + j).style.display = 'none';
        };
    }(i)));
}​

思考JS作用域的简化方法是嵌套函数继承父作用域(这是真的)。调用函数时,变量将按原样传递给调用范围。

这里有一个有趣的小警告:基元通过值和对象通过引用传递。

var x = 5; (function(n) { ++n; }(x)); /* x is still 5 */
var o = {foo: 'bar'}; (function(obj) { obj.far = 'baz'; }(o)); /* o === {foo: 'bar', far: 'baz'} */