Javascript 臭名昭著的循环问题?

IT技术 javascript closures
2020-12-10 23:27:46

我有以下代码片段。

function addLinks () {
    for (var i=0, link; i<5; i++) {
        link = document.createElement("a");
        link.innerHTML = "Link " + i;
        link.onclick = function () {
            alert(i);
        };
        document.body.appendChild(link);
    }
}

上面的代码用于生成 5 个链接并将每个链接与警报事件绑定以显示当前链接 id。但它不起作用。当您单击生成的链接时,它们都会说“链接 5”。

但是下面的代码片段符合我们的预期。

function addLinks () {
    for (var i=0, link; i<5; i++) {
        link = document.createElement("a");
        link.innerHTML = "Link " + i;
        link.onclick = function (num) {
            return function () {
                alert(num);
            };
        }(i);
        document.body.appendChild(link);
    }
}

以上 2 个片段引用自此处正如作者的解释一样,闭包神奇。

但是它是如何工作的以及闭包如何使它工作都超出了我的理解。为什么第一个不起作用而第二个起作用?谁能详细解释一下这个魔法?

5个回答

引用我自己对第一个示例的解释:

JavaScript 的作用域是函数级的,而不是块级的,创建闭包只是意味着将封闭的作用域添加到封闭函数的词法环境中。

循环终止后,函数级变量 i 的值为 5,这就是内部函数“看到”的。

在第二个示例中,对于每个迭代步骤,外部函数文字将评估为具有自己的作用域和局部变量 的新函数对象num,其值设置为的当前值i由于num从未修改过,它将在闭包的整个生命周期内保持不变:由于函数对象是独立的,因此下一个迭代步骤不会覆盖旧值。

请记住,这种方法效率很低,因为必须为每个链接创建两个新的函数对象。这是不必要的,因为如果您使用 DOM 节点进行信息存储,它们可以很容易地共享:

function linkListener() {
    alert(this.i);
}

function addLinks () {
    for(var i = 0; i < 5; ++i) {
        var link = document.createElement('a');
        link.appendChild(document.createTextNode('Link ' + i));
        link.i = i;
        link.onclick = linkListener;
        document.body.appendChild(link);
    }
}
@PhilippLudwig 我建议替换link.i = ilink.setAttribute("data-link-index",i)和替换alert(this.i)alert(Number(this.getAttribute("data-link-index")))
2021-02-11 23:27:46
请记住,扩展 DOM(参见link.i = i;)被认为是一种不好的做法
2021-02-18 23:27:46
@check_ca 你会推荐什么?至少这个解决方案有效,不像基于闭包的解决方案。
2021-02-23 23:27:46
“如果您使用 DOM 节点进行信息存储,则可以轻松共享它们”- 很有教育意义,谢谢!!
2021-02-24 23:27:46
@check_ca,然而,同样的事情可以用数据属性来完成,或者像 jQuery 的 .data()。这些通常可以解决该文章中的问题(例如,数据是为用户保留的,因此未来的标准永远不会定义data-something属性)。
2021-03-08 23:27:46

我们在页面上有 5 个 div,每个都有一个 ID ... div1, div2, div3, div4, div5

jQuery 可以做到这一点......

for (var i=1; i<=5; i++) {
    $("#div" + i).click ( function() { alert ($(this).index()) } )
}

但真正解决这个问题(并慢慢建立它)......

第1步

for (var i=1; i<=5; i++) {
    $("#div" + i).click (
        // TODO: Write function to handle click event
    )
}

第2步

for (var i=1; i<=5; i++) {
    $("#div" + i).click (
        function(num) {
            // A functions variable values are set WHEN THE FUNCTION IS CALLED!
            // PLEASE UNDERSTAND THIS AND YOU ARE HOME AND DRY (took me 2 years)!
            // Now the click event is expecting a function as a handler so return it
            return function() { alert (num) }
        }(i) // We call the function here, passing in i
    )
}

简单易懂的替代方案

如果您无法理解这一点,那么这应该更容易理解并且具有相同的效果......

for (var i=1; i<=5; i++) {

    function clickHandler(num) {    
        $("#div" + i).click (
            function() { alert (num) }
        )
    }
    clickHandler(i);
    
}

如果您记得在调用函数时设置了函数变量值,这应该很容易理解(但这使用与以前完全相同的思维过程)

我注意到你需要更多的代表,所以对于直接版本+1!虽然我认为我个人会将clickHandler函数声明放在循环之外,只是为了风格。
2021-02-14 23:27:46
我正在为演示文稿寻找一些关于闭包的很好的解释......你的方法是迄今为止最好的,荣誉。
2021-02-24 23:27:46
我知道我迟到了,但无论如何。它被称为闭包。即使外部函数已经返回,内部函数也可以访问外部函数中的值。所以外部函数是一个 IIFE,所以它存储了 num 值。单击时,内部函数会执行并返回 num。
2021-03-04 23:27:46
这是一个我仍然无法理解的问题。当您说“在调用函数时设置值”时,您的意思是仅当在 div 上单击时才设置 div 的每个值?它一直通过引用来节省函数作用域
2021-03-05 23:27:46
糟糕的是,这不再起作用,警报根本不会显示,控制台中也没有任何内容。
2021-03-08 23:27:46

基本上,在第一个示例中,您将处理程序i内部onclick直接绑定处理程序i外部onclick因此,当处理程序i外部onclick发生变化时,处理程序的i内部onclick也会发生变化。

在第二个例子中,而不是将其绑定到numonclick处理程序中,你传递给函数,然后绑定到numonclick处理程序。当您将其传递给函数时,i复制的值,而不是绑定num所以当i变化时,num保持不变。复制的发生是因为 JavaScript 中的函数是“闭包”,这意味着一旦将某些内容传递给函数,它就会“关闭”以供外部修改。

我已经阅读了关于这个主题的几个答案,试图解释为什么。你最后一句话的后半句终于点亮了我的脑袋,……谢谢,谢谢,谢谢!
2021-03-08 23:27:46

其他人已经解释了发生了什么,这里有一个替代解决方案。

function addLinks () {
  for (var i = 0, link; i < 5; i++) {
    link = document.createElement("a");
    link.innerHTML = "Link " + i;

    with ({ n: i }) {
      link.onclick = function() {
        alert(n);
      };
    }
    document.body.appendChild(link);
  }
}

基本上穷人让绑定。

嗯,我以前从未见过使用 with 语句的解决方案,很好;)
2021-02-22 23:27:46

在第一个示例中,您只需将此函数绑定到 onclick 事件:

function() {alert(i);};

这意味着在点击事件上 js 应该提醒 addlink 函数 i 变量的值。由于 for loop(),它的值将是 5。

在第二个示例中,您生成一个与另一个函数绑定的函数:

function (num) {
  return function () { alert(num); };
}

这意味着:如果用一个值调用,返回一个函数来提醒输入值。例如调用function(3)将返回function() { alert(3) };

您在每次迭代时使用值 i 调用此函数,因此您为每个链接创建单独的 onclick 函数。

关键是在第一个示例中,您的函数包含一个变量引用,而在第二个示例中,在外部函数的帮助下,您将引用替换为实际值。这大致称为闭包,因为您将变量的当前值“封闭”在函数中,而不是保留对它的引用。