10个经典JavaScript小项目源码:从Todo List到计算器

作者:像素漫游

时间:2026-10-30

浏览:415

精选10个适合进阶练习的经典JavaScript原生小项目源码与架构解析,从Todo List与计算器到异步天气查询与Canvas画板,涵盖DOM操作、事件流、状态管理及Web API的实际应用。

文章目录

很多开发者在学完变量、循环、函数等JavaScript基础语法后,往往会停滞在“看得懂语法但写不出完整功能”的阶段。解决这个瓶颈最有效的方法,是用原生JavaScript(Vanilla JS)实现一批覆盖不同知识点的小项目。本文梳理了10个从易到难、涵盖DOM操作、事件模型、时间控制、异步网络请求与Canvas图形渲染的经典项目源码与实现逻辑,帮助你建立扎实的代码组织与调试能力。

10个经典JavaScript小项目源码:从Todo List到计算器
图1:10个小项目的技术演进与能力递进路线

项目一:待办事项清单(Todo List)

Todo List 是前端开发的“Hello World”。它看似简单,但完整实现它需要兼顾数据驱动视图、DOM事件委托以及浏览器的 localStorage 永久存储。

核心目标:掌握“纯数据状态(State)”与“DOM渲染(Render)”分离的思维,使用 JSON.stringify 与 JSON.parse 实现本地持久化。
10个经典JavaScript小项目源码:从Todo List到计算器
图2:状态驱动视图与 LocalStorage 读写生命周期

核心代码实现

// 数据状态
let todos = JSON.parse(localStorage.getItem('my_todos')) || [];

const inputEl = document.querySelector('#todo-input');
const listEl = document.querySelector('#todo-list');
const addBtn = document.querySelector('#add-btn');

// 保存并重新渲染
function saveAndRender() {
  localStorage.setItem('my_todos', JSON.stringify(todos));
  render();
}

function render() {
  listEl.innerHTML = '';
  todos.forEach((item, index) => {
    const li = document.createElement('li');
    li.className = item.completed ? 'completed' : '';
    li.innerHTML = `
      <span class="toggle-check" data-index="${index}">${item.completed ? '☑' : '☐'}</span>
      <span class="text">${escapeHtml(item.text)}</span>
      <button class="del-btn" data-index="${index}">删除</button>
    `;
    listEl.appendChild(li);
  });
}

// 事件委托:避免为每个按钮重复绑定监听
listEl.addEventListener('click', (e) => {
  const index = e.target.dataset.index;
  if (index === undefined) return;

  if (e.target.classList.contains('del-btn')) {
    todos.splice(Number(index), 1);
    saveAndRender();
  } else if (e.target.classList.contains('toggle-check')) {
    todos[index].completed = !todos[index].completed;
    saveAndRender();
  }
});

addBtn.addEventListener('click', () => {
  const text = inputEl.value.trim();
  if (!text) return;
  todos.push({ text, completed: false });
  inputEl.value = '';
  saveAndRender();
});

