JavaScript入门练手项目推荐:ToDo List与计算器实现思路

作者:窗边听雨时间:2026-10-30 18:42:55浏览:395

针对JavaScript初学者的经典练手项目推荐,深入解析ToDo List与计算器的核心实现思路、数据设计、关键代码与避坑指南。

很多初学者在掌握了 JavaScript 的基础语法(变量、函数、条件分支、循环与数组方法)后,都会经历一段“看得懂语法但写不出完整功能”的平台期。脱离语法练习、进入实际项目开发的最佳路径,是挑选既涵盖核心技术点又具备明确输入输出的微型应用。ToDo List(待办清单)和简易计算器是公认最适合初学者的两个练手项目,它们分别对应了前端开发的两大基本范式:以 DOM 操作与数据存储为代表的“数据驱动型应用”,以及以输入流处理与分支状态为代表的“逻辑状态机应用”。本文将系统解析这两个项目的核心架构、关键实现思路与常见避坑点。

JavaScript入门练手项目推荐:ToDo List与计算器实现思路
图 1:初学阶段两大项目的核心侧重点与学习进阶路径

一、项目一: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">`)上绑定一个点击监听,通过事件冒泡机制识别触发的目标元素。

JavaScript入门练手项目推荐:ToDo List与计算器实现思路
图 2: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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

初学者高频踩坑点:直接使用 `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)。
JavaScript入门练手项目推荐:ToDo List与计算器实现思路
图 3:简易计算器按键处理与运算状态机流转图

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 = '';
    }
  }
}

三、两个项目的关键异同与选型对比

虽然两者都是入门级项目,但它们锻炼的开发能力与思维维度有明显的互补性:

JavaScript入门练手项目推荐:ToDo List与计算器实现思路
图 4:两大入门项目的核心维度对比与总结

四、进阶扩展与代码优化建议

当上述基本功能完成后,不要急于寻找下一个项目,建议在此基础上进行功能拓展以体会真实工程中的复杂度升级:

  • 为 ToDo List 增加过滤筛选与可编辑功能:增加“全部 / 未完成 / 已完成”筛选标签。双击某项任务文本可以直接就地转换为输入框进行二次修改,失焦或回车后自动保存。
  • 为计算器加入键盘事件映射:监听 `window.addEventListener('keydown', ...)`,使用户敲击小键盘数字或 Enter 键时,界面能产生对应的按压效果与运算反应。
  • 模块化拆分:尝试使用 ES Modules(import / export)将界面展示逻辑、数据层逻辑、工具函数拆分为独立的 .js 文件,体验工程化的模块划分。

动手独立写出这两款应用后,不仅能将零散的基础语法真正融会贯通,还会清晰体会到原生 JavaScript 在手动维护 DOM 与同步状态时的繁琐与痛点。带着这种切身体会再去学习现代前端框架(如 React、Vue),你将能一眼看透响应式系统、虚拟 DOM 和组件化设计的初衷与本质。

作者最新文章
相关文章
更多