大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
综合练习与下一步
这一课你将能做到
- 独立做一页「学习清单」小工具(增、删、切换完成状态)
- 对照清单自检:变量、函数、数组、DOM、基础错误处理都用到了
- 选准下一门课(TypeScript 或 React),并写下自己的薄弱点复习计划
为什么要学
前面每课都是零件:变量、函数、数组、对象、DOM、异步、模块、调试。
若不串成一个小作品,知识会停在「好像看过」。
这一课强制你手写。
能独立做完清单工具,再上 React/TS 时,你会知道框架到底替你省了哪一步。
先建立图像
数据(唯一真相)
items = [{ id, title, done }, ...]
↓ 每次变更后
render()
↓
DOM 列表(只是投影)
| 操作 | 改数据 | 然后 |
|---|---|---|
| 添加 | push 新对象 | render |
| 完成 | 找到项,翻 done | render |
| 删除 | filter/splice | render |
别走「只改 DOM、不管数组」的路线——状态会很快不一致。
跟着做
步骤 1:搭页面骨架
新建 study-list.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>学习清单</title>
<style>
body { font-family: sans-serif; max-width: 480px; margin: 24px auto; padding: 0 12px; }
.done { text-decoration: line-through; opacity: 0.6; }
.err { color: #b91c1c; }
li { margin: 8px 0; }
</style>
</head>
<body>
<h1>学习清单</h1>
<input id="title" type="text" placeholder="例如:复习数组 map" />
<button type="button" id="add">添加</button>
<p id="hint" class="err" hidden></p>
<ul id="list"></ul>
<script src="./study-list.js"></script>
</body>
</html>
先用浏览器打开,确认标题与输入框在;控制台暂无报错(脚本文件下一步创建)。
步骤 2:建立数据与 render
新建同目录 study-list.js:
// 数据:唯一真相
const items = []; // { id: number, title: string, done: boolean }
const listEl = document.getElementById("list");
const hintEl = document.getElementById("hint");
const inputEl = document.getElementById("title");
function showHint(msg) {
if (!msg) {
hintEl.hidden = true;
hintEl.textContent = "";
return;
}
hintEl.hidden = false;
hintEl.textContent = msg;
}
function render() {
listEl.replaceChildren();
for (const item of items) {
const li = document.createElement("li");
if (item.done) li.classList.add("done");
const span = document.createElement("span");
span.textContent = item.title;
li.appendChild(span);
const toggle = document.createElement("button");
toggle.type = "button";
toggle.textContent = item.done ? "标为未完成" : "完成";
toggle.dataset.action = "toggle";
toggle.dataset.id = String(item.id);
const del = document.createElement("button");
del.type = "button";
del.textContent = "删除";
del.dataset.action = "delete";
del.dataset.id = String(item.id);
li.append(" ", toggle, " ", del);
listEl.appendChild(li);
}
}
render(); // 初始为空列表也要跑通
刷新:应无红字;列表为空。
步骤 3:实现添加(含空输入校验)
document.getElementById("add").addEventListener("click", function () {
const title = inputEl.value.trim();
if (!title) {
showHint("请输入内容后再添加");
return;
}
showHint("");
items.push({ id: Date.now(), title, done: false });
inputEl.value = "";
render();
});
// 回车也能添加(体验小细节)
inputEl.addEventListener("keydown", function (e) {
if (e.key === "Enter") {
document.getElementById("add").click();
}
});
试:空添加应提示;输入文字添加后列表出现一项。
步骤 4:事件委托实现完成 / 删除
listEl.addEventListener("click", function (e) {
const btn = e.target.closest("button[data-action]");
if (!btn) return;
const id = Number(btn.dataset.id);
const action = btn.dataset.action;
const idx = items.findIndex((it) => it.id === id);
if (idx < 0) return;
if (action === "toggle") {
items[idx].done = !items[idx].done;
} else if (action === "delete") {
items.splice(idx, 1);
}
render();
});
试:点「完成」应出现删除线;再点恢复;点「删除」该项消失。
步骤 5:自检与加固
对照验收清单逐项打勾,并做两处加固:
// 1) 防止极端情况:id 不是数字
function findIndexById(id) {
if (!Number.isFinite(id)) return -1;
return items.findIndex((it) => it.id === id);
}
// 2) 可选:统计文案
function render() {
listEl.replaceChildren();
for (const item of items) {
/* ...同上生成 li... */
}
document.title = `学习清单(${items.length})`;
}
打开控制台,全程操作一轮,确认无明显报错。
步骤 6:扩展选做(至少做一项)
任选其一,写进你的文件:
- 过滤:按钮「仅看未完成」,用
filter派生再渲染 - 持久化:
localStorage.setItem("items", JSON.stringify(items)),启动时JSON.parse读回(包 try/catch) - 异步彩蛋:点「激励」按钮
await一个wait(300)后显示随机一句鼓励
选做不是加戏炫技,而是把本轨道学过的点再压一遍。
讲清楚要点
- 因为 DOM 只是投影,所以所有变更先改
items,再render。 - 因为列表项会增减,所以用事件委托,避免重复绑定。
- 因为空输入没有意义,所以添加前
trim并提示。 - 因为完成状态是数据字段,所以用
done布尔值驱动样式类,而不是只改一次 DOM 样式。 - 因为要可维护,所以拆
render/showHint等小函数,避免一个巨型点击回调。 - 因为下一门课建立在这些习惯上,所以宁可实现朴素,也要状态清晰。
你可能会懵的地方
-
渲染重复了 / 列表越点越多?
每次render必须先清空(replaceChildren或等价方式)。若只append不清空,就会叠一层又一层。 -
事件绑多次?
不要在render里给每个按钮addEventListener还重复调用;委托绑在ul上一次即可。 -
想存到刷新后还在?
用localStorage+ JSON(自行查 MDN);读写都要 try/catch,防止坏数据崩掉整页。 -
Date.now()当 id 会不会冲突?
练习足够;同一毫秒连点极端情况可用自增计数器。重点是「有稳定 id 方便查找」。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 只改 DOM 不改数组 | 刷新/重渲染后状态丢 | 数据优先 |
用 innerHTML 拼用户标题 | XSS 风险 | textContent |
全盘 var / 隐式全局 | 难维护 | const/let |
| render 里重复绑监听 | 一次点击触发多次 | 委托或绑一次 |
| 空输入也 push | 一堆空白项 | trim + 提示 |
自测三问(总复习)
- 为什么清单数据放在数组里,而不是只写在 HTML 里?
- 点击后改的是 DOM 还是先改数据再渲染?你选了哪种?为何?
- 若进入 React,它帮你自动做了哪一件你现在手写的事(提示:UI 与状态同步)?
验收清单
- 使用
const/let,无var - 有函数(至少
render) - 修改文本用
textContent - 有空输入判断与提示
- 控制台无明显报错
- 完成 / 删除 / 添加三条路径都手测过
站内下一步
| 目标 | 课程 |
|---|---|
| 类型更严 | /tracks/typescript-essentials |
| 组件化 UI | /tracks/react-essentials |
| 补网页结构 | /tracks/html-css-essentials |
| 岗位路线 | /learn?role=frontend |
留一句
「JS 里我最容易懵的是 ,我准备这样练:。」
小结一句话
用一个小清单把变量、函数、数组、DOM 与基础校验串起来;能独立做完,再上框架才不虚。
延伸
- 把作业文件夹留好,作为作品集第 0 号
- MDN:JavaScript 指南 查漏补缺
- 可选:为清单加上
localStorage持久化,对照 JSON 课再走一遍 stringify/parse