第6章 6.1 DOM 操作:querySelector/event
🎯 开场:为什么你点的外卖和你想的不一样?
你有没有过这种经历——在网上选中一家店,结果送来的东西完全不一样?
我有个朋友点了「红烧肉」,结果送来的是「红烧排骨」。他说他明明看的是红烧肉啊!
后来才发现,他点的是第二个菜,不是第一个。网页上显示的「红烧肉」其实是第二个选项,但他点击了第一个。
这像不像你写网页时的困惑?
- 想操作第二个按钮,结果操作的是第一个
- 想改标题的文字,结果改成了副标题
- 想给某个元素加动画,结果整个页面都动了
问题出在哪? 你没有准确地「选中」你想要的那个元素。
JavaScript 里有一个强大的工具能帮我们精准定位任何网页元素——就是今天要学的 DOM 操作。
学完这章,你能:
1. 用 querySelector 像 GPS 一样精准定位任意网页元素
2. 用事件监听让网页「活」起来,响应你的操作
3. 组合使用做出真正能交互的小工具
🧱 基础:DOM 是个什么鬼?
\n\n
\n\n
\n\n 1. DOM = 网页的「骨架」
先说个生活比喻:
想象你有一栋乐高积木房子。房子有很多零件(墙、门、窗、屋顶),这些零件按照一定顺序拼在一起,形成了整栋房子的结构。
DOM(Document Object Model)就是网页的「乐高结构图」。浏览器把你写的 HTML 标签(如 <div>、<p>、<button>)组织成一棵树,这棵树就叫 DOM 树。
网页结构:
<body>
<header>
<h1>我的网站</h1>
</header>
<main>
<div class="container">
<p id="intro">欢迎光临</p>
<button class="btn">点我</button>
</div>
</main>
</body>
用 DOM 树来表示:
body(身体)
├── header(头部)
│ └── h1(标题): "我的网站"
└── main(主体)
└── div.container(容器)
├── p#intro(段落): "欢迎光临"
└── button.btn(按钮): "点我"
为什么要学 DOM? 因为网页上每一个你能看到的东西,都是 DOM 树上的一个「节点」。你想让网页动起来,就得操作这些节点。
2. querySelector:网页元素的「搜索框」
想象你要在淘宝找一个商品。你不会去问客服「帮我找那个红色的上衣」,你会用搜索框搜。
querySelector 就是网页的搜索框,你告诉它「我要找什么」,它帮你找到第一个匹配的元素。
// 找到第一个 class 为 "btn" 的按钮
document.querySelector('.btn')
// 找到第一个 id 为 "title" 的元素
document.querySelector('#title')
// 找到第一个 <button> 标签
document.querySelector('button')
如果页面有多个匹配的元素,但你全都要?那就用 querySelectorAll,它会返回一个「列表」:
// 找到所有 class 为 "item" 的元素
document.querySelectorAll('.item')
一句话解释:querySelector 就像 Ctrl+F 搜索,但比 Ctrl+F 精准一万倍。
3. 创建和添加元素:网页的「装修队」
有时候你不想修改已有元素,而是想新增一个元素。就像装修房子,你需要先预制一个家具,再把它搬进去。
// 1. 创建一个新的 <div> 元素
let newDiv = document.createElement('div')
// 2. 给它加点内容
newDiv.textContent = '我是新来的'
// 3. 把它添加到页面里(比如添加到 body 的最后)
document.body.appendChild(newDiv)
一句话解释:创建元素是「造家具」,追加是「搬进房子」。
4. 事件监听:让网页「听话」
你按电梯按钮,电梯会动。你点外卖按钮,外卖会下单。网页也一样——你点击一个按钮,网页应该有所响应。
事件监听就是给网页元素装上一个「监听器」,一旦用户做了什么动作(点击、鼠标悬停、键盘输入),监听器就会触发你设定的函数。
// 获取按钮
let btn = document.querySelector('.btn')
// 给按钮添加「点击事件」监听器
btn.addEventListener('click', function() {
alert('按钮被点击了!')
})
当用户点击这个按钮,浏览器就会弹出「按钮被点击了!」的提示框。
你也可以把函数单独定义,让代码更清晰:
function handleClick() {
alert('按钮被点击了!')
}
btn.addEventListener('click', handleClick)
一句话解释:addEventListener 就是在元素上安装一个「警报器」,指定「发生什么事」时触发什么「动作」。
5. 事件冒泡:网页的「连锁反应」
这是一个很多新手会踩的坑。
想象你在家里厨房闻到烟味,你爸也闻到了,你妈也闻到了——但实际上只是你的锅里糊了。
事件冒泡就是这个原理:当你在一个子元素上触发事件,这个事件会「向上冒泡」到父元素、祖父元素,一路传到 body 和 document。
// 外层容器
let container = document.querySelector('.container')
// 里面的按钮
let btn = document.querySelector('.btn')
// 给按钮加点击事件
btn.addEventListener('click', function() {
console.log('按钮被点了')
})
// 给容器也加一个点击事件
container.addEventListener('click', function() {
console.log('容器被点了')
})
当你点击按钮,控制台会输出:
按钮被点了
容器被点了
因为点击事件先在按钮上触发,然后「冒泡」到容器。
有时候这是你想要的,比如点击整个卡片都能触发某个动作。有时候这不是你想要的,你需要阻止冒泡:
btn.addEventListener('click', function(event) {
event.stopPropagation() // 阻止事件继续向上冒泡
console.log('按钮被点了')
})
这样点击按钮就只会输出「按钮被点了」,不会触发容器的事件。
🔥 实战:3个小项目
项目 1:点击切换灯的开关(5分钟)
场景:网页上有一个灯的图标,点击一下亮,再点击一下灭。
这是最基础的交互——学会这个你就入门了!
完整可运行代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>灯的开关</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: #1a1a2e;
font-family: Arial, sans-serif;
}
.lamp {
font-size: 100px;
cursor: pointer;
transition: transform 0.2s;
}
.lamp:hover {
transform: scale(1.1);
}
.lamp.off::after {
content: '💡';
}
.lamp.on::after {
content: '🔦';
}
p {
color: white;
text-align: center;
margin-top: 20px;
}
</style>
</head>
<body>
<div>
<div id="lamp" class="lamp off"></div>
<p id="status">灯是灭的</p>
</div>
<script>
// 获取灯和状态文字的元素
let lampElement = document.querySelector('#lamp')
let statusText = document.querySelector('#status')
// 用一个变量记录灯的状态
let isOn = false
// 定义切换灯状态的函数
function toggleLamp() {
isOn = !isOn // 取反:开变关,关变开
if (isOn) {
// 灯开了
lampElement.className = 'lamp on'
statusText.textContent = '灯是亮的'
} else {
// 灯关了
lampElement.className = 'lamp off'
statusText.textContent = '灯是灭的'
}
}
// 给灯添加点击事件监听
lampElement.addEventListener('click', toggleLamp)
</script>
</body>
</html>
预期效果:
- 初始状态:显示 💡 灯泡图标,文字「灯是灭的」
- 点击一下:显示 🔦 手电筒图标,文字「灯是亮的」
- 再点击一下:又变回 💡 和「灯是灭的」
一句话解释:用一个布尔变量 isOn 记录状态,点击时取反(!isOn),根据状态切换显示。
项目 2:待办清单 To-Do List(15分钟)
场景:你每天有很多事要做,想在网页上记录下来。输入内容,点击添加,就能在列表里看到,还能点击删除。
这个比项目1更进一步——涉及创建元素、循环遍历、删除操作。
完整可运行代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>我的待办清单</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 500px;
margin: 50px auto;
padding: 20px;
background: #f5f5f5;
}
h1 {
text-align: center;
color: #333;
}
.input-group {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
input {
flex: 1;
padding: 12px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 16px;
}
input:focus {
border-color: #4CAF50;
outline: none;
}
button#addBtn {
padding: 12px 24px;
background: #4CAF50;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
}
button#addBtn:hover {
background: #45a049;
}
ul {
list-style: none;
padding: 0;
}
li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: white;
border-bottom: 1px solid #eee;
}
li:last-child {
border-bottom: none;
}
.deleteBtn {
background: #ff4444;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
}
.deleteBtn:hover {
background: #cc0000;
}
.empty {
text-align: center;
color: #999;
padding: 20px;
}
</style>
</head>
<body>
<h1>📝 我的待办清单</h1>
<div class="input-group">
<input type="text" id="todoInput" placeholder="输入新任务...">
<button id="addBtn">添加</button>
</div>
<ul id="todoList">
<li class="empty">还没有任务,添加一个吧!</li>
</ul>
<script>
// 获取需要用到的元素
let inputElement = document.querySelector('#todoInput')
let addBtn = document.querySelector('#addBtn')
let todoList = document.querySelector('#todoList')
// 定义添加任务的函数
function addTodo() {
let text = inputElement.value.trim() // 获取输入并去掉首尾空格
// 如果输入为空,提示用户
if (!text) {
alert('请输入任务内容!')
return
}
// 先移除「空状态」提示
let emptyHint = todoList.querySelector('.empty')
if (emptyHint) {
emptyHint.remove()
}
// 创建新的 li 元素
let newLi = document.createElement('li')
// 创建任务文字
let span = document.createElement('span')
span.textContent = text
// 创建删除按钮
let deleteBtn = document.createElement('button')
deleteBtn.textContent = '删除'
deleteBtn.className = 'deleteBtn'
// 给删除按钮添加点击事件
deleteBtn.addEventListener('click', function() {
newLi.remove() // 删除这个 li
// 如果列表空了,显示提示
if (todoList.children.length === 0) {
let emptyLi = document.createElement('li')
emptyLi.className = 'empty'
emptyLi.textContent = '还没有任务,添加一个吧!'
todoList.appendChild(emptyLi)
}
})
// 组装:把 span 和 deleteBtn 放进 li
newLi.appendChild(span)
newLi.appendChild(deleteBtn)
// 把 li 添加到列表
todoList.appendChild(newLi)
// 清空输入框
inputElement.value = ''
inputElement.focus()
}
// 点击添加按钮触发
addBtn.addEventListener('click', addTodo)
// 按回车也能添加
inputElement.addEventListener('keypress', function(event) {
if (event.key === 'Enter') {
addTodo()
}
})
</script>
</body>
</html>
预期效果:
- 在输入框输入文字,点击「添加」或按回车,任务出现在列表
- 每条任务右边有红色「删除」按钮,点击可删除
- 列表为空时显示提示文字
一句话解释:用 createElement 创建新元素,用 appendChild 添加到列表,删除时用 remove() 移除自身。
项目 3:简易天气预报卡片(15分钟)
场景:从 JSON 数据中读取几个城市的天气,在网页上展示成卡片。点击不同城市,切换显示详情。
这个项目综合了数据渲染、DOM 创建循环、事件委托。
完整可运行代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>天气预报</title>
<style>
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 30px;
margin: 0;
}
h1 {
text-align: center;
color: white;
margin-bottom: 30px;
}
.container {
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
max-width: 1000px;
margin: 0 auto;
}
.city-card {
background: white;
border-radius: 16px;
padding: 25px;
width: 200px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.city-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0,0,0,0.2);
}
.city-card.selected {
border: 3px solid #667eea;
}
.city-name {
font-size: 20px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
}
.weather-icon {
font-size: 50px;
text-align: center;
margin: 15px 0;
}
.temperature {
font-size: 28px;
color: #ff6b6b;
text-align: center;
font-weight: bold;
}
.weather-desc {
text-align: center;
color: #666;
margin-top: 5px;
}
.detail-panel {
background: white;
border-radius: 16px;
padding: 30px;
max-width: 500px;
margin: 30px auto;
text-align: center;
display: none;
}
.detail-panel.show {
display: block;
}
.detail-title {
font-size: 24px;
margin-bottom: 20px;
}
.detail-info {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
.detail-item {
background: #f8f8f8;
padding: 15px;
border-radius: 8px;
}
.detail-label {
color: #999;
font-size: 12px;
}
.detail-value {
font-size: 18px;
color: #333;
font-weight: bold;
}
</style>
</head>
<body>
<h1>🌤️ 天气预报</h1>
<div class="container" id="cityContainer">
<!-- 城市卡片会动态生成 -->
</div>
<div class="detail-panel" id="detailPanel">
<h2 class="detail-title" id="detailTitle">城市详情</h2>
<div class="detail-info" id="detailInfo"></div>
</div>
<script>
// 模拟的天气数据(实际项目中可能来自API)
let weatherData = [
{
city: '北京',
temp: 26,
humidity: 45,
wind: 12,
pressure: 1013,
icon: '☀️',
desc: '晴朗',
aqi: 58
},
{
city: '上海',
temp: 28,
humidity: 72,
wind: 8,
pressure: 1008,
icon: '🌤️',
desc: '多云',
aqi: 42
},
{
city: '广州',
temp: 32,
humidity: 85,
wind: 15,
pressure: 1005,
icon: '🌧️',
desc: '中雨',
aqi: 35
},
{
city: '成都',
temp: 24,
humidity: 68,
wind: 6,
pressure: 1015,
icon: '⛅',
desc: '阴天',
aqi: 78
}
]
let container = document.querySelector('#cityContainer')
let detailPanel = document.querySelector('#detailPanel')
let detailTitle = document.querySelector('#detailTitle')
let detailInfo = document.querySelector('#detailInfo')
// 遍历数据,生成每个城市的卡片
weatherData.forEach(function(cityData) {
// 创建卡片容器
let card = document.createElement('div')
card.className = 'city-card'
card.dataset.city = cityData.city // 记住这个城市名
// 创建卡片内容
card.innerHTML = `
<div class="city-name">${cityData.city}</div>
<div class="weather-icon">${cityData.icon}</div>
<div class="temperature">${cityData.temp}°C</div>
<div class="weather-desc">${cityData.desc}</div>
`
// 给卡片添加点击事件
card.addEventListener('click', function() {
// 移除其他卡片的选中状态
document.querySelectorAll('.city-card').forEach(function(c) {
c.classList.remove('selected')
})
// 给当前卡片添加选中状态
card.classList.add('selected')
// 显示详情面板
showDetail(cityData)
})
// 把卡片添加到容器
container.appendChild(card)
})
// 显示详情面板的函数
function showDetail(data) {
detailTitle.textContent = data.city + ' 详细天气'
detailInfo.innerHTML = `
<div class="detail-item">
<div class="detail-label">湿度</div>
<div class="detail-value">${data.humidity}%</div>
</div>
<div class="detail-item">
<div class="detail-label">风速</div>
<div class="detail-value">${data.wind} km/h</div>
</div>
<div class="detail-item">
<div class="detail-label">气压</div>
<div class="detail-value">${data.pressure} hPa</div>
</div>
<div class="detail-item">
<div class="detail-label">空气质量指数</div>
<div class="detail-value">${data.aqi}</div>
</div>
`
detailPanel.classList.add('show')
}
</script>
</body>
</html>
预期效果:
- 页面加载后显示 4 个城市的天气卡片
- 点击某个城市卡片,该卡片高亮,底部弹出详细面板显示更多信息
- 有悬停动画效果
一句话解释:用 forEach 遍历数据批量生成卡片,点击时调用 showDetail 显示详情。
💪 进阶:5个新人必踩的坑
坑1:找不到元素?因为页面还没加载完!
❌ 错误写法:
let btn = document.querySelector('.btn')
btn.addEventListener('click', handleClick)
// 报错:Cannot read property 'addEventListener' of null
为什么?因为 JavaScript 执行时,页面还没加载完,那个按钮根本不存在!
✅ 正确写法:
// 写法1:把 script 放到 body 最后
// </body> 前面写 <script>...</script>
// 写法2:用 DOMContentLoaded 事件
document.addEventListener('DOMContentLoaded', function() {
let btn = document.querySelector('.btn')
btn.addEventListener('click', handleClick)
})
坑2:querySelector 和 querySelectorAll 的返回值不一样
❌ 错误理解:
let btn = document.querySelector('.btn')
// 以为 btn 是数组,可以用 btn[0]
btn[0].addEventListener('click', handleClick) // 报错!
✅ 正确理解:
// querySelector 返回第一个匹配元素(单个),直接用
let singleBtn = document.querySelector('.btn')
singleBtn.addEventListener('click', handleClick) // ✅
// querySelectorAll 返回所有匹配的「NodeList(像数组)」
let allBtns = document.querySelectorAll('.btn')
allBtns.forEach(function(btn) { // 用 forEach 遍历
btn.addEventListener('click', handleClick)
})
坑3:修改 innerHTML 会丢失之前的事件监听
❌ 错误做法:
container.innerHTML = '<button>新按钮</button>'
// 之前给旧按钮加的事件监听,全没了!
✅ 正确做法:
// 不要用 innerHTML 整体覆盖,改用 createElement
let newBtn = document.createElement('button')
newBtn.textContent = '新按钮'
newBtn.addEventListener('click', handleClick)
container.appendChild(newBtn) // 追加而不是覆盖
坑4:事件委托的父元素选错了
❌ 错误写法:
// 给 1000 个 li 加点击事件,累死浏览器
document.querySelectorAll('li').forEach(function(li) {
li.addEventListener('click', handleClick)
})
✅ 正确写法(事件委托):
// 把事件监听放在父元素上,让子元素的点击「冒泡」上来
document.querySelector('ul').addEventListener('click', function(event) {
// event.target 是实际被点击的元素
if (event.target.tagName === 'LI') {
handleClick(event.target)
}
})
坑5:在循环里绑定事件,结果闭包变量共享
❌ 错误代码:
for (var i = 0; i < 3; i++) {
document.querySelectorAll('.btn')[i].addEventListener('click', function() {
alert('点击了按钮 ' + i) // 永远弹出 "点击了按钮 3"
})
}
✅ 正确写法:
// 用 let 代替 var,或者用 forEach
document.querySelectorAll('.btn').forEach(function(btn, i) {
btn.addEventListener('click', function() {
alert('点击了按钮 ' + (i + 1)) // 正确!
})
})
调试技巧:console.log 是你的好朋友
let btn = document.querySelector('.btn')
console.log('找到的按钮:', btn) // 看元素到底有没有找到
console.log('按钮的文字是:', btn.textContent)
btn.addEventListener('click', function(event) {
console.log('事件对象:', event) // 看事件的详细信息
console.log('点击的元素:', event.target)
})
打开浏览器控制台(F12),你就能看到这些输出,比猜错哪里出了问题高效 100 倍。
✏️ 练习题
练习 1(1分钟):改改看
- 输入:把项目1里的 emoji 换一下(💡 换成 🕯️,🔦 换成 ✨)
- 预期输出:点击后显示不同的 emoji
- 提示:只改 CSS 里
::after的内容
练习 2(2分钟):加个判断
- 输入:在项目1的 toggleLamp 函数里,加一个判断:只有灯是灭的时候,才播放声音(用
console.log('播放开灯音效')模拟) - 预期输出:灯灭→开时打印「播放开灯音效」,灯开→灭时不做声
- 提示:在 if (isOn) 里面加 console.log
练习 3(3分钟):换个数据源
- 输入:把项目2的待办数据改成你自己的 3 个任务
- 预期输出:页面加载时直接显示你的 3 个任务(不需要手动输入)
- 提示:在页面加载时用 JavaScript 动态创建 3 个 li,而不是等用户输入
练习 4(4分钟):串联两个项目
- 输入:给项目2的每个待办项添加「完成」按钮,点击后划掉这条任务(加个删除线样式)
- 预期输出:点击完成按钮,任务文字有删除线效果
- 提示:用
event.target.classList.add('completed')给元素加 class,配合 CSS.completed { text-decoration: line-through; }
练习 5(5分钟):分析报错
- 输入:以下代码为什么会报错?怎么修?
let title = document.querySelector('#title')
title.addEventListener('click', function() {
title.textContent = '被点击了'
})
title.remove()
- 预期输出:能运行,不报错
- 提示:考虑一下,remove 之后,还能调用 title.addEventListener 吗?
作业:做一个「学生成绩管理页面」
需求描述:班主任想用一个网页方便地查看和管理学生成绩。
功能点:
1. 显示一个学生列表(至少5个学生,每个有姓名、分数)
2. 点击「添加学生」可以新增一条记录
3. 每个学生右边有「删除」按钮,点谁删谁
4. 分数低于 60 分的,显示红色高亮
加分项(选做):
1. 点击学生姓名可以修改名字(输入框变成可编辑)
2. 添加「按分数排序」功能
验收标准:
- 能跑起来(复制到 HTML 文件里直接用)
- 添加和删除功能正常工作
- 低分红色高亮正常显示
- 代码有适当注释
提交方式:把代码贴到评论区,或者 GitHub 链接甩过来!
📚 总结 + 资源
本文学了 3 件事:
1. DOM 是网页的骨架,用 querySelector 能像 GPS 一样精准定位任意元素
2. 事件监听让网页「活」起来,addEventListener('click', fn) 最常用
3. 事件冒泡是双刃剑——善用 stopPropagation() 阻止它,善用委托优化性能
延伸学习资源:
- MDN 官方文档(中文):https://developer.mozilla.org/zh-CN/docs/Web/API/Document/querySelector
- 《JavaScript 高级程序设计》:红宝书,体系完整,值得反复看
- 视频:B 站「渡一教育」JS 教程第 6-8 集,讲得很细
互动钩子:
你在工作中有没有遇到过「明明想点 A,结果 B 触发了」的场景?是因为事件冒泡还是选择器写错了?评论区聊聊,老粉优先回复!
下章预告:
学会了选中元素和绑定事件,但如果我想让一个按钮响应很多种操作(点一下、双击、长按、拖拽……)怎么办?下一章我们来聊聊事件处理与委托,让你的网页交互从「单机版」升级成「多功能键盘」!

评论(0)