JavaScript 闭包中的变量作用域问题

27 阅读1分钟

一、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>

三、问题描述

  1. 预期行为:当点击每个按钮时,预期在控制台输出该按钮对应的索引值,即点击 “按钮 0” 输出 “点击的按钮索引是: 0”,点击 “按钮 1” 输出 “点击的按钮索引是: 1”,以此类推。
  2. 实际行为:然而,实际情况是,无论点击哪个按钮,控制台输出的都是 “点击的按钮索引是: 5”。这是因为在 JavaScript 中,var声明的变量具有函数作用域,而不是块级作用域。在循环中,i变量是在函数作用域内共享的。当点击按钮时,循环已经结束,i的值已经变为 5。每个按钮的点击事件回调函数都引用了同一个i变量,此时i的值是循环结束后的最终值。

四、解决方案

  1. 使用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>
  1. 使用立即执行函数表达式(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>