🧠 为什么要从项目学 JavaScript?
JavaScript 是 Web 的「大脑」。HTML 定义结构,CSS 控制样式,而 JavaScript 让页面「活」起来 — 它可以响应用户操作、动态修改内容、与服务器通信。对于前端初学者来说,最容易犯的错误是花太多时间读语法书而迟迟不动手。To-Do List 是一个完美的入门项目:它足够简单让你快速完成,又包含了真实应用的所有核心环节(数据管理、用户交互、界面更新)。
在本教程中,你将学到:变量声明与数据类型、函数定义与调用、DOM 元素的选择与创建、事件监听与处理、数组和对象的简单使用 — 这些都是构建任何 JavaScript 应用都绕不开的基础知识。如果你想进一步学习CSS和HTML的前置知识,可以先阅读我们的HTML语义化元素指南,打好前端基础。
📦 第一步:HTML 结构
任何 JavaScript 交互都始于 HTML。我们的 To-Do List 需要三个部分:输入区域(文本框 + 添加按钮)、列表容器(放置待办项)、以及每个待办项的模板(复选框 + 文字 + 删除按钮)。
<!-- 输入区域 -->
<div class="todo-input-group">
<input type="text" id="todoInput" placeholder="输入待办事项...">
<button id="addTodoBtn">添加</button>
</div>
<!-- 列表容器 -->
<ul class="todo-list" id="todoList">
<li>
<input type="checkbox" class="todo-check">
<span class="todo-text">学习 HTML 基础</span>
<button class="todo-delete">✕</button>
</li>
</ul>
HTML 结构的关键是给需要操作的元素加上 id 或 class,这样 JavaScript 才能准确找到它们。这里 todoInput 和 addTodoBtn 用 id(唯一),列表项内的元素用 class(复用)。
🎯 第二步:用 JS 选中 DOM 元素
JavaScript 通过 DOM API 与 HTML 交互。「DOM」是 Document Object Model 的缩写,浏览器把 HTML 文档解析成一棵对象树,JS 可以在这棵树上增删改查。最常用的选择方法有两个:
// 通过 ID 选中(返回单个元素)
const todoInput = document.getElementById('todoInput');
const addBtn = document.getElementById('addTodoBtn');
const todoList = document.getElementById('todoList');
// 通过 class 选中(返回类数组 HTMLCollection)
const allChecks = document.querySelectorAll('.todo-check');
// querySelectorAll 更灵活,支持任何 CSS 选择器
getElementById 是性能最快的选择器,适合有 id 的元素。querySelectorAll 支持完整的 CSS 选择器语法,适合复杂查询。在初学阶段,掌握这两个就足够了。
🖱️ 第三步:事件监听 — 让按钮有反应
事件监听(Event Listener)是 JavaScript 交互的核心。当用户点击按钮、按下键盘、或移动鼠标时,浏览器会触发对应的事件。我们用 addEventListener 来「监听」这些事件并执行代码:
// 监听添加按钮的点击事件
addBtn.addEventListener('click', function() {
const text = todoInput.value.trim(); // 获取输入框内容
if (!text) return; // 空内容不处理
// 这里稍后会写添加待办项的代码
todoInput.value = ''; // 清空输入框
todoInput.focus(); // 让输入框保持焦点
});
// 监听回车键(更自然的交互方式)
todoInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
addBtn.click(); // 模拟点击添加按钮
}
});
addEventListener 的第一个参数是事件类型('click'、'keydown' 等),第二个参数是回调函数 — 事件发生时执行的代码。注意 e.key === 'Enter' 检查按键是否为回车,让用户按 Enter 就能添加待办。
🏗️ 第四步:动态创建 DOM 元素
每次用户添加一个新待办,我们需要动态创建一个 <li> 元素并插入到列表中。JS 提供了 document.createElement 来创建元素:
function createTodoItem(text) {
// 创建三个子元素
const li = document.createElement('li');
const check = document.createElement('input');
check.type = 'checkbox';
check.className = 'todo-check';
const span = document.createElement('span');
span.className = 'todo-text';
span.textContent = text; // 设置文字内容
const del = document.createElement('button');
del.className = 'todo-delete';
del.textContent = '✕';
// 为复选框和删除按钮添加事件
check.addEventListener('change', function() {
span.classList.toggle('done', check.checked);
});
del.addEventListener('click', function() {
li.remove(); // 从 DOM 中删除整个 li
});
// 组装:li 包含 check + span + del
li.appendChild(check);
li.appendChild(span);
li.appendChild(del);
return li;
}
注意 classList.toggle('done', check.checked) 的用法 — 第二个参数是布尔值,当 check.checked 为 true 时添加 class,false 时移除。CSS 中 .todo-text.done 设置了删除线样式,视觉上标记任务完成。
🔗 第五步:串联所有功能
现在把添加逻辑和创建逻辑串起来:
function addTodo() {
const text = todoInput.value.trim();
if (text === '') return; // 防止添加空项
const newItem = createTodoItem(text);
todoList.appendChild(newItem); // 插入到列表末尾
todoInput.value = ''; // 清空输入
todoInput.focus();
}
addBtn.addEventListener('click', addTodo);
todoInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') addTodo();
});
这里把添加逻辑提取成 addTodo 函数,让点击按钮和按回车都能调用同一套逻辑。遵循 DRY(Don't Repeat Yourself)原则是写出整洁代码的第一步。
📝 完整代码汇总
下面是 To-Do List 的完整 HTML 文件,复制即可运行:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的 To-Do List</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: system-ui, sans-serif;
background: #1a1a2e;
color: #e0e0e0;
display: flex;
justify-content: center;
padding: 3rem 1rem;
}
.container { max-width: 500px; width: 100%; }
h1 { text-align: center; margin-bottom: 1.5rem; color: #f472b6; }
.input-group { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; }
.input-group input {
flex: 1;
padding: 0.7rem 1rem;
border-radius: 0.5rem;
border: 1px solid rgba(255,255,255,0.1);
background: rgba(255,255,255,0.06);
color: white;
outline: none;
}
.input-group input:focus { border-color: #f472b6; }
.input-group button {
padding: 0.7rem 1.2rem;
background: #f472b6;
color: #1a1a2e;
border: none;
border-radius: 0.5rem;
font-weight: 700;
cursor: pointer;
}
.input-group button:hover { background: #ec4899; }
ul { list-style: none; }
ul li {
display: flex;
align-items: center;
padding: 0.7rem 1rem;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 0.5rem;
margin-bottom: 0.5rem;
}
ul li span { flex: 1; margin: 0 0.75rem; }
ul li span.done { text-decoration: line-through; color: #6b7280; }
ul li .del {
background: none;
border: none;
color: #ef4444;
cursor: pointer;
font-size: 1.1rem;
}
</style>
</head>
<body>
<div class="container">
<h1>📋 我的待办列表</h1>
<div class="input-group">
<input type="text" id="input" placeholder="添加新任务...">
<button id="addBtn">添加</button>
</div>
<ul id="list"></ul>
</div>
<script>
const input = document.getElementById('input');
const addBtn = document.getElementById('addBtn');
const list = document.getElementById('list');
function createItem(text) {
const li = document.createElement('li');
const check = document.createElement('input');
check.type = 'checkbox';
const span = document.createElement('span');
span.textContent = text;
const del = document.createElement('button');
del.className = 'del';
del.textContent = '✕';
check.addEventListener('change', () => span.classList.toggle('done'));
del.addEventListener('click', () => li.remove());
li.append(check, span, del);
return li;
}
addBtn.addEventListener('click', () => {
const text = input.value.trim();
if (!text) return;
list.appendChild(createItem(text));
input.value = '';
input.focus();
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') addBtn.click();
});
</script>
</body>
</html>
🚀 扩展思路
下面是一些让你继续深入的建议:
- 本地存储 — 用
localStorage保存待办数据,刷新页面不丢失。 - 分类标签 — 给每个待办添加「工作」「学习」「生活」等标签。
- 拖拽排序 — 用 HTML5 Drag & Drop API 让用户拖动调整顺序。
- 全部/活跃/已完成筛选 — 添加筛选按钮切换显示模式。
- 待办数量统计 — 实时显示「已完成 X / 共 Y 项」的统计信息。
🎯 总结
通过 To-Do List 项目,你已经掌握了 JavaScript 最核心的三个概念:DOM 操作(选中、创建、插入、删除元素)、事件监听(click、keydown 等交互响应)、函数封装(将逻辑抽象为可复用的代码块)。这些都是构建任何 Web 应用的基础能力。接下来可以挑战更复杂的项目:计算器、计时器、天气查询等。记住,动手做项目是学习编程最快的方式 — 哪怕一开始代码不够优雅,只要跑起来就是胜利。
🧩 深入理解JavaScript的核心概念
完成了To-Do List项目之后,你可能对一些JavaScript概念还有疑问。这里对项目中最核心的几个概念做更深入的说明:
1. 事件冒泡与事件委托
在To-Do List中,我们给每个删除按钮单独添加了事件监听。但当列表中的项目数量增多时,这种方式的效率会下降。更好的做法是使用事件委托(Event Delegation):只在列表容器上设置一个事件监听器,利用事件冒泡机制捕获子元素上触发的事件。这样做不仅代码更简洁,而且即使后续动态添加新的列表项,也天然支持事件处理,不需要重新绑定。事件委托是大规模DOM交互场景下的标准实践。
2. 箭头函数 vs 普通函数
你可能注意到教程中混用了两种函数写法。箭头函数(() => {})和普通函数(function() {})之间的一个关键区别是this的指向。箭头函数没有自己的this,它会捕获定义时的上下文中的this值。在事件监听回调中,如果你需要使用this来引用触发事件的元素,应该使用普通函数;如果你需要保持外部上下文的this,则使用箭头函数。理解这个区别是掌握JavaScript进阶的关键一步。
3. DOM操作的性能优化
在To-Do List中,每次添加项目都会直接操作DOM(appendChild)。但当列表包含成百上千个项目时,频繁的DOM操作会导致性能问题。优化方案包括:使用DocumentFragment批量添加元素、使用innerHTML一次性渲染HTML字符串(注意XSS风险)、或者使用虚拟DOM库(如React的核心思想)。在入门阶段,理解直接DOM操作的开销和优化方向比实现最复杂的优化方案更重要。
4. 本地存储(localStorage)实战
让To-Do List在页面刷新后保留数据是下一个最自然的升级方向。使用localStorage.setItem('todos', JSON.stringify(todoArray))保存数据,使用JSON.parse(localStorage.getItem('todos'))读取数据。关键在于找到合适的「保存时机」——每次数据变化时都保存(添加、删除、修改状态),并在页面加载时从localStorage恢复数据。这种「数据驱动UI」的思想是现代前端开发的核心范式。关于如何将HTML和JavaScript更好地结合,可以查阅我们的HTML语义化元素教程,学习如何写出语义清晰、易于维护的前端代码。