适合JavaScript初学者的实战项目:制作简易待办事项列表
专为JavaScript初学者设计的简易待办事项列表开发教程,通过原生HTML、CSS与JavaScript,从DOM获取、动态节点创建到事件委托完整讲解新增、标记完成与删除任务的实现流程。
一、为什么待办事项列表是初学者的最佳实战项目
许多初学者在学完 JavaScript 的变量、数据类型、条件分支和函数后,常常面临“看得懂语法但写不出具体页面功能”的瓶颈。这种卡顿的核心原因,在于尚未在头脑中建立起“用户操作 → 浏览器事件 → JavaScript 处理逻辑 → 页面 DOM 结构变更”的数据与视图对应模型。
简易待办事项列表恰好完整涵盖了 Web 前端开发中最基础、高频的三大操作场景:
- 读写交互数据:从输入框
<input>中捕获用户键入的字符串,执行非空过滤,并在提交后自动复位。 - 动态装配节点:在不刷新页面的前提下,通过脚本动态创建
<li>、文本容器与操作按钮,将它们作为子节点插入到页面文档树中。 - 双向状态维护:即时响应用户的后续操作,既能通过切换 CSS 类名修改任务外观(完成状态),也能直接将对应节点从 DOM 树中彻底移除。
二、项目结构准备与静态页面搭建
在开始编写 JavaScript 逻辑之前,必须先搭建好清晰规范的 HTML 语义骨架与基础样式。本项目无需任何打包构建工具或外部第三方库,仅需一个标准的 HTML 文档即可直接在任意浏览器中运行。
1. HTML 语义化骨架设计
打开本地代码编辑器,创建一个名为 index.html 的文件。在页面主体中,我们需要三个核心交互容器:用于键入任务的输入框、触发新增的按钮,以及用于挂载任务子节点的无序列表容器:
<!-- 待办事项应用容器 -->
<div class="todo-app">
<h2>我的待办清单</h2>
<!-- 输入交互区 -->
<div class="input-section">
<input type="text" id="task-input" placeholder="输入新的待办事项..." autocomplete="off">
<button id="add-btn">添加任务</button>
</div>
<!-- 任务列表承载容器(初始为空) -->
<ul id="task-list"></ul>
</div>
2. 核心交互样式与状态类预设
为了让界面层次清晰并为 JavaScript 提供可操作的视觉钩子,我们需要通过 CSS 预设两种状态:任务项的基础布局样式,以及代表任务完成的 .completed 样式。特别注意的是,完成状态的视觉效果(删除线与字体颜色淡化)应当完全交由 CSS 类名管理,而不是由 JavaScript 直接修改内联属性,这是保持样式与行为解耦的规范做法。
/* 任务项基础排版 */
#task-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 14px;
background-color: #FAF7F1;
border: 1px solid #DDD5C7;
border-radius: 6px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
/* 任务已完成状态:划线置灰(由 JavaScript 动态赋予) */
#task-list li.completed .task-text {
text-decoration: line-through;
color: #8C9BA5;
}
#task-list li.completed {
background-color: #F2EFE9;
border-color: #E2DBD0;
}
/* 删除按钮样式 */
.delete-btn {
background-color: transparent;
color: #A34845;
border: 1px solid #DDD5C7;
padding: 4px 10px;
border-radius: 4px;
font-size: 13px;
cursor: pointer;
}
.delete-btn:hover {
background-color: #A34845;
color: #FFFFFF;
}
三、核心逻辑实现:四步落地法
在理解了页面骨架与交互流向后,我们将 JavaScript 脚本拆解为清晰的四个步骤,按实际开发顺序逐一构建。
步骤一:获取 DOM 元素引用
在操作任何页面元素前,JavaScript 必须通过选择器抓取对应的 DOM 节点对象。我们在脚本开头通过 document.getElementById() 集中获取三个关键元素:
// 获取输入框、添加按钮与任务列表容器的 DOM 引用
const taskInput = document.getElementById('task-input');
const addBtn = document.getElementById('add-btn');
const taskList = document.getElementById('task-list');
<script> 标签写在 HTML 的 <head> 区域且未加 defer 属性,浏览器执行脚本时页面元素尚未生成,会导致获取到的变量为 null。初学者建议始终将脚本置于 </body> 标签正上方,确保文档节点解析完毕后再执行代码。
步骤二:编写添加任务函数与防空输入校验
添加任务的核心逻辑包含读取文本、验证内容有效性、重置输入框。使用字符串原生的 trim() 方法去除两端多余空格,能有效杜绝用户仅输入连续空格产生的“隐形无意义任务”:
function addTask() {
// 1. 读取输入值并修剪两端空格
const text = taskInput.value.trim();
// 2. 防空校验:若内容为空,直接中断函数执行
if (text === '') {
taskInput.focus();
return;
}
// 3. 动态创建 li 元素
const li = document.createElement('li');
// 4. 创建承载文本的 span 标签
const span = document.createElement('span');
span.className = 'task-text';
span.textContent = text; // 使用 textContent 确保安全性
// 5. 创建操作按钮:删除
const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-btn';
deleteBtn.textContent = '删除';
// 6. 拼装子节点并挂载至列表容器
li.appendChild(span);
li.appendChild(deleteBtn);
taskList.appendChild(li);
// 7. 清空输入框并将焦点重置回输入框,方便连续输入
taskInput.value = '';
taskInput.focus();
}
<li>
<span class="task-text">复习事件监听</span>
<button class="delete-btn">删除</button>
</li>
</ul>
步骤三:绑定用户触发事件(点击与回车响应)
用户添加任务有两种常见途径:鼠标点击“添加任务”按钮,或在键盘输入完成后直接敲击“Enter”回车键。我们分别为按钮绑定 'click' 事件,为输入框绑定 'keydown' 事件:
// 点击添加按钮触发新增
addBtn.addEventListener('click', addTask);
// 监听键盘按键:按下回车键(Enter)时直接提交
taskInput.addEventListener('keydown', function(event) {
if (event.key === 'Enter') {
addTask();
}
});
步骤四:使用事件委托实现“标记完成”与“删除”
新生成的任务项需要具备两种交互能力:点击任务文字时切换完成状态(划线),点击删除按钮时将任务从界面中彻底剔除。初学者最容易犯的错误是直接为每个新生成的按钮单独绑定监听,这在动态元素增加时非常脆弱。这里我们使用更为优雅高效的事件委托(Event Delegation)机制。
通过在共同的父级容器 taskList 上监听单一的点击事件,利用事件冒泡捕获底层触发源:
// 在父级 ul 容器上绑定统一的事件委托
taskList.addEventListener('click', function(event) {
const target = event.target;
// 分支一:如果点击的是删除按钮
if (target.classList.contains('delete-btn')) {
// 获取当前按钮所在的父级 li 节点并将其从 DOM 中移除
const item = target.closest('li');
if (item) {
item.remove();
}
return;
}
// 分支二:如果点击的是任务列表项或任务文本
const item = target.closest('li');
if (item && taskList.contains(item)) {
// 切换 completed 类名(有则移除,无则添加)
item.classList.toggle('completed');
}
});
四、关键细节剖析:为什么必须掌握事件委托
在很多初学者的早期代码中,常常会在 addTask 函数内部每次新创建按钮后,写一行 deleteBtn.addEventListener('click', ...)。虽然这在功能上暂时可行,但在工程实践中存在明显的弊端:
| 对比维度 | 单独绑定监听器(反模式) | 父级事件委托(推荐最佳实践) |
|---|---|---|
| 监听器数量 | 随着任务数线性增长(N 个任务需产生 2N 个监听器函数) | 无论生成多少任务,全局仅需 1 个统一监听器 |
| 动态元素感知 | 后续动态插入的节点若遗漏绑定就会彻底失效 | 动态新增节点天然受父级管辖,无需任何额外绑定操作 |
| 内存占用与清理 | 节点频繁销毁时若未显式解绑,存在潜在内存泄漏隐患 | 事件统一绑定在稳定容器上,子节点随用随移,GC 无压力 |
| 代码维护性 | 创建节点与绑定逻辑交织,代码冗长且不易维护 | 节点创建逻辑只管组装,交互逻辑统归事件代理,职责清晰 |
span.textContent = text; 而非 span.innerHTML = text;。因为用户输入的内容可能包含尖括号等 HTML 特殊标记。使用 textContent 会由浏览器底层将其视作纯文本转义存储,能够从根本上杜绝潜在的跨站脚本注入(XSS)隐患。
五、完整单文件代码与操作验证
以下是经过整合精简的完整可运行代码。读者可将其复制并保存为本地的 todo.html,双击直接在 Chrome、Edge 或 Safari 等浏览器中打开体验:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>简易待办事项列表</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
background-color: #FAF7F1;
display: flex;
justify-content: center;
padding: 40px 16px;
}
.todo-card {
background: #FFFFFF;
width: 100%;
max-width: 480px;
padding: 24px;
border-radius: 10px;
border: 1px solid #DDD5C7;
box-shadow: 0 4px 12px rgba(57, 73, 87, 0.06);
}
h1 {
font-size: 20px;
color: #394957;
margin-bottom: 16px;
text-align: center;
}
.input-box {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
#task-input {
flex: 1;
padding: 10px 12px;
border: 1px solid #DDD5C7;
border-radius: 6px;
font-size: 14px;
outline: none;
}
#task-input:focus {
border-color: #AD8B58;
}
#add-btn {
background-color: #AD8B58;
color: #FAF7F1;
border: none;
padding: 0 16px;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
}
#task-list {
list-style: none;
padding: 0;
margin: 0;
}
#task-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
background: #FAF7F1;
border: 1px solid #E5E0D6;
border-radius: 6px;
margin-bottom: 8px;
cursor: pointer;
}
#task-list li.completed span {
text-decoration: line-through;
color: #8C9BA5;
}
.del-btn {
background: transparent;
color: #A34845;
border: 1px solid #E5E0D6;
border-radius: 4px;
padding: 2px 8px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="todo-card">
<h1>我的待办清单</h1>
<div class="input-box">
<input type="text" id="task-input" placeholder="输入待办事项..." autocomplete="off">
<button id="add-btn">添加</button>
</div>
<ul id="task-list"></ul>
</div>
<script>
const taskInput = document.getElementById('task-input');
const addBtn = document.getElementById('add-btn');
const taskList = document.getElementById('task-list');
function addTask() {
const text = taskInput.value.trim();
if (!text) return;
const li = document.createElement('li');
const span = document.createElement('span');
span.textContent = text;
const delBtn = document.createElement('button');
delBtn.className = 'del-btn';
delBtn.textContent = '删除';
li.appendChild(span);
li.appendChild(delBtn);
taskList.appendChild(li);
taskInput.value = '';
taskInput.focus();
}
addBtn.addEventListener('click', addTask);
taskInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') addTask();
});
// 事件委托统一管理完成划线与删除
taskList.addEventListener('click', (e) => {
if (e.target.classList.contains('del-btn')) {
e.target.closest('li').remove();
return;
}
const li = e.target.closest('li');
if (li && taskList.contains(li)) {
li.classList.toggle('completed');
}
});
</script>
</body>
</html>
结果验证操作清单
完成编写后,请按照以下四个具体步骤验证应用是否运行正常:
- 空值拦截测试:不输入任何文字,直接点击“添加”按钮或按回车。预期结果:列表中没有任何新元素生成,页面保持整洁。
- 回车提交测试:在输入框键入“阅读 JavaScript 文档”并按下键盘 Enter 键。预期结果:列表底部平滑出现该项,输入框文字自动清空且输入光标未丢失。
- 状态切换测试:单击刚添加的任务文字。预期结果:任务文字立即出现中划线且颜色变淡;再次单击,中划线消失,恢复未完成状态。
- 动态删除测试:点击该任务右侧的“删除”按钮。预期结果:该任务所在整行节点立即从页面中移除,不影响其他任务项。
六、初学者常见问题与排查思路
在实际编码过程中,遇到功能异常是正常现象。以下列出本项目中最集中的两项调试经验:
- 排查一:按下回车键无响应或导致页面刷新
若你的输入框被包裹在<form>表单标签内,按下回车会触发浏览器的默认表单提交动作,刷新整个页面导致数据重置。解决方法是在回车事件处理中加入event.preventDefault(),阻止浏览器的原生表单跳转行为。 - 排查二:点击删除按钮却同时触发了完成划线
这是事件冒泡在未正确分流时的典型表现。因为删除按钮本身是<li>的子元素,点击删除按钮时,事件会先后经过按钮和列表项。解决方法是在判断为删除按钮的分支内执行移除操作后,立即执行return;退出函数,避免继续向下执行切换划线类的逻辑。
七、实战复盘与后续进阶路径
完成这个简易待办事项列表,意味着你已经真正掌握了原生 JavaScript 与浏览器 DOM 的核心互动方式。你不仅掌握了静态页面向动态应用的转变过程,也理解了状态驱动视觉更新的现代开发模式。
在熟练运行当前代码后,推荐读者沿着以下三个阶梯进行自主扩展练习:
- 引入 LocalStorage 解决数据持久化:当前应用在浏览器刷新后数据会重置。可通过
localStorage.setItem('todos', JSON.stringify(...))将数据保存在用户浏览器本地,页面加载时读取还原,让待办事项真正具备实用工具价值。 - 升级为数据驱动模式(Data-Driven UI):尝试不再直接拼接 DOM 节点,而是维护一个 JavaScript 内存数组
const todos = [{ id: 1, text: '学习JS', completed: false }]。任何交互均只修改数组,随后调用统一定义的render()函数根据数组重绘列表。这一步是平滑过渡到 React 或 Vue 等现代前端框架的最核心心智模型。 - 增加筛选分类功能:在列表顶部或底部增加【全部】、【进行中】、【已完成】三个过滤切换按钮,提升对数组过滤方法(如
filter())与条件渲染的运用能力。
vtk.js 渲染窗口黑屏或无内容显示的常见原因及排查
2026-11-05 17:30:30
js-cookie 安装与基础用法:set、get、remove 详解
2026-11-05 16:35:06
JS打开新标签页时怎么通过URL传递参数
2026-11-05 12:21:08
sigma.js 自定义节点样式:设置颜色、大小及标签显示的技巧
2026-11-05 09:55:00
JS 生成的统计图怎么导出为 PNG 图片或 PDF 文件?
2026-11-05 09:37:58