function escapeHtml(str) {
  return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

// 初始渲染
render();
注意事项:许多初学者喜欢直接操作 DOM 节点的 innerHTML 或用 li.remove() 删除节点,这容易导致页面显示的节点与存储在 LocalStorage 中的数据脱节。必须坚持“只修改数组,随后统一调用 render()”的原则。此外,向 HTML 模板插值时务必进行字符转义,防止 XSS 攻击。

项目二:简易四则运算计算器(Calculator)

很多初学者实现计算器会直接使用 eval(),这既存在安全隐患,也无法精确掌控输入状态。高质量的做法是建立有限状态机(State Machine),管理“当前输入数”、“前置操作数”与“运算符”。

10个经典JavaScript小项目源码:从Todo List到计算器
图3:计算器状态机分支与求值流程

核心代码实现

class Calculator {
  constructor(displayElement) {
    this.displayElement = displayElement;
    this.clear();
  }

  clear() {
    this.currentOperand = '0';
    this.previousOperand = '';
    this.operation = undefined;
    this.updateDisplay();
  }

  appendNumber(number) {
    // 阻止重复输入小数点
    if (number === '.' && this.currentOperand.includes('.')) return;
    if (this.currentOperand === '0' && number !== '.') {
      this.currentOperand = number.toString();
    } else {
      this.currentOperand = this.currentOperand.toString() + number.toString();
    }
    this.updateDisplay();
  }

  chooseOperation(operation) {
    if (this.currentOperand === '') return;
    if (this.previousOperand !== '') {
      this.compute();
    }
    this.operation = operation;
    this.previousOperand = this.currentOperand;
    this.currentOperand = '';
  }

  compute() {
    let result;
    const prev = parseFloat(this.previousOperand);
    const current = parseFloat(this.currentOperand);
    if (isNaN(prev) || isNaN(current)) return;

    switch (this.operation) {
      case '+': result = prev + current; break;
      case '-': result = prev - current; break;
      case '×': result = prev * current; break;
      case '÷': 
        if (current === 0) {
          alert('除数不能为0');
          this.clear();
          return;
        }
        result = prev / current; 
        break;
      default: return;
    }
    // 解决 0.1 + 0.2 = 0.30000000000000004 精度问题
    this.currentOperand = Math.round(result * 1e12) / 1e12;
    this.operation = undefined;
    this.previousOperand = '';
    this.updateDisplay();
  }

  updateDisplay() {
    this.displayElement.innerText = this.currentOperand || '0';
  }
}

项目三:高精度倒计时器与秒表(Stopwatch & Timer)

很多人实现秒表时直接在 setInterval(..., 1000) 里进行 seconds++。但浏览器在切换标签页、省电模式或主线程繁忙时会延缓定时器,导致累计误差越来越大。正确的做法是通过时间戳差值(Timestamp Delta)来校准时间。

核心代码实现

let startTime = 0;
let elapsedTime = 0;
let timerInterval = null;
const timeDisplay = document.querySelector('#time-display');

function formatTime(ms) {
  const totalSeconds = Math.floor(ms / 1000);
  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds % 60;
  const centiseconds = Math.floor((ms % 1000) / 10);
  return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(centiseconds).padStart(2, '0')}`;
}

function startTimer() {
  if (timerInterval) return;
  // 关键:以实际系统时间为基准,消除累积时钟漂移
  startTime = Date.now() - elapsedTime;
  timerInterval = setInterval(() => {
    elapsedTime = Date.now() - startTime;
    timeDisplay.innerText = formatTime(elapsedTime);
  }, 10);
}

function pauseTimer() {
  clearInterval(timerInterval);
  timerInterval = null;
}

function resetTimer() {
  pauseTimer();
  elapsedTime = 0;
  timeDisplay.innerText = '00:00.00';
}

项目四:安全随机密码生成器(Password Generator)

该项目用于练习字符池过滤、随机数洗牌以及浏览器现代剪贴板 API(navigator.clipboard.writeText)。

核心代码实现

const CHAR_SETS = {
  uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
  lowercase: 'abcdefghijklmnopqrstuvwxyz',
  numbers: '0123456789',
  symbols: '!@#$%^&*()_+~|}{[]:;?><,./-='
};

function generatePassword(length = 16, options = { upper: true, lower: true, nums: true, syms: true }) {
  let charPool = '';
  let guaranteedChars = [];

  if (options.upper) {
    charPool += CHAR_SETS.uppercase;
    guaranteedChars.push(getRandomChar(CHAR_SETS.uppercase));
  }
  if (options.lower) {
    charPool += CHAR_SETS.lowercase;
    guaranteedChars.push(getRandomChar(CHAR_SETS.lowercase));
  }
  if (options.nums) {
    charPool += CHAR_SETS.numbers;
    guaranteedChars.push(getRandomChar(CHAR_SETS.numbers));
  }
  if (options.syms) {
    charPool += CHAR_SETS.symbols;
    guaranteedChars.push(getRandomChar(CHAR_SETS.symbols));
  }

  if (!charPool) return '';

  const passwordArr = [...guaranteedChars];
  for (let i = passwordArr.length; i < length; i++) {
    passwordArr.push(getRandomChar(charPool));
  }

  // 数组随机乱序(Fisher-Yates Shuffle 洗牌算法)
  for (let i = passwordArr.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [passwordArr[i], passwordArr[j]] = [passwordArr[j], passwordArr[i]];
  }

  return passwordArr.join('');
}

function getRandomChar(str) {
  return str.charAt(Math.floor(Math.random() * str.length));
}

// 复制到剪贴板
async function copyPassword(text) {
  try {
    await navigator.clipboard.writeText(text);
    alert('密码已成功复制到剪贴板!');
  } catch (err) {
    alert('复制失败,请手动选取复制');
  }
}

项目五:实时表单验证器(Form Validator)

表单验证用于掌握正则表达式、失焦(blur)与实时输入(input)事件协同、以及 CSS 动态样式反馈。

核心代码实现

const form = document.querySelector('#register-form');
const username = document.querySelector('#username');
const email = document.querySelector('#email');
const password = document.querySelector('#password');

function showError(input, message) {
  const formControl = input.parentElement;
  formControl.className = 'form-control error';
  const small = formControl.querySelector('small');
  small.innerText = message;
}

function showSuccess(input) {
  const formControl = input.parentElement;
  formControl.className = 'form-control success';
}

function checkEmail(input) {
  const re = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/;
  if (!re.test(input.value.trim())) {
    showError(input, '请输入合法的邮箱格式');
    return false;
  }
  showSuccess(input);
  return true;
}

function checkRequired(inputArr) {
  let isAllValid = true;
  inputArr.forEach(input => {
    if (input.value.trim() === '') {
      showError(input, `${input.dataset.name || '此项'}不能为空`);
      isAllValid = false;
    } else {
      showSuccess(input);
    }
  });
  return isAllValid;
}

// 离开焦点时即时校验
email.addEventListener('blur', () => checkEmail(email));

form.addEventListener('submit', (e) => {
  e.preventDefault();
  const validRequired = checkRequired([username, email, password]);
  const validEmail = checkEmail(email);

  if (validRequired && validEmail) {
    alert('表单验证通过,准备提交!');
  }
});

项目六:无缝平移图片轮播器(Image Carousel)

轮播器训练 CSS transform: translateX() 与 JS 索引边界判断的配合,以及鼠标悬停停止自动轮播与移出恢复播放的控制。

核心代码实现

const track = document.querySelector('.carousel-track');
const slides = Array.from(track.children);
const nextBtn = document.querySelector('#next-btn');
const prevBtn = document.querySelector('#prev-btn');
let currentIndex = 0;
let autoPlayTimer = null;

function moveToSlide(index) {
  if (index < 0) {
    currentIndex = slides.length - 1;
  } else if (index >= slides.length) {
    currentIndex = 0;
  } else {
    currentIndex = index;
  }
  track.style.transform = `translateX(-${currentIndex * 100}%)`;
}

nextBtn.addEventListener('click', () => moveToSlide(currentIndex + 1));
prevBtn.addEventListener('click', () => moveToSlide(currentIndex - 1));

// 自动轮播与悬停暂停
function startAutoPlay() {
  autoPlayTimer = setInterval(() => moveToSlide(currentIndex + 1), 3000);
}
function stopAutoPlay() {
  clearInterval(autoPlayTimer);
}

track.addEventListener('mouseenter', stopAutoPlay);
track.addEventListener('mouseleave', startAutoPlay);
startAutoPlay();

项目七:模态弹窗与手风琴折叠面板(Modal & Accordion)

模态弹窗与折叠面板的核心是 CSS 类名切换(classList.toggle)以及利用遮罩层冒泡阻断和键盘 Escape 事件监听。

核心代码实现

// 手风琴折叠交互
const accordionHeaders = document.querySelectorAll('.accordion-header');

accordionHeaders.forEach(header => {
  header.addEventListener('click', () => {
    const body = header.nextElementSibling;
    const isOpen = header.classList.contains('active');

    // 排他折叠:关闭其他面板
    document.querySelectorAll('.accordion-header').forEach(h => {
      h.classList.remove('active');
      h.nextElementSibling.style.maxHeight = null;
    });

    if (!isOpen) {
      header.classList.add('active');
      body.style.maxHeight = body.scrollHeight + 'px';
    }
  });
});

// 模态弹窗交互
const modal = document.querySelector('#demo-modal');
const openModalBtn = document.querySelector('#open-modal');
const closeModalBtn = document.querySelector('#close-modal');

function toggleModal(show) {
  modal.style.display = show ? 'flex' : 'none';
}

openModalBtn.addEventListener('click', () => toggleModal(true));
closeModalBtn.addEventListener('click', () => toggleModal(false));

// 点击弹窗外部阴影关闭
window.addEventListener('click', (e) => {
  if (e.target === modal) toggleModal(false);
});

// ESC 按键便捷关闭
window.addEventListener('keydown', (e) => {
  if (e.key === 'Escape' && modal.style.display === 'flex') {
    toggleModal(false);
  }
});

项目八:实时天气查询卡片(Fetch API 与异步交互)

前端核心技能之一是使用 async/await 处理异步网络请求,妥善管理加载中状态(Loading State)、异常捕获(Error Handling)并更新视图。

核心代码实现

const cityInput = document.querySelector('#city-input');
const searchBtn = document.querySelector('#search-btn');
const weatherCard = document.querySelector('#weather-card');

async function fetchWeather(city) {
  // 视图进入加载状态
  weatherCard.innerHTML = '<p class="loading">正在查询天气数据...</p>';

  try {
    // 采用公开规范的地理编码或天气接口示例
    const apiUrl = `https://api.open-meteo.com/v1/forecast?latitude=39.9042&longitude=116.4074&current_weather=true`;
    const response = await fetch(apiUrl);

    if (!response.ok) {
      throw new Error(`网络响应错误: 状态码 ${response.status}`);
    }

    const data = await response.json();
    displayWeather(data.current_weather);
  } catch (error) {
    weatherCard.innerHTML = `
      <div class="error-msg">
        <p>查询失败: ${error.message}</p>
        <button onclick="fetchWeather('北京')">重试</button>
      </div>
    `;
  }
}

