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

6 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();
Run Code Online (Sandbox Code Playgroud)

这段代码会输出什么?我回答了 - "对应于按钮的数字...... 1,2等"

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

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

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

同时我也在阅读关于如何做javascript-closures-work的细节

bre*_*ine 14

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

button.addEventListener("click", function (e) {
    alert(i);
}, false);
Run Code Online (Sandbox Code Playgroud)

人们可能期望每个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);
Run Code Online (Sandbox Code Playgroud)

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

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);
Run Code Online (Sandbox Code Playgroud)

如您所见,所有这些参数的第二个参数是以下函数:

function(e){
   alert(i);
}
Run Code Online (Sandbox Code Playgroud)

因此,当您单击其中一个按钮时,将执行上述功能.所以你点击一个按钮并执行该功能,这是一个有效的问题:什么是价值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
Run Code Online (Sandbox Code Playgroud)

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

我们可以让事件侦听器的第二个参数立即执行,像这样:

button.addEventListener("click", (function (e) {
    alert(i);
})(), false);
Run Code Online (Sandbox Code Playgroud)

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

那不是我们真正想要的.我们真正想要的是addEventListener从该循环中获取火力.简单易用 - 只需将其包装在一个立即执行的功能中:

(function(i){
   button.addEventListener("click", function (e) {
     alert(i);
   }, false);
})(i);
Run Code Online (Sandbox Code Playgroud)

请注意,();我们(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);
Run Code Online (Sandbox Code Playgroud)

所以你现在可以看到,点击时触发的五个函数都有值,如果i在那里硬编码的话.如果你得到这个,你会得到关闭.就个人而言,我不明白为什么人们在解释问题时会使问题过于复杂.请记住以下内容:

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

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


Bod*_*vid 5

上面给出的解释很棒。然而,现在有一种更简单的方法来代替使用立即调用函数表达式)IIFEECMAScript 2015 引入了一个新的关键字。与工作方式类似,只是它是块作用域的。因此,只需引入关键字即可重构代码。letletvarlet

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();
Run Code Online (Sandbox Code Playgroud)