一个关于 for 和 onclick() 事件的问题?

一个学习案例中遇到的问题

index.html

<div class="d-flex gap-3">
    <div class="box"></div>
    <div class="box"></div>
    <div class="box"></div>
</div>

index.css

.box {
    width: 100px;
    height: 50px;
    border: 1px solid;
}

index.js

var boxs = document.getElementsByClassName('box');

for (var i = 0; i < boxs.length; i++) {
    boxs[i].onclick = function () {  // i = 1,2,3
        console.log(i); // i = 3
    }
}

上述代码中, 我在循环里为每个 .box 添加点击事件, 但是在事件里的i始终是循环体执行完毕后的结果 3, 为什么会出现这种情况?

因为之前写代码在 onclick 事件里基本都是用 this, 或者用 let 来代替 var 来定义变量, 所以绕过了这个坑
但现在遇到了这样的 bug, 还是想问下导致这个问题的原因是什么?

补充

关于 var 的特性, 我能想到的是 全局污染和变量提升, 简单地说

  • 全局污染: for 中定义 var, 作用域会提升到全局
  • 变量提升: 允许先调用后定义

但是上述两个特性都不能合理的解释 -> 为什么在for循环中定义的onclick, 循环变量i在onclick事件中永远是for最终执行后的i?

阅读 2.8k
avatarAI BotBETA

这个问题的原因是 JavaScript 的变量提升(Hoisting)特性。

在 JavaScript 中,变量的声明会被提升到其所在作用域的顶部,但赋值操作不会。在你的代码中,var i 这一行代码被提升到了循环的顶部,但赋值 i = 0i = 1i = 2 这些操作并没有被提升。

所以,当你在循环中点击 .box 元素时,实际上 i 的值已经被设为了 3。这是因为循环结束后,i 的值已经变成了 3,并且这个值被赋给了每个 .box 元素的 onclick 事件处理器。

如果你希望在事件处理器中 i 的值是 012,你需要将 var i 改为 let i。使用 let 关键字声明的变量有块级作用域,并且不会发生变量提升。这样,每个 .box 元素的 onclick 事件处理器都会在其自己的作用域中获取到正确的 i 值。

以下是你的代码,其中将 var i 改为了 let i

<div class="d-flex gap-3">
    <div class="box"></div>
    <div class="box"></div>
    <div class="box"></div>
</div>
.box {
    width: 100px;
    height: 50px;
    border: 1px solid;
}
var boxs = document.getElementsByClassName('box');

for (let i = 0; i < boxs.length; i++) {
    boxs[i].onclick = function () {  // i = 0,1,2
        console.log(i); // i = 0,1,2
    }
}
6 个回答

你换个思路,在你的代码中有几个i呢,是不是只有一个,所有当for循环结束i的值也只能是一个,因为i一个变量不可能同时又是1又是2又是3

补充一个比较好理解的

var i = 0;
console.log(i); // 0
var i = 3;

上述代码中输出的 i = 0, 因为 console.log() 是同步方法, 当它执行完毕后才会执行下面的 i = 3


var i = 0;
boxs[i].onclick = function() {
    console.log(i); // 3
}
var i = 3;

而在 onclick 中, 因为是一个异步方法, 所以执行到时不会发生阻塞, 而是同时执行了 i = 3, 导致最终 click 触发时 i 的值已经是 3 了

这个循环转化这样就好理解了

var i = 0;
{
    boxs[i].onclick = function () {  
        console.log(i); // i = 3
    }
}
++i;
{
    boxs[i].onclick = function () {  
        console.log(i); // i = 3
    }
}
++i;
{
    boxs[i].onclick = function () {  
        console.log(i); // i = 3
    }
}

如果我们使用的是let,可以这样理解

{ 
    let t = 0;
    {    
        let i = t;
        boxs[i].onclick = function () {  
            console.log(i); // i = 3
        }
    }
    ++t;
    {
        let i = t;
        boxs[i].onclick = function () {  
            console.log(i); // i = 3
        }
    }
    ++t;
    {
        let i = t;
        boxs[i].onclick = function () {  
            console.log(i); // i = 3
        }
    }
}
新手上路,请多包涵

补充下,这题知识点是js 的作用域, 点击按钮的时候打印i, js引擎会去找click函数外部的作用域,变量i 的值。然而触发click事件时,<script> 引入的代码早就执行完了【此时i 为3】。你打个断点去chorme debugger, 一步步执行下就可以理解了。js 知识内容建议去看《JavaScript忍者秘籍》。

这问题当初学到闭包的时候就有这个例子,比较经典

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题