function displayWeather(weather) {
  weatherCard.innerHTML = `
    <div class="weather-info">
      <h3>当前温度: ${weather.temperature} °C</h3>
      <p>风速: ${weather.windspeed} km/h</p>
      <p>观测时间: ${weather.time}</p>
    </div>
  `;
}

searchBtn.addEventListener('click', () => {
  const city = cityInput.value.trim();
  if (city) fetchWeather(city);
});

项目九:Canvas 涂鸦白板(Drawing Board)

通过 HTML5 <canvas> 元素与 2D 渲染上下文,监听鼠标的 mousedown、mousemove、mouseup 事件实现轨迹连续绘制与橡皮擦功能。

核心代码实现

const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
const colorPicker = document.querySelector('#color');
const strokeSize = document.querySelector('#size');
const clearBtn = document.querySelector('#clear');

let isDrawing = false;
let lastX = 0;
let lastY = 0;

function draw(e) {
  if (!isDrawing) return;

  ctx.strokeStyle = colorPicker.value;
  ctx.lineWidth = strokeSize.value;
  ctx.lineCap = 'round';
  ctx.lineJoin = 'round';

  ctx.beginPath();
  ctx.moveTo(lastX, lastY);
  ctx.lineTo(e.offsetX, e.offsetY);
  ctx.stroke();

  [lastX, lastY] = [e.offsetX, e.offsetY];
}

