闭包(let关键字)- JavaScript

11
function first(){
    var items = document.getElementsByTagName("li");

    for(var x = 0; x < items.length; x++){
        items[x].onclick = function() {
            console.log(x);
        }
    }
}

function second(){
    var items = document.getElementsByTagName("li");

    for(var x = 0; x < items.length; x++){
        (function(val) {
            items[val].onclick = function() {
                console.log(val);
            }
        })(x);
    }
}

function third(){
    var items = document.getElementsByTagName("li");

    for(let x = 0; x < items.length; x++){
        items[x].onclick = function() {
            console.log(x);
        }
    }
}

列表中有4个元素。这3个函数的输出结果:

第一个函数:4 4 4 4
第二个函数:0 1 2 3
第三个函数:0 1 2 3

我无法理解第三个函数的输出。在第二个函数中,每次调用IIFE都会创建一个新的函数对象和一个新的val变量。但是在第三个函数中,变量x只有一个副本,那么如何得出输出结果:0 1 2 3。

如果我错了,请指出来。


这就是使用 let 的目的之一,它只在创建它的作用域中定义。items.length 个匿名函数都引用了不同实例的 x,因为你使用 let 定义了它。 - Spencer Wieczorek
1
@SpencerWieczorek "...它只在创建它的范围内定义。" 虽然正确,但这实际上并没有回答问题。正如James Thorpe在下面指出的那样,当用于初始化for循环时,let在闭包内“按预期工作”的事实是let正常语义的一个例外,而不是其结果。 const也被限定在其块中,但我们不能做for (const a = i; a < n; a++) - 这将在第二个循环迭代中引发错误,因为我们正在尝试为已经初始化的const分配一个值。 - user1974458
简而言之,letconst都限定于包含它们的块级作用域,但是在for循环中,let还有一个特殊行为,将其限定于循环迭代而不是块级作用域。 - user1974458
@SpencerWieczorek 我知道。 :) 我认为我们现在是在纠缠哪一个被称为“特殊”行为,哪一个不是; 我的观点仅仅是你不能通过声明这是块范围的一个结果来正确回答原始问题,因为在Javascript循环中没有单一的块作用域行为。 “块作用域”可以轻松地暗示-例如在C ++中以及在Javascript中使用const-每次迭代只有一个绑定循环变量(而不是每次迭代一个变量),这将无法回答OP的问题。 - user1974458
2
换句话说,行为的原因不是 let 作用域限定在块级别,而是作用域限定在迭代中。从技术上讲,迭代变量甚至没有在块中被定义;它是在循环结构中定义的,这些绑定的工作方式是基于具体情况而定的。个人认为 letconst 之间的不一致性令人惊讶和反直觉。我们不能使用 const 来初始化 for 循环的唯一原因是循环只有一个绑定。然后,如果我们使用 let,他们会给我们提供多个绑定! - user1974458
显示剩余3条评论
3个回答

6
在MDN的let文档中,有一个示例涵盖了内部函数的干净代码。具体来说,在内部函数中编写更清晰的代码一节进行了详细介绍。请注意保留HTML标记,但不要添加解释性内容。
for (let i = 1; i <= 5; i++) {
  let item = document.createElement('li');
  item.appendChild(document.createTextNode('Item ' + i));

  item.onclick = function(ev) {
    console.log('Item ' + i + ' is clicked.');
  };
  list.appendChild(item);
}

上面的示例之所以按预期工作,是因为五个(匿名)内部函数的实例指向五个不同的变量i的实例。请注意,如果您将let替换为var,则不会按预期工作,因为所有内部函数将返回i的相同最终值:6。此外,我们可以通过将创建新元素的代码移动到每个循环的范围内,使得循环周围的范围更加清晰。 对于您的情况也是如此,因为您使用let,每个匿名函数都会引用不同的x实例。每次循环迭代都有一个不同的实例。这是由于let具有块级作用域,而不是var具有的全局函数作用域。

0

来自文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/let

let 允许你声明变量,其作用域限制在使用它的块级作用域、语句或表达式中。 这与 var 关键字不同,后者定义全局变量或函数整体范围内的本地变量,而不考虑块级作用域。

当变量为 var 时,它会像所有变量一样被提升。

当变量为 let 时,其作用域仅限于定义它的块中。


6
这并不能完全回答这个问题——对于使用 let 声明的变量,for 循环也有特殊语义,即每个变量不仅作用于整个 for 循环,而且还作用于每次迭代。 - James Thorpe
@JamesThorpe 我希望我能再点赞你的评论九次。我一直在阅读关于for循环中let行为及其对闭包的影响的答案,大多数答案都不完整或者是错误的。而你的回答则涵盖了这些问题。 - user1974458

0

这是关于 let 关键字最棘手的例子之一。

Let 绑定变量到块(在这种情况下是 for 循环)意味着它将变量绑定到循环的每次迭代。因此,当循环完成时,您将有 4 个项目(从 item[0] 到 item[3] 的项目)监听点击事件。

实际上,第三个函数 中的 for 循环 产生以下结果:

items[0].onclick = function() {
        console.log(0);
   }
items[1].onclick = function() {
        console.log(1);
   }
items[2].onclick = function() {
        console.log(2);
   }
items[3].onclick = function() {
        console.log(3);
   }

请务必阅读有关 Let 的更多信息在MDN这里,还可以在那里找到其他令人兴奋的案例。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接