循环内创建的TD元素ID均为循环最终值问题求助
嗨,这个问题我太熟啦!你遇到的是JavaScript闭包和变量作用域的经典陷阱,我给你掰扯清楚问题出在哪,再给你几个靠谱的解决方案~
问题根源:var的作用域坑
你大概率是用var声明循环变量或者td变量的对吧?比如类似这样的代码:
var i = 1; while(i <= 6) { var td = document.createElement('td'); td.innerHTML = i; td.id = '2018-02-' + i; td.onclick = function() { alert(td.id); // 或者你可能用了i的值,结果都是最后一次的 }; // 假设把td加到表格里 document.querySelector('table').appendChild(td); i++; }
这里的核心问题是:var声明的变量没有块级作用域,循环里的i和td其实都是同一个全局/函数级变量。当循环跑完后,i变成了7,td指向最后创建的那个单元格,所以所有点击事件触发时,引用的都是这个最终的变量值。
而innerHTML正常显示是因为赋值是即时执行的——每次循环到td.innerHTML = i时,i是当前循环的数值,直接就写到元素里了,不会受后续变量变化影响。
几种靠谱的解决方案
1. 用let代替var(ES6+最推荐)
let有块级作用域,每次循环都会创建一个新的变量绑定,相当于给每个循环周期都单独存了一份变量值:
let i = 1; while(i <= 6) { let td = document.createElement('td'); td.innerHTML = i; td.id = `2018-02-${i}`; // 用模板字符串更清爽 td.onclick = function() { alert(td.id); // 或者用this.id更稳妥! }; document.querySelector('table').appendChild(td); i++; }
如果改成for循环会更直观,因为let可以直接写在循环条件里:
for(let i = 1; i <= 6; i++) { let td = document.createElement('td'); td.innerHTML = i; td.id = `2018-02-${i}`; td.onclick = function() { alert(this.id); // this指向当前点击的TD,完全不用依赖外部变量 }; document.querySelector('table').appendChild(td); }
2. 立即执行函数(IIFE)——兼容旧浏览器
如果要支持ES6之前的老环境,用立即执行函数把每次循环的变量封装成独立作用域:
var i = 1; while(i <= 6) { // 把当前的i传进去,创建独立作用域 (function(currentI) { var td = document.createElement('td'); td.innerHTML = currentI; td.id = '2018-02-' + currentI; td.onclick = function() { alert(td.id); }; document.querySelector('table').appendChild(td); })(i); i++; }
这里每次循环都会调用一个匿名函数,把当前的i值作为参数传入,函数内部的currentI是独立的,所以每个点击回调引用的都是自己循环周期的变量。
3. 直接用this(最简单的偷懒办法)
其实还有个更直接的思路:在点击事件里用this,因为事件回调里的this就是触发事件的元素本身,完全不用依赖外部的td变量:
var i = 1; while(i <= 6) { var td = document.createElement('td'); td.innerHTML = i; td.id = '2018-02-' + i; td.onclick = function() { alert(this.id); // 直接拿当前点击元素的ID,完美避开闭包问题 }; document.querySelector('table').appendChild(td); i++; }
4. 事件委托(优雅的批量处理)
如果你的所有TD都在同一个父元素(比如tbody)下面,用事件委托更高效——不用给每个TD单独绑定事件,只给父元素绑一次就行:
// 先批量创建TD并添加到DOM var i = 1; var tbody = document.querySelector('tbody'); while(i <= 6) { var td = document.createElement('td'); td.innerHTML = i; td.id = `2018-02-${i}`; tbody.appendChild(td); i++; } // 给父元素绑定点击事件,判断点击的是TD tbody.addEventListener('click', function(e) { if(e.target.tagName === 'TD') { alert(e.target.id); } });
这种方式不仅解决了作用域问题,还减少了事件绑定的数量,性能更好,尤其是TD数量多的时候。
总结一下
核心问题就是var的作用域导致所有回调共享同一个变量,解决思路要么是给每个循环创建独立作用域(let/IIFE),要么是直接引用触发事件的元素本身(this/事件委托)。选哪个看你的浏览器兼容需求和代码风格啦~
内容的提问来源于stack exchange,提问作者user8782879