canvas.addEventListener('mousedown', (e) => {
  isDrawing = true;
  [lastX, lastY] = [e.offsetX, e.offsetY];
});

canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', () => isDrawing = false);
canvas.addEventListener('mouseleave', () => isDrawing = false);

clearBtn.addEventListener('click', () => {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
});

项目十:经典贪吃蛇游戏(Snake Game)

游戏是综合检验逻辑控制的最佳项目。贪吃蛇涉及二维网格坐标转换、键盘监听防掉头、游戏主循环更新以及边界自撞碰撞检测。

核心代码实现

const gameCanvas = document.querySelector('#gameCanvas');
const gCtx = gameCanvas.getContext('2d');
const GRID_SIZE = 20;
const TILE_COUNT = gameCanvas.width / GRID_SIZE;

let snake = [{ x: 10, y: 10 }];
let food = { x: 5, y: 5 };
let dx = 0;
let dy = 0;
let score = 0;
let gameLoopId = null;

function update() {
  const head = { x: snake[0].x + dx, y: snake[0].y + dy };

  // 撞墙或撞自身检测
  if (
    head.x < 0 || head.x >= TILE_COUNT ||
    head.y < 0 || head.y >= TILE_COUNT ||
    snake.some(segment => segment.x === head.x && segment.y === head.y)
  ) {
    clearInterval(gameLoopId);
    alert(`游戏结束!最终得分: ${score}`);
    return;
  }

  snake.unshift(head);

  // 吃到食物
  if (head.x === food.x && head.y === food.y) {
    score += 10;
    spawnFood();
  } else {
    snake.pop(); // 没吃到食物时移除尾部
  }

  drawGame();
}

