一、Bug 场景
在一个网页应用中,需要创建一系列按钮,每个按钮点击后会显示其对应的索引值。开发人员使用循环来创建这些按钮,并为每个按钮添加点击事件监听器。
二、代码示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>闭包 Bug 示例</title>
</head>
<body>
<div id="button-container"></div>
<script>
const buttonContainer = document.getElementById('button-container');
for (var i = 0; i < 5; i++) {
const button = document.createElement('button');
button.textContent = `按钮 ${i}`;
button.addEventListener('click', function () {
console.log('点击的按钮索引是:', i);
});
buttonContainer.appendChild(button);
}
</script>
</body>
</html>
三、问题描述
- 预期行为:当点击每个按钮时,预期在控制台输出该按钮对应的索引值,即点击 “按钮 0” 输出 “点击的按钮索引是: 0”,点击 “按钮 1” 输出 “点击的按钮索引是: 1”,以此类推。
- 实际行为:然而,实际情况是,无论点击哪个按钮,控制台输出的都是 “点击的按钮索引是: 5”。这是因为在 JavaScript 中,
var声明的变量具有函数作用域,而不是块级作用域。在循环中,i变量是在函数作用域内共享的。当点击按钮时,循环已经结束,i的值已经变为 5。每个按钮的点击事件回调函数都引用了同一个i变量,此时i的值是循环结束后的最终值。
四、解决方案
- 使用
let声明变量:let声明的变量具有块级作用域,在每次循环迭代时,let会创建一个新的变量实例。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>闭包 Bug 修复 - let</title>
</head>
<body>
<div id="button-container"></div>
<script>
const buttonContainer = document.getElementById('button-container');
for (let i = 0; i < 5; i++) {
const button = document.createElement('button');
button.textContent = `按钮 ${i}`;
button.addEventListener('click', function () {
console.log('点击的按钮索引是:', i);
});
buttonContainer.appendChild(button);
}
</script>
</body>
</html>
- 使用立即执行函数表达式(IIFE) :通过 IIFE 为每个按钮的点击事件创建一个独立的作用域,将当前的
i值传递进去。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>闭包 Bug 修复 - IIFE</title>
</head>
<body>
<div id="button-container"></div>
<script>
const buttonContainer = document.getElementById('button-container');
for (var i = 0; i < 5; i++) {
const button = document.createElement('button');
button.textContent = `按钮 ${i}`;
(function (index) {
button.addEventListener('click', function () {
console.log('点击的按钮索引是:', index);
});
})(i);
buttonContainer.appendChild(button);
}
</script>
</body>
</html>