You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

石头剪刀布游戏点击“再来一局”后事件监听器无法重新绑定

问题

我用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节点根本没有被绑定事件,所以点击无反应。

另外还有两个潜在问题:

  1. 事件绑定使用了捕获模式(第三个参数true),此处完全没必要,反而可能导致事件触发逻辑混乱。
  2. 游戏分数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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 07:29:50