function drawGame() {
  // 清屏
  gCtx.fillStyle = '#1E293B';
  gCtx.fillRect(0, 0, gameCanvas.width, gameCanvas.height);

  // 绘制食物
  gCtx.fillStyle = '#A66C5B';
  gCtx.fillRect(food.x * GRID_SIZE, food.y * GRID_SIZE, GRID_SIZE - 2, GRID_SIZE - 2);

  // 绘制蛇身
  gCtx.fillStyle = '#52677D';
  snake.forEach(segment => {
    gCtx.fillRect(segment.x * GRID_SIZE, segment.y * GRID_SIZE, GRID_SIZE - 2, GRID_SIZE - 2);
  });
}

function spawnFood() {
  food = {
    x: Math.floor(Math.random() * TILE_COUNT),
    y: Math.floor(Math.random() * TILE_COUNT)
  };
}

window.addEventListener('keydown', (e) => {
  if (e.key === 'ArrowUp' && dy === 0) { dx = 0; dy = -1; }
  else if (e.key === 'ArrowDown' && dy === 0) { dx = 0; dy = 1; }
  else if (e.key === 'ArrowLeft' && dx === 0) { dx = -1; dy = 0; }
  else if (e.key === 'ArrowRight' && dx === 0) { dx = 1; dy = 0; }
});

function startGame() {
  snake = [{ x: 10, y: 10 }];
  dx = 1; dy = 0;
  score = 0;
  spawnFood();
  if (gameLoopId) clearInterval(gameLoopId);
  gameLoopId = setInterval(update, 120);
}
startGame();
10个经典JavaScript小项目源码:从Todo List到计算器
图4:10个经典项目涉及的核心技术与关键思维转变

学习建议与进阶拓展

动手实现这10个项目时,建议遵循以下三个步骤:

  1. 先做最小可行版本(MVP):不要一开始就追求华丽的 CSS 阴影或复杂特效。优先用最简 HTML 结构把 JS 数据流与交互逻辑跑通。
  2. 加入异常边界防护:比如计算器处理“除以零”、Todo List 拦截“空文本提交”、表单过滤“非法字符”、异步请求增加“断网重试提示”。
  3. 重构与模块化:在能够熟练编写原生 JavaScript 之后,尝试将通用方法(如防抖、节流、时间格式化、请求封装)提取为独立工具函数,这能为你后续深入学习 Vue、React 或 Node.js 打下扎实的工程化基础。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章