针对JavaScript初学者的经典练手项目推荐,深入解析ToDo List与计算器的核心实现思路、数据设计、关键代码与避坑指南。
JavaScript入门练手项目推荐:ToDo List与计算器实现思路
时间:2026-10-30
浏览:394
文章目录
很多初学者在掌握了 JavaScript 的基础语法(变量、函数、条件分支、循环与数组方法)后,都会经历一段“看得懂语法但写不出完整功能”的平台期。脱离语法练习、进入实际项目开发的最佳路径,是挑选既涵盖核心技术点又具备明确输入输出的微型应用。ToDo List(待办清单)和简易计算器是公认最适合初学者的两个练手项目,它们分别对应了前端开发的两大基本范式:以 DOM 操作与数据存储为代表的“数据驱动型应用”,以及以输入流处理与分支状态为代表的“逻辑状态机应用”。本文将系统解析这两个项目的核心架构、关键实现思路与常见避坑点。
一、项目一:ToDo List(待办清单)的实现思路
ToDo List 涵盖了 Web 开发中无处不在的“CRUD”(创建、读取、更新、删除)逻辑。许多初学者最初写 ToDo List 时,往往直接在 DOM 树上硬操作:点击添加就向页面 `innerHTML += '<li>'`,点击删除就直接 `element.remove()`。这种“直接操纵页面元素”的做法在列表稍微复杂(例如需要增加筛选、持久化、批量清空)时,代码会迅速变得混乱不堪。
标准的实现思路是采用“数据驱动视图(State-Driven UI)”模式:在内存中维护一个纯 JavaScript 数组作为真实数据源(Single Source of Truth),所有用户交互只修改这个数据数组,修改完毕后再调用统一的渲染函数将数据映射为 DOM 元素。
1. 数据模型与状态设计
每个待办项应抽象为一个独立的 JavaScript 对象。建议的数据结构如下:
// 内存中的任务列表数据
let todos = [
{
id: 1711200000001, // 使用时间戳或随机数作为唯一标识符
text: "完成 JavaScript 计算器状态图绘制",
completed: false
},
{
id: 1711200000002,
text: "复习数组 filter 与 map 方法",
completed: true
}
];
2. 核心交互流与事件委托
在传统的做法中,每生成一个新任务节点就要为其单独绑定 `click` 事件。如果任务数量很多,会产生大量事件监听器,且在删除节点时容易出现内存泄漏。正确的工程解法是事件委托(Event Delegation):只在任务列表的父级容器(如 `<ul id="todo-list">`)上绑定一个点击监听,通过事件冒泡机制识别触发的目标元素。
3. 核心步骤代码示例
以下是 ToDo List 核心逻辑的精简参考代码,体现了“数据变更带动视图刷新”与“本地持久化”的配合:
// 1. 初始化读取本地缓存
const STORAGE_KEY = 'my_todos_v1';
let todos = JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
const todoList = document.getElementById('todo-list');
const todoInput = document.getElementById('todo-input');
const todoForm = document.getElementById('todo-form');
// 2. 统一保存与渲染
function saveAndRender() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
render();
}
function render() {
todoList.innerHTML = '';
todos.forEach(item => {
const li = document.createElement('li');
li.className = item.completed ? 'completed' : '';
// 注意:利用 data-id 存储标识符,方便事件委托捕获
li.innerHTML = `
<input type="checkbox" class="toggle-btn" data-id="${item.id}" ${item.completed ? 'checked' : ''}>
<span class="text">${escapeHtml(item.text)}</span>
<button class="del-btn" data-id="${item.id}">删除</button>
`;
todoList.appendChild(li);
});
}
// 3. 事件委托:在父级处理所有子项的交互
todoList.addEventListener('click', (e) => {
const id = Number(e.target.dataset.id);
if (!id) return;
if (e.target.classList.contains('del-btn')) {
todos = todos.filter(t => t.id !== id);
saveAndRender();
} else if (e.target.classList.contains('toggle-btn')) {
const targetItem = todos.find(t => t.id === id);
if (targetItem) {
targetItem.completed = !targetItem.completed;
saveAndRender();
}
}
});
// XSS 转义函数,避免直接 innerHTML 注入安全漏洞
function escapeHtml(str) {
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
初学者高频踩坑点:直接使用 `innerHTML` 拼接用户输入的内容时,如果用户输入了类似 `<img src=x onerror=alert(1)>` 的字符,可能会引发跨站脚本(XSS)隐患。推荐使用专门的转义函数,或使用 `textContent` 赋值文本节点。
二、项目二:简易计算器的实现思路
很多新手初写计算器时,最容易走捷径直接使用 `eval(expressionString)`。这种做法虽然两三行代码就能出结果,但在工程上存在两个致命缺陷:一是极大的安全风险;二是无法控制复杂的界面交互逻辑(例如多位数字连续输入、多次按运算符的替换、连续按等号、浮点数精度等)。
编写一个健壮的计算器,本质上是在构建一个有限状态机(Finite State Machine)。每次用户按下一个按键(数字、小数点、运算符、功能键),系统都需要根据当前所处的状态,决定屏幕显示什么、内部暂存什么,并转换到下一个状态。
1. 状态模型定义
一个标准的双操作数计算器通常需要维护以下 4 个核心状态变量:
currentOperand:当前正在输入的数字字符串(如"12.5")。previousOperand:此前输入的数字或阶段性计算结果(如"30")。operation:当前选定的运算符(+、-、×、÷或null)。shouldResetScreen:布尔标记,用于标识下次点击数字时是“拼接在末尾”还是“清屏开始输入新数字”(通常在按下运算符或等号后置为 true)。
2. 计算器核心逻辑代码实现
推荐采用面向对象或类的方式封装计算器的状态,能大幅提升代码的可读性与健壮性:
class Calculator {
constructor(prevDisplayElement, currDisplayElement) {
this.prevDisplayElement = prevDisplayElement;
this.currDisplayElement = currDisplayElement;
this.clear();
}
clear() {
this.currentOperand = '0';
this.previousOperand = '';
this.operation = undefined;
this.shouldResetScreen = false;
}
delete() {
if (this.shouldResetScreen) return;
if (this.currentOperand.length === 1) {
this.currentOperand = '0';
} else {
this.currentOperand = this.currentOperand.slice(0, -1);
}
}
appendNumber(number) {
if (number === '.' && this.currentOperand.includes('.')) return;
if (this.currentOperand === '0' && number !== '.' || this.shouldResetScreen) {
this.currentOperand = number.toString();
this.shouldResetScreen = false;
} else {
this.currentOperand += number.toString();
}
}
chooseOperation(operation) {
if (this.currentOperand === '' && this.previousOperand === '') return;
// 如果已经有上一个操作数和运算符,直接连续计算
if (this.previousOperand !== '') {
this.compute();
}
this.operation = operation;
this.previousOperand = this.currentOperand;
this.shouldResetScreen = true;
}
compute() {
let computation;
const prev = parseFloat(this.previousOperand);
const current = parseFloat(this.currentOperand);
if (isNaN(prev) || isNaN(current)) return;
switch (this.operation) {
case '+': computation = prev + current; break;
case '-': computation = prev - current; break;
case '×': computation = prev * current; break;
case '÷':
if (current === 0) {
alert('除数不能为 0');
this.clear();
return;
}
computation = prev / current;
break;
default: return;
}
// 解决 IEEE 754 浮点数精度(例如 0.1 + 0.2 = 0.30000000000000004)
this.currentOperand = (Math.round(computation * 1e12) / 1e12).toString();
this.operation = undefined;
this.previousOperand = '';
this.shouldResetScreen = true;
}
updateDisplay() {
this.currDisplayElement.innerText = this.currentOperand;
if (this.operation != null) {
this.prevDisplayElement.innerText = `${this.previousOperand} ${this.operation}`;
} else {
this.prevDisplayElement.innerText = '';
}
}
}
三、两个项目的关键异同与选型对比
虽然两者都是入门级项目,但它们锻炼的开发能力与思维维度有明显的互补性:
四、进阶扩展与代码优化建议
当上述基本功能完成后,不要急于寻找下一个项目,建议在此基础上进行功能拓展以体会真实工程中的复杂度升级:
- 为 ToDo List 增加过滤筛选与可编辑功能:增加“全部 / 未完成 / 已完成”筛选标签。双击某项任务文本可以直接就地转换为输入框进行二次修改,失焦或回车后自动保存。
- 为计算器加入键盘事件映射:监听 `window.addEventListener('keydown', ...)`,使用户敲击小键盘数字或 Enter 键时,界面能产生对应的按压效果与运算反应。
- 模块化拆分:尝试使用 ES Modules(
import / export)将界面展示逻辑、数据层逻辑、工具函数拆分为独立的.js文件,体验工程化的模块划分。
动手独立写出这两款应用后,不仅能将零散的基础语法真正融会贯通,还会清晰体会到原生 JavaScript 在手动维护 DOM 与同步状态时的繁琐与痛点。带着这种切身体会再去学习现代前端框架(如 React、Vue),你将能一眼看透响应式系统、虚拟 DOM 和组件化设计的初衷与本质。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。