石头剪刀布游戏点击“再来一局”后事件监听器无法重新绑定
问题
我用HTML、CSS和JavaScript开发了一款简易石头剪刀布游戏,初始状态下用户可正常选择并与电脑对战。但当一局游戏结束并点击“再来一局”按钮后,游戏选项(rock、paper、scissor)的事件监听器无法正确重新绑定,导致用户无法进行下一轮游戏。
已尝试移除并重新绑定事件监听器,但未达预期效果;游戏UI元素可正常重置,但选项的点击事件未重新生效;浏览器控制台无报错;已检查HTML结构、CSS类并确认事件监听器代码已执行。
相关代码如下:
function getComputerChoice(){ const choice = ['rock','paper','scissor'] const length = choice.length return choice[Math.floor(Math.random() * length)] } const choicesImage = document.querySelectorAll('.choice img') const choices = document.querySelectorAll('.choice') const container = document.querySelector('.container') const playerResult = document.querySelector('.playerChoice') const computerResult = document.querySelector('.computerChoice') const innerContent = container.innerHTML const status = document.querySelector('.status') let playerCurrentScore = 0 , computerCurrentScore = 0 function declareResult(){ if(playerCurrentScore === 5 || computerCurrentScore === 5 ){ container.classList.add('new') const newContent = document.querySelector('.new') newContent.textContent = playerCurrentScore === 5 ? 'Player Wins!':'Computer Wins!' newContent.style.fontSize = "35px" newContent.style.textAlign = "center" const btn = document.createElement('button') btn.textContent = "Play Again" newContent.append(btn) btn.addEventListener('click',changeContent) } } function changeContent(){ container.classList.remove('new') container.innerHTML = innerContent attachEvents(); } function attachEvents(){ choicesImage.forEach((choice) => { choice.addEventListener('click',game,true); }); } attachEvents(); function game(e){ let index = -Infinity if(e.target.alt === 'rock') index = 0 else if(e.target.alt === 'paper') index = 1 else if(e.target.alt === 'scissor') index = 2 choices[index].classList.add('active') choicesImage.forEach((others,indices) => { if(index !== indices) choices[indices].classList.remove('active') }); playerResult.src = 'images/rock.svg' computerResult.src = 'images/rock.svg' container.classList.add('start') status.textContent = "Loading.." setTimeout(() => { container.classList.remove('start') let user = e.target.alt let computer= getComputerChoice() if(user === 'scissor'){ playerResult.src = `images/${user}.png` }else{ playerResult.src = `images/${user}.svg` } if(computer === 'scissor'){ computerResult.src = `images/${computer}.png` }else{ computerResult.src = `images/${computer}.svg` } let playerScore = document.querySelector('.playerScore') let computerScore = document.querySelector('.computerScore') if(user === 'rock' && computer === 'scissor' || user === 'paper' && computer === 'rock' || user === 'scissor' && computer === 'paper'){ status.textContent = `You win! ${user} beats ${computer}` playerCurrentScore++ playerScore.textContent = playerCurrentScore computerScore.textContent = computerCurrentScore }else if(user === computer){ status.textContent = `Draw Match...` playerScore.textContent = playerCurrentScore computerScore.textContent = computerCurrentScore }else{ status.textContent = `You Lose! ${computer} beats ${user}` computerCurrentScore++ playerScore.textContent = playerCurrentScore computerScore.textContent = computerCurrentScore } declareResult() },500) }
@import url('https://fonts.googleapis.com/css2?family=Poppins&display=swap'); :root{ --gradient: linear-gradient( to right, #8B008Ba1, #800080b2, slateblue, darkslateblue ); --bodyFont:'Poppins', sans-serif; } *{ box-sizing: border-box; margin: 0; padding: 0; } body{ background-image: var(--gradient); background-repeat: no-repeat; background-attachment: fixed; animation: bg-animation 20s ease infinite 2s alternate; background-size: 300%; font-size: 62.5%; font-family: var(--bodyFont); display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; overflow-y: hidden; } @keyframes bg-animation{ 0%{ background-position: left; } 50%{ background-position: right; } 100%{ background-position: left; } } .container{ background-color: white; padding: 2.5rem; width: 90%; max-width: 500px; border-radius: 1.5rem; box-shadow: 2px 2px 30px 4px rgba(60, 103, 108, 0.593); } .container.start .choice{ pointer-events: none; } .game-container{ display: flex; justify-content: space-around; } .playerChoice,.computerChoice{ width: 6rem; height: 6rem; transform: rotate(90deg); } .container.start .playerChoice{ animation: userShake .5s ease infinite; } @keyframes userShake{ 0%{ transform: rotate(85deg); } 50%{ transform: rotate(99deg); } } .container.start .computerChoice{ animation: computerShake .5s ease infinite; } @keyframes computerShake{ 0%{ transform: rotate(265deg) rotateY(175deg); } 50%{ transform: rotate(279deg) rotateY(190deg); } } .computerChoice{ transform: rotate(270deg) rotateY(180deg); } .status{ text-align: center; margin: 2.5rem auto; font-size: 1.5rem; } .choice-container{ display: flex; justify-content: space-around; align-items: center; } .choice{ display: flex; flex-direction: column; justify-content: center; align-items: center; opacity: 0.5; transition: opacity 0.1s; } .choice:hover{ cursor:pointer; opacity: 1; } .active{ opacity: 1; } .choice img{ width: 4rem; height: 4rem; } .desc{ font-size: 1.2rem; font-weight: bold; } .score{ display:flex; flex-direction: column; justify-content: center; align-items: center; } .player,.computer{ font-size: 1.2rem; margin-top: 2.0rem; } .playerScore,.computerScore{ font-size: 1.5rem; vertical-align: middle; font-weight: bold; margin-left: 1.0rem; } .container.new p{ font-size: 35px; text-align: center; } .container.new button{ display: block; width: 250px; height: 50px; border-radius: 10px; font-size: 15px; margin: auto; }
<div class="container"> <div class="game-container"> <img src="images/rock.svg" alt="player" class="playerChoice"> <img src="images/rock.svg" alt="computer" class="computerChoice"> </div> <p class="status"></p> <div class="choice-container"> <span class="choice"> <img src="images/rock.svg" alt="rock"> <p class="desc">Rock</p> </span> <span class="choice"> <img src="images/paper.svg" alt="paper"> <p class="desc">Paper</p> </span> <span class="choice"> <img src="images/scissor.png" alt="scissor"> <p class="desc">Scissor</p> </span> </div> <div class="results"> <div class="score"> <p class="player">Player Score: <span class="playerScore">0</span></p> <p class="computer">Computer Score: <span class="computerScore">0</span></p> </div> <p class="announce"></p> </div> </div>
原因分析
核心问题在于DOM元素引用失效:
- 页面初始化时,
choicesImage、choices等变量通过querySelectorAll获取的是初始DOM节点集合,这些引用指向的是最初的DOM节点。 - 执行
container.innerHTML = innerContent重置UI时,原来的DOM节点被完全替换成新节点,但choicesImage和choices依然指向已从DOM树中移除的旧节点。 - 调用
attachEvents()时,实际是给这些已失效的旧节点绑定事件,新渲染的UI节点根本没有被绑定事件,所以点击无反应。
另外还有两个潜在问题:
- 事件绑定使用了捕获模式(第三个参数
true),此处完全没必要,反而可能导致事件触发逻辑混乱。 - 游戏分数
playerCurrentScore和computerCurrentScore在重置UI时未清零,会导致新游戏直接延续上一局的分数。
解决思路与修正方案
1. 动态获取新DOM节点
在attachEvents()函数内部,每次重新获取当前DOM中的choicesImage,而非依赖初始化时的旧引用:
function attachEvents(){ // 每次绑定事件前,重新获取当前DOM中的元素 const currentChoicesImage = document.querySelectorAll('.choice img'); currentChoicesImage.forEach((choice) => { choice.addEventListener('click',game); // 去掉捕获模式,使用默认冒泡模式即可 }); }
2. 重置游戏分数
在changeContent()中添加分数清零逻辑,确保新游戏从0开始:
function changeContent(){ container.classList.remove('new'); container.innerHTML = innerContent; // 重置分数 playerCurrentScore = 0; computerCurrentScore = 0; // 同步更新UI上的分数显示 document.querySelector('.playerScore').textContent = '0'; document.querySelector('.computerScore').textContent = '0'; // 重置状态文本 document.querySelector('.status').textContent = ''; attachEvents(); }
3. 优化事件绑定逻辑(可选)
使用事件委托替代逐个绑定事件,这样即使DOM节点被替换,事件依然有效,无需手动重新绑定:
// 初始化时给父容器绑定一次事件即可,无需attachEvents函数 document.querySelector('.choice-container').addEventListener('click', (e) => { // 判断点击的是选项图片 if(e.target.matches('.choice img')){ game(e); } });
使用事件委托后,changeContent()中可去掉attachEvents()调用,简化代码逻辑。
内容的提问来源于stack exchange,提问作者Metha shankar G.A
相关产品推荐
相关产品推荐

