DOM 实战入门教程
作者:wcy 适用人群:已经学完基础 HTML、CSS 和 JavaScript,想把页面交互做出来的初学者
目录
1. DOM 是什么
原计划学完 JavaScript 语法后,我们发现它只能"自己算",还碰不到页面。而要让网页真正动起来——点击按钮换文字、输入内容动态展示、删除列表项——JS 必须能操作页面上的元素。
这就是 DOM(Document Object Model,文档对象模型) 的用武之地。
用大白话解释:
浏览器把 HTML 网页"翻译"成一棵元素树,树上的每一个节点(body、p、div、文字……)都是 JS 可以访问和操作的对象。通过 JS 修改这些对象,页面就跟着变了。
简单说,DOM 就是 JS 与 HTML 之间的"桥"。
本教程不做概念堆砌,直接通过一个个能运行的实战例子,带你掌握 DOM 的核心操作。每一节都附完整可运行代码,建议边看边在浏览器里敲一遍。
提示:按 F12 打开开发者工具,在 Console(控制台)里可以直接输入下面例子中的 JS 代码实时测试。
2. DOM 树结构
先看一个最简单的页面:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>树</title>
</head>
<body>
<h1>标题</h1>
<p>一段文字</p>
</body>
</html>
它在浏览器内部会变成这样一棵"树"(父节点在上,子节点在下):
document(文档)
└── html
├── head
│ ├── meta
│ └── title
└── body
├── h1
└── p
关键概念:
| 文档节点 | 整棵树的根,就是 document | 整个页面 |
| 元素节点 | 各种标签 | html、body、h1、p |
| 文本节点 | 标签里的文字 | “标题”、“一段文字” |
| 父节点 | 直接包含某个节点的节点 | body 是 h1 的父节点 |
| 子节点 | 被某个节点直接包含的节点 | h1、p 是 body 的子节点 |
| 兄弟节点 | 同一个父节点下的其他节点 | h1 和 p 互为兄弟 |
记不住的词没关系,实战里用得最多的是:拿到某个元素 → 改它/加它/删它。我们接下来要学的操作,本质都是在树上"找节点、动节点"。
3. 获取元素
要操作一个元素,第一步是"拿到它"。下面是常用的获取方法。
3.1 用 id 获取(最常用)
<p id="msg">原来的文字</p>
<script>
let msg = document.getElementById("msg");
console.log(msg); // 打印出这个元素
</script>
getElementById:按照 id 精确找到一个元素,找不到返回 null。
3.2 用 CSS 选择器获取(最灵活)
querySelector 支持写任何 CSS 选择器,推荐优先使用:
<p class="box">第一个</p>
<p class="box">第二个</p>
<script>
// 取第一个匹配的元素
let first = document.querySelector(".box");
// 取所有匹配的元素,返回一个"类数组"
let all = document.querySelectorAll(".box");
console.log(all.length); // 2
</script>
常见写法:
document.querySelector("#msg"); // 按 id
document.querySelector(".box"); // 按 class
document.querySelector("p"); // 按标签
document.querySelector("div.box"); // div 且 class 为 box
document.querySelector("ul li:first-child"); // 复杂选择器也支持
注意:querySelector 永远只返回第一个匹配的;要操作全部元素,用 querySelectorAll 后再遍历。
3.3 遍历元素集合
<ul id="list">
<li>苹果</li>
<li>香蕉</li>
<li>橙子</li>
</ul>
<script>
let items = document.querySelectorAll("#list li");
items.forEach(function (li, index) {
console.log(index, li.textContent);
});
</script>
3.4 父子兄弟关系
<div id="parent">
<p id="child">文本</p>
</div>
<script>
let child = document.getElementById("child");
child.parentNode; // 父节点 → div
child.nextElementSibling; // 下一个兄弟元素
child.previousElementSibling;// 上一个兄弟元素
</script>
4. 修改内容与属性
拿到元素后,第一件事通常是改它显示的内容。
4.1 textContent:改纯文本
<p id="msg">旧内容</p>
<button id="btn">点我换文字</button>
<script>
document.getElementById("btn").addEventListener("click", function () {
document.getElementById("msg").textContent = "新内容!";
});
</script>
textContent 只能放纯文本,就算给它 HTML 字符串,也会原样显示成文字(安全)。
4.2 innerHTML:改 HTML 内容
innerHTML 会把字符串当作 HTML 来解析:
<div id="box"></div>
<script>
let box = document.getElementById("box");
box.innerHTML = "<b>加粗文字</b><span style='color:red'>和红色文字</span>";
</script>
安全提醒:如果内容来自用户输入(比如表单),不要用 innerHTML,它可能被塞入恶意脚本。用户输入一律用 textContent。
4.3 修改属性
<a id="link" href="#">我的博客</a>
<img id="pic" src="a.jpg" alt="图片">
<script>
let link = document.getElementById("link");
link.href = "https://blog.csdn.net/wangchunyu114";
link.textContent = "去博客逛逛";
let pic = document.getElementById("pic");
pic.src = "b.jpg"; // 直接改属性
// 通用写法:
pic.setAttribute("alt", "新的描述");
pic.getAttribute("alt"); // 读取属性值
pic.hasAttribute("src"); // 判断是否有该属性 → true
pic.removeAttribute("src"); // 删除属性
</script>
4.4 实战:点击切换标题文字
<h1 id="title">今天吃什么?</h1>
<button id="switchBtn">换一个</button>
<script>
const foods = ["火锅", "烧烤", "炒饭", "面条", "汉堡"];
let index = 0;
document.getElementById("switchBtn").addEventListener("click", function () {
index = (index + 1) % foods.length; // 循环取下一个
document.getElementById("title").textContent = foods[index];
});
</script>
保存成 switch.html 打开,每点一次按钮标题就换一个菜名。
5. 修改样式
5.1 用 style 改行内样式
<p id="text">这段文字会很调皮</p>
<button id="styleBtn">变红加粗</button>
<script>
document.getElementById("styleBtn").addEventListener("click", function () {
let t = document.getElementById("text");
t.style.color = "red";
t.style.fontSize = "24px";
t.style.fontWeight = "bold";
});
</script>
注意:CSS 属性名里的短横线,在 JS 里要换成驼峰写法:
| font-size | fontSize |
| background-color | backgroundColor |
| margin-top | marginTop |
5.2 用 classList 切换样式类(更推荐)
与其在 JS 里写一堆样式,不如定义好 CSS 类,再用 JS 切换类名。这样做"样式归 CSS、逻辑归 JS",更干净:
.dark {
background-color: #222222;
color: white;
}
<p id="text">在明暗之间切换</p>
<button id="toggleBtn">切换主题</button>
<script>
let t = document.getElementById("text");
document.getElementById("toggleBtn").addEventListener("click", function () {
t.classList.toggle("dark"); // 有则去掉,无则加上
});
</script>
classList 的四个常用方法:
| add("类名") | 添加类 |
| remove("类名") | 删除类 |
| toggle("类名") | 有则删、无则加(最常用) |
| contains("类名") | 判断有没有,返回布尔值 |
5.3 实战:暗黑模式开关
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>暗黑模式</title>
<style>
body {
transition: background-color 0.3s, color 0.3s;
}
body.dark {
background-color: #1e1e1e;
color: #eeeeee;
}
</style>
</head>
<body>
<button id="themeBtn">切换到暗黑模式</button>
<script>
let btn = document.getElementById("themeBtn");
btn.addEventListener("click", function () {
document.body.classList.toggle("dark");
btn.textContent = document.body.classList.contains("dark")
? "切换到亮色模式"
: "切换到暗黑模式";
});
</script>
</body>
</html>
5.4 用 style.cssText 一次写多个样式
let box = document.getElementById("box");
box.style.cssText = "width: 100px; height: 100px; background: red;";
适合一次性重置大量样式,日常用得不多,了解即可。
6. 创建与删除元素
动态增删元素,是做"列表、消息流、购物车"等功能的基础。
6.1 创建并插入元素
<ul id="list">
<li>已有一项</li>
</ul>
<button id="addBtn">添加一项</button>
<script>
document.getElementById("addBtn").addEventListener("click", function () {
let li = document.createElement("li"); // 1. 创建
li.textContent = "新项目"; // 2. 设置内容
document.getElementById("list").appendChild(li); // 3. 插到末尾
});
</script>
标准三步:创建 → 设置内容 → 插入页面。
6.2 插入位置:appendChild / insertBefore
let parent = document.getElementById("list");
parent.appendChild(li); // 放到最后
parent.insertBefore(li, parent.firstChild); // 放到最前面
6.3 删除元素
li.remove(); // 直接删除自己(现代写法,推荐)
parent.removeChild(li); // 通过父节点删除(旧写法)
6.4 清空内容
document.getElementById("list").innerHTML = ""; // 清空所有子元素
6.5 实战:动态添加和删除
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>动态列表</title>
</head>
<body>
<input type="text" id="itemInput" placeholder="输入内容">
<button id="addBtn">添加</button>
<ul id="list"></ul>
<script>
let list = document.getElementById("list");
let input = document.getElementById("itemInput");
document.getElementById("addBtn").addEventListener("click", addItem);
input.addEventListener("keydown", function (e) {
if (e.key === "Enter") { // 按回车也添加
addItem();
}
});
function addItem() {
let text = input.value.trim();
if (!text) return; // 空内容不添加
let li = document.createElement("li");
li.textContent = text;
let del = document.createElement("button");
del.textContent = "删除";
del.addEventListener("click", function () {
li.remove(); // 点击删除自己
});
li.appendChild(del);
list.appendChild(li);
input.value = "";
}
</script>
</body>
</html>
7. 事件详解
前面几节的例子都在用 addEventListener,这一节把事件讲透。
7.1 常用事件一览
| click | 鼠标单击 |
| dblclick | 鼠标双击 |
| mouseover / mouseout | 鼠标移入 / 移出 |
| mouseenter / mouseleave | 鼠标进入 / 离开(不冒泡) |
| keydown / keyup | 键盘按下 / 松开 |
| input | 输入框内容变化(即时触发) |
| change | 输入框/下拉框失去焦点且值变了 |
| submit | 表单提交 |
| focus / blur | 获得焦点 / 失去焦点 |
7.2 event 事件对象
事件发生时,浏览器会传入一个事件对象 e,里面装着这次事件的细节:
<input type="text" id="myInput" placeholder="打字试试">
<script>
let input = document.getElementById("myInput");
input.addEventListener("keydown", function (e) {
console.log("按下的键:", e.key); // 比如 "a"、"Enter"
console.log("是否按下 Ctrl:", e.ctrlKey);
});
input.addEventListener("input", function (e) {
console.log("当前输入值:", e.target.value); // 实时内容
});
</script>
常用属性:
| e.target | 实际触发事件的元素 |
| e.key | 按下的按键(键盘事件) |
| e.ctrlKey / e.shiftKey / e.altKey | 是否同时按了修饰键 |
| e.clientX / e.clientY | 鼠标距浏览器窗口的坐标 |
7.3 阻止默认行为 preventDefault
有些标签自带行为:点击 <a> 会跳转、提交表单会刷新页面。不想让它发生时用 preventDefault():
<a id="aLink" href="https://www.baidu.com">不许跳转</a>
<script>
document.getElementById("aLink").addEventListener("click", function (e) {
e.preventDefault(); // 阻止跳转
alert("链接被拦住了");
});
</script>
7.4 事件传播与 stopPropagation
一个元素嵌套另一个元素时,点击内层事件会冒泡到外层(从内向外依次触发)。想阻止向上传播,用 stopPropagation():
<div id="outer" style="padding: 20px; background: lightblue;">
外层 div
<button id="inner">内层按钮</button>
</div>
<script>
document.getElementById("outer").addEventListener("click", function () {
console.log("外层被触发");
});
document.getElementById("inner").addEventListener("click", function (e) {
e.stopPropagation(); // 阻止冒泡到外层
console.log("内层被触发");
});
</script>
注释掉 e.stopPropagation() 再点按钮,观察两层都会打印——这就是"冒泡"。
7.5 事件委托(重要技巧)
如果有很多个子元素都要绑定点击,一个个绑效率低、还容易漏掉动态新增的。事件委托的思路是:把监听绑在父元素上,用 e.target 判断点的是谁:
<ul id="list">
<li>项目一</li>
<li>项目二</li>
<li>项目三</li>
</ul>
<script>
document.getElementById("list").addEventListener("click", function (e) {
if (e.target.tagName === "LI") { // 判断点的是不是 li
alert("你点了:" + e.target.textContent);
}
});
</script>
好处:无论多少个 li、哪怕之后动态新增 li,都不用重新绑定。
7.6 实战:图片悬停放大
<style>
.gallery { display: flex; gap: 10px; }
.gallery img {
width: 100px;
transition: transform 0.3s;
cursor: pointer;
}
.gallery img.zoom { transform: scale(1.3); }
</style>
<div class="gallery">
<img src="cat1.jpg" alt="猫1">
<img src="cat2.jpg" alt="猫2">
<img src="cat3.jpg" alt="猫3">
</div>
<script>
let imgs = document.querySelectorAll(".gallery img");
imgs.forEach(function (img) {
img.addEventListener("mouseenter", function () {
img.classList.add("zoom");
});
img.addEventListener("mouseleave", function () {
img.classList.remove("zoom");
});
});
</script>
8. 表单实战
表单是网页交互的核心场景。下面通过一个"注册表单校验"把表单相关的 DOM 操作串起来。
8.1 读取表单输入
let input = document.getElementById("username");
input.value; // 读取输入值
input.value = "预设值"; // 设置值
8.2 实战:注册表单校验
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>表单校验</title>
</head>
<body>
<form id="regForm">
<p>
用户名
<input type="text" id="username" placeholder="至少 3 个字符">
<span id="userTip" style="color: red;"></span>
</p>
<p>
邮箱
<input type="text" id="email" placeholder="xxx@example.com">
<span id="emailTip" style="color: red;"></span>
</p>
<button type="submit">注册</button>
</form>
<script>
let form = document.getElementById("regForm");
// 阻止表单默认的页面刷新提交行为,改用我们自己校验
form.addEventListener("submit", function (e) {
e.preventDefault();
let username = document.getElementById("username").value.trim();
let email = document.getElementById("email").value.trim();
let userTip = document.getElementById("userTip");
let emailTip = document.getElementById("emailTip");
let ok = true;
if (username.length < 3) {
userTip.textContent = "用户名太短了";
ok = false;
} else {
userTip.textContent = "";
}
let hasAt = email.includes("@") && email.split("@").length === 2;
let emailValid = hasAt && email.split("@")[1].includes(".");
if (!emailValid) {
emailTip.textContent = "邮箱格式不对";
ok = false;
} else {
emailTip.textContent = "";
}
if (ok) {
alert("注册成功!用户名:" + username);
}
});
</script>
</body>
</html>
8.3 实战:两个下拉框联动
省份选择城市:
<select id="province">
<option value="zj">浙江</option>
<option value="gd">广东</option>
<option value="sc">四川</option>
</select>
<select id="city">
<option>请先选择省份</option>
</select>
<script>
// 省份对应的城市
let cityMap = {
zj: ["杭州", "宁波", "温州"],
gd: ["广州", "深圳", "珠海"],
sc: ["成都", "绵阳", "乐山"]
};
let province = document.getElementById("province");
let city = document.getElementById("city");
province.addEventListener("change", function () {
let cities = cityMap[province.value] || [];
// 清空并重新填充城市下拉框
city.innerHTML = "";
cities.forEach(function (c) {
let option = document.createElement("option");
option.value = c;
option.textContent = c;
city.appendChild(option);
});
});
</script>
9. 定时器实战
定时器配合 DOM,能做出倒计时、打字机、轮播、秒表等常见效果。
9.1 setTimeout:延时执行一次
setTimeout(function () {
console.log("3 秒后执行一次");
}, 3000);
9.2 setInterval:每隔一段时间重复执行
let timer = setInterval(function () {
console.log("每 1 秒一次");
}, 1000);
// 停止:
clearInterval(timer);
9.3 实战:秒表
<p id="watch">00:00</p>
<button id="startBtn">开始</button>
<button id="stopBtn">暂停</button>
<button id="resetBtn">重置</button>
<script>
let seconds = 0;
let timer = null;
function render() {
let min = Math.floor(seconds / 60);
let sec = seconds % 60;
let pad = function (n) { return n < 10 ? "0" + n : n; };
document.getElementById("watch").textContent = pad(min) + ":" + pad(sec);
}
document.getElementById("startBtn").addEventListener("click", function () {
if (timer) return; // 已在计时就不重复开
timer = setInterval(function () {
seconds++;
render();
}, 1000);
});
document.getElementById("stopBtn").addEventListener("click", function () {
clearInterval(timer);
timer = null;
});
document.getElementById("resetBtn").addEventListener("click", function () {
clearInterval(timer);
timer = null;
seconds = 0;
render();
});
</script>
9.4 实战:自动轮播(定时器版)
<style>
#banner { width: 300px; height: 150px; text-align: center; font-size: 40px; line-height: 150px; color: white; }
</style>
<div id="banner"></div>
<script>
let colors = ["#e74c3c", "#3498db", "#2ecc71", "#f39c12"];
let index = 0;
let banner = document.getElementById("banner");
banner.textContent = "第 1 张";
banner.style.backgroundColor = colors[0];
setInterval(function () {
index = (index + 1) % colors.length;
banner.textContent = "第 " + (index + 1) + " 张";
banner.style.backgroundColor = colors[index];
}, 1500);
</script>
10. 实战一:选项卡切换
最经典的 DOM 实战:点击不同选项卡,显示对应内容。综合运用"遍历、classList 切换、循环绑定"。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>选项卡</title>
<style>
.tabs { display: flex; gap: 0; border-bottom: 2px solid #eee; }
.tab {
padding: 10px 20px;
cursor: pointer;
border: 1px solid #eee;
border-bottom: none;
background: #fafafa;
}
.tab.active { background: #ffffff; color: #1a73e8; font-weight: bold; border-top: 2px solid #1a73e8; }
.panel { display: none; padding: 20px; border: 1px solid #eee; border-top: none; }
.panel.active { display: block; }
</style>
</head>
<body>
<div class="tabs">
<div class="tab active" data-index="0">公告</div>
<div class="tab" data-index="1">动态</div>
<div class="tab" data-index="2">设置</div>
</div>
<div class="panel active">这里是公告内容。</div>
<div class="panel">这里是动态内容。</div>
<div class="panel">这里是设置内容。</div>
<script>
let tabs = document.querySelectorAll(".tab");
let panels = document.querySelectorAll(".panel");
tabs.forEach(function (tab) {
tab.addEventListener("click", function () {
// 1. 所有 tab 去掉 active
tabs.forEach(function (t) { t.classList.remove("active"); });
// 2. 当前 tab 加 active
tab.classList.add("active");
// 3. 所有 panel 隐藏
panels.forEach(function (p) { p.classList.remove("active"); });
// 4. 对应的 panel 显示
panels[tab.dataset.index].classList.add("active");
});
});
</script>
</body>
</html>
技巧:data-index 是自定义属性,JS 里通过 元素.dataset.index 读取,用来记录"这是第几块面板"。
11. 实战二:发消息系统
综合运用"创建元素、插入列表、事件委托、清空输入"。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>消息系统</title>
<style>
body { font-family: "微软雅黑"; }
#chat {
width: 360px;
height: 260px;
border: 1px solid #ddd;
overflow-y: auto;
padding: 10px;
background: #f9f9f9;
}
.msg {
padding: 6px 10px;
margin: 6px 0;
width: fit-content;
max-width: 80%;
border-radius: 8px;
}
.mine { background: #1a73e8; color: white; margin-left: auto; }
.other { background: #eeeeee; }
</style>
</head>
<body>
<h2>客服对话</h2>
<div id="chat"></div>
<input type="text" id="input" placeholder="输入消息,回车发送">
<button id="sendBtn">发送</button>
<script>
let chat = document.getElementById("chat");
let input = document.getElementById("input");
function addMsg(text, type) {
let div = document.createElement("div");
div.className = "msg " + type; // mine / other
div.textContent = text;
chat.appendChild(div);
chat.scrollTop = chat.scrollHeight; // 自动滚到底部
}
function send() {
let text = input.value.trim();
if (!text) return;
addMsg(text, "mine");
input.value = "";
// 模拟对方回复
setTimeout(function () {
addMsg("已收到:" + text, "other");
}, 800);
}
document.getElementById("sendBtn").addEventListener("click", send);
input.addEventListener("keydown", function (e) {
if (e.key === "Enter") send();
});
</script>
</body>
</html>
chat.scrollTop = chat.scrollHeight 是一个实用技巧:让可滚动的消息区自动滚到最新一条。
12. 实战三:待办清单(升级版)
最后一个综合实战,把之前的操作全面串起来,并加入 localStorage 持久化:刷新页面数据不丢。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>待办清单</title>
<style>
body { font-family: "微软雅黑"; max-width: 420px; margin: 20px auto; }
.done { text-decoration: line-through; color: #999; }
li { user-select: none; }
li span { cursor: pointer; }
</style>
</head>
<body>
<h1>待办清单</h1>
<input type="text" id="taskInput" placeholder="输入新任务">
<button id="addBtn">添加</button>
<ul id="list"></ul>
<script>
let list = document.getElementById("list");
let input = document.getElementById("taskInput");
let addBtn = document.getElementById("addBtn");
// 1. 数据模型:从 localStorage 读取(首次为空数组)
let tasks = JSON.parse(localStorage.getItem("tasks") || "[]");
// 2. 保存到本地
function save() {
localStorage.setItem("tasks", JSON.stringify(tasks));
}
// 3. 渲染整个列表
function render() {
list.innerHTML = "";
tasks.forEach(function (task, index) {
let li = document.createElement("li");
li.textContent = task.text;
if (task.done) li.classList.add("done");
// 点击文字:切换完成状态
li.addEventListener("click", function () {
tasks[index].done = !tasks[index].done;
save();
render();
});
// 删除按钮
let del = document.createElement("button");
del.textContent = "删除";
del.style.marginLeft = "10px";
del.addEventListener("click", function (e) {
e.stopPropagation(); // 别触发 li 的点击
tasks.splice(index, 1);
save();
render();
});
li.appendChild(del);
list.appendChild(li);
});
}
// 4. 添加任务
function add() {
let text = input.value.trim();
if (!text) return;
tasks.push({ text: text, done: false });
save();
input.value = "";
render();
}
addBtn.addEventListener("click", add);
input.addEventListener("keydown", function (e) {
if (e.key === "Enter") add();
});
// 5. 首次渲染
render();
</script>
</body>
</html>
这个例子的核心架构(也是小型前端应用的标准套路):
数据(tasks 数组) → 渲染(render 函数) → 事件(增/删/改触发 save + render)
改数据 → 保存 → 重新渲染,页面永远和数据保持一致。
13. 小结与练习
本章总结
| 获取元素 | getElementById、querySelector、querySelectorAll |
| 修改内容 | textContent(纯文本)、innerHTML(HTML,慎用) |
| 修改属性 | href、src、setAttribute、getAttribute |
| 修改样式 | style.xxx(驼峰)、classList.add/remove/toggle/contains |
| 增删元素 | createElement、appendChild、insertBefore、remove |
| 事件 | addEventListener、preventDefault、stopPropagation、事件委托 |
| 表单 | value、submit、change、input、下拉框联动 |
| 定时器 | setTimeout、setInterval、clearInterval |
| 持久化 | localStorage + JSON.stringify/JSON.parse |
| 开发套路 | 数据模型 → 渲染函数 → 事件绑定(改数据 → 保存 → 重新渲染) |
课后练习
常见问题
问:querySelectorAll 返回的是什么?能用 querySelectorAll(…).style 吗? 答:它返回一个节点集合(类数组),不是单个元素,不能直接 .style。要遍历后逐个操作,比如 el.classList 或 document.querySelectorAll("p")[0]。
问:动态创建的元素为什么点击没反应? 答:如果你在创建后用 addEventListener 绑定了应该没问题。如果事件绑在了页面加载时还不存在的元素上,就需要改用事件委托(绑到父元素上)。
问:innerHTML 和 textContent 到底用哪个? 答:插入自己写的、确定安全的 HTML 结构用 innerHTML;插入用户输入的内容一律用 textContent,避免脚本注入风险。
问:刷新后数据就没了? 答:数据只存在内存里。要跨页面刷新保留,需要 localStorage 持久化(第 12 节的做法)。
DOM 是前端开发的"地基",练好它,从"会写页面"到"会做应用"就只差临门一脚。继续学下去,你会发现 React、Vue 这些框架,本质都是在帮你更高效地操作 DOM。
如果觉得教程有帮助,欢迎关注我的博客

