了解事件处理程序的javascript闭包

时间:2013-10-29 10:36:14

标签: javascript closures

我对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等”

好的,然后我用Google搜索并找到答案,其中说: -

  

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

好的,现在一切都在我头顶......这怎么可能?坦率地说,我是一个愚蠢的JavaScript,并试图尽可能多地学习。所以,我要通过基础知识!!

同时我也在阅读how-do-javascript-closures-work

的详细信息

3 个答案:

答案 0 :(得分:14)

除此部分外,其中大部分都会按预期方式工作:

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

有人可能会认为每个addEventListener都有三个参数:

  1. "click"
  2. 功能的结果
  3. false
  4. 如果是这种情况,五个事件监听器将如下所示:

    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点击按钮对应的数字呢?如果使用立即执行的函数,则可以执行此操作,因此返回函数的结果,而不是函数本身。为此,您只需将函数表达式包装在parens中,然后使用();

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

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

    我们可以使事件监听器的第二个参数立即执行like so

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

    然后你会在加载页面后立即得到5个警报,0-4,点击时没有任何警告,因为该函数没有返回任何内容。

    这不是我们真正想要的。我们真正想要的是让addEventListener从该循环中触发。简单易用 - 只需将其包装在一个立即执行的功能中:

    (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); 传递给它,但除此之外它们都是相同的。现在该函数立即被触发,a.k.a。我们将立即获得该函数的结果。这个新的立即执行功能的结果是什么?它实际上会根据循环的迭代次数而改变。以下是循环每次迭代的函数结果:

    i

    因此,您现在可以看到,如果function newFunc(){}在其中进行了硬编码,那么将在点击时触发的五个函数都具有值。如果你得到这个,你会得到关闭。就个人而言,我不明白为什么人们在解释问题时会使问题过于复杂。请记住以下内容:

    1. 子功能可以访问其父变量。
    2. 用作表达式的函数(例如,分配给变量或其他内容,未定义为( ... )();)使用作为函数而不是作为函数的结果即可。
    3. 如果您需要该功能的结果,可以通过将其包装在{{1}}
    4. 中立即执行该功能

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

答案 1 :(得分:3)

编写代码,如

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 here函数只是将范围分开。

演示: your code examplemy code example

答案 2 :(得分:0)

上面给出的解释很好。但是,现在有一种更简单的方法,而不是使用( I 立即 I 调用 F 连接 E xpression)< strong> IIFE 。 ECMAScript 2015年引入了一个新关键字letletvar的工作方式类似,不同之处在于它是以块为单位因此,只需引入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();