井字棋游戏重启按钮事件异常:函数触发过早或重复触发
解决井字棋重启按钮的事件触发异常问题
问题根源分析
第一个代码版本的自动触发问题
你在addEventListener里写了mainLogic(i, mainArray),这是直接调用函数,而非传递函数引用。浏览器会立即执行这个函数,导致页面加载或点击重启时所有格子的逻辑被自动触发,根本等不到用户点击。
第二个代码版本的重复触发问题
循环内定义命名函数mainLogic,每次循环都会创建一个全新的函数实例。移除监听时,mainLogic指向的是最后一次循环创建的函数,之前添加的监听根本没被移除。用var mainLogic时,变量提升导致每次循环都覆盖这个变量,最终移除的还是最后一个函数,上一轮未点击的格子会残留旧的监听,点击时触发两次,导致标记错误。
修正方案(推荐第一种)
方案1:使用统一的回调函数,通过事件对象获取格子索引
这种方式避免闭包和函数引用不一致的问题,逻辑更清晰:
function getReady() { // 重置游戏状态(必须做,否则重启后状态不对) if (gameOver) { boxes.forEach(box => { box.removeEventListener('click', handleBoxClick); box.textContent = ''; }); mainArray = Array(9).fill(null); // 重置存储X/O的数组 gameOver = false; } // 统一的格子点击处理函数 function handleBoxClick(event) { const clickedBox = event.target; // 从NodeList中获取当前格子的索引 const boxIndex = Array.from(boxes).indexOf(clickedBox); // 跳过已填充或游戏结束的情况 if (mainArray[boxIndex] || gameOver) return; const sign = checkState(); clickedBox.textContent = sign; mainArray[boxIndex] = sign; checkGame(sign, mainArray); } // 给所有格子添加监听 boxes.forEach(box => { box.addEventListener('click', handleBoxClick, { once: true }); }); }
方案2:用自定义属性存储函数引用(适合需要保留索引的场景)
如果必须在循环里绑定索引,可以把每个格子的回调函数存在元素的自定义属性中,确保移除时引用一致:
function getReady() { if (gameOver) { boxes.forEach(box => { // 从自定义属性获取之前的函数引用并移除 if (box._clickHandler) { box.removeEventListener('click', box._clickHandler); delete box._clickHandler; } box.textContent = ''; }); mainArray = Array(9).fill(null); gameOver = false; } for (let i = 0; i < boxes.length; i++) { const mainLogic = () => { if (mainArray[i] || gameOver) return; const sign = checkState(); boxes[i].textContent = sign; mainArray[i] = sign; checkGame(sign, mainArray); }; // 把函数存在格子元素的自定义属性里 boxes[i]._clickHandler = mainLogic; boxes[i].addEventListener('click', mainLogic, { once: true }); } }
关键注意事项
- 永远不要在
addEventListener的第二个参数里直接调用函数,要传递函数本身(比如handleBoxClick,而不是handleBoxClick())。 - 移除监听时必须使用和添加时完全相同的函数引用,匿名函数或每次创建新函数都会导致移除失败。
- 重启时必须重置所有游戏状态:包括存储X/O的数组、
gameOver标记、格子的文本内容,否则会出现逻辑混乱。
内容的提问来源于stack exchange,提问作者CookieJar
相关产品推荐
相关产品推荐

