8

我对 javascript 很陌生,最近在了解闭包时,我遇到了面试官提出的一个问题:-

function initButtons() {
    var body = document.body,
        button, i;

    for (i = 0; i < 5; i++) {
        button = document.createElement("button");
        button.innerHTML = "Button " + i;
        button.addEventListener("click", function (e) {
            alert(i);
        }, false);
        body.appendChild(button);
    }
}
initButtons();

这段代码的输出是什么?为此我回答 - “与按钮相对应的数字.. 1、2等。”

好的,然后我用谷歌搜索并找到了一个答案,其中指出:-

发生这种情况的原因是,当在 for 循环的每次迭代期间调用 addEventListener 方法时,会创建一个闭包。

好吧,现在一切都在我头上……这怎么可能?坦率地说,我在 javascript 方面是个笨蛋,并试图尽可能多地学习。所以,我要通过基础!

与此同时,我也在阅读how-do-javascript-closures-work 的详细信息

4

3 回答 3

21

其中大部分将按照您期望的方式工作,除了这部分:

button.addEventListener("click", function (e) {
    alert(i);
}, false);

人们可能期望每个都addEventListener获得三个参数:

  1. "click"
  2. 函数的结果
  3. false

如果是这种情况,五个事件侦听器将如下所示:

button.addEventListener("click", alert(0), false);
button.addEventListener("click", alert(1), false);
button.addEventListener("click", alert(2), false);
button.addEventListener("click", alert(3), false);
button.addEventListener("click", alert(4), false);

但是,设置为第二个参数的不是函数的结果,而是函数本身。所以实际上,这里有五个事件监听器:

button.addEventListener("click", function(e){alert(i);}, false);
button.addEventListener("click", function(e){alert(i);}, false);
button.addEventListener("click", function(e){alert(i);}, false);
button.addEventListener("click", function(e){alert(i);}, false);
button.addEventListener("click", function(e){alert(i);}, false);

如您所见,它们的第二个参数是以下函数:

function(e){
   alert(i);
}

因此,当您单击其中一个按钮时,将执行上述功能。因此,您单击一个按钮并执行该函数,这是一个可操作的问题: 的值是i多少?上下文在这里很重要。上下文不是循环的中间,上下文是你点击了按钮并触发了这个函数。

一个聪明的猜测i是在这种情况下是未定义的。但是当一个函数在另一个函数中创建时会发生这个棘手的事情:子函数可以访问父函数中的变量。所以i还是可以的!那么现在是什么i?它是5,因为我们已经将它循环到 5,它仍然是 5。

所以当你听到人们谈论闭包时,他们真正的意思是“子功能”。子函数很棘手,因为它们以这种方式携带其父母的变量。

那么如果你想让按钮alert的编号对应于被点击的按钮呢?如果您使用一个立即执行的函数,则可以这样做,因此返回函数的结果,而不是函数本身。为此,您只需将函数表达式包装在括号中,然后使用();.

var one = function(){}      // one contains a function.
var two = (function(){})(); // two contains the result of a function

让我们稍微玩一下,看看它会如何影响代码。

我们可以让事件监听器的第二个参数立即执行,如下所示

button.addEventListener("click", (function (e) {
    alert(i);
})(), false);

然后你会在加载页面时立即收到五个警报,0-4,单击时什么也没有,因为该函数没有返回任何内容。

这不是我们真正想要的。我们真正想要的是addEventListener从那个循环中触发。Easy peasy——只需将其包装在一个立即执行的函数中:

(function(i){
   button.addEventListener("click", function (e) {
     alert(i);
   }, false);
})(i);

请注意,不是();我们(i);现在有,因为我们将参数传递i给它,但除此之外它都是一样的。现在该函数立即被触发,也就是我们将立即获得该函数的结果。这个新的立即执行功能的结果是什么?它实际上会根据它所在的循环迭代而变化。以下是循环每次迭代的函数结果:

button.addEventListener("click", function (e) {
  alert(0);
}, false);
button.addEventListener("click", function (e) {
  alert(1);
}, false);
button.addEventListener("click", function (e) {
  alert(2);
}, false);
button.addEventListener("click", function (e) {
  alert(3);
}, false);
button.addEventListener("click", function (e) {
  alert(4);
}, false);

因此,您现在可以看到,在单击时触发的五个函数都具有i硬编码的值。如果你得到这个,你就会得到关闭。就个人而言,我不明白为什么人们在解释问题时会过度复杂化。请记住以下几点:

  1. 子函数可以访问其父函数的变量。
  2. 用作表达式的函数(例如,分配给变量或其他东西,未定义为function newFunc(){})用作函数而不是函数的结果
  3. 如果你想要函数的结果,你可以通过包装它立即执行函数( ... )();

就我个人而言,我发现这是一种更直观的方式来理解闭包,而无需使用所有疯狂的术语。

于 2013-10-29T20:36:50.127 回答
4

像这样写你的代码

function initButtons() {
    var body = document.body, button, i;
    for (i = 0; i < 5; i++) {
        (function(i) {
            button = document.createElement("button");
            button.innerHTML = "Button " + i;
            button.addEventListener("click", function (e) {
                alert(i);
            }, false);
            body.appendChild(button);
        }(i));
    }
}
initButtons();

否则,所有警报将显示 的最后结果i,因为在您的代码i中是在全局范围内,并且每次循环发生时都会更新。在这个例子中,这里的 wrapper/anonymous 函数只是分隔作用域。

演示: 您的代码示例我的代码示例

于 2013-10-29T11:21:22.847 回答
3

上面给出的解释很棒。然而,现在有一种更简单的方法,而不是使用 ( I mmediately I nvoked F unction E expression ) IIFEECMAScript 2015 引入了一个新关键字letlet与工作方式类似,var只是它是块作用域的。因此,只需引入let关键字即可重构代码。

function initButtons() {
    var body = document.body,
    button;

    for (let i = 0; i < 5; i++) {
        button = document.createElement("button");
        button.innerHTML = "Button " + i;
        button.addEventListener("click", function (e) {
            alert(i);
        }, false);
        body.appendChild(button);
    }
}
initButtons();
于 2020-09-18T09:00:59.647 回答