有个读者在后台留言说学了三个月前端,HTML和CSS都能写了,但一到JavaScript就卡住——看教程都懂,自己写就不知道从哪下手。投了几份实习简历,面试官问"你做过什么项目",他只能说出课堂上的静态页面,当场就被下一个候选人比下去了。
这个问题太常见了。JavaScript和HTML/CSS最大的区别在于:后两者是"声明式"的,你写什么就显示什么;JS是"逻辑式"的,你得自己想清楚每一步做什么、什么时候做。光看书不写项目,就像背了所有游泳口诀但从不下水。下面这12个案例从几十行代码到几百行,从DOM操作到异步请求再到Canvas动画,每做一个都能填上你知识体系里的一个窟窿。
12个案例难度分级速览
| 1 | 入门级(1-4):动态时钟、Todo清单、计算器、随机名言 —— 纯DOM操作,30-80行代码,一天能做完两个 |
| 2 | 进阶级(5-8):图片轮播、天气查询、购物车、Markdown预览器 —— 涉及API请求、事件委托、状态管理 |
| 3 | 面试级(9-12):无限滚动加载、Canvas粒子动画、音乐播放器、贪吃蛇 —— 涵盖性能优化、Canvas API、面向对象设计 |
一、动态时钟——理解setInterval和Date对象的第一个项目
别小看一个时钟。这可能是你第一次真正理解"JS如何让页面动起来"的项目。核心就两个知识点:new Date() 获取当前时间,setInterval() 每隔一秒更新一次DOM。
function updateClock() {const now = new Date();const h = String(now.getHours()).padStart(2, '0');const m = String(now.getMinutes()).padStart(2, '0');const s = String(now.getSeconds()).padStart(2, '0');document.getElementById('clock').textContent = `${h}:${m}:${s}`;}setInterval(updateClock, 1000);updateClock(); // 立即显示,不等1秒值得注意的细节:padStart(2, '0') 保证个位数前面补零,不然9点5分显示成"9:5"。setInterval之后先手动调一次updateClock(),否则页面加载后等整整一秒才显示时间。做完后加暗黑模式切换、12/24小时制切换,每加一个功能都是一次新练习。
练什么:Date对象、setInterval/clearInterval、DOM.textContent操作、模板字符串
二、Todo清单——CRUD四大操作全练到,面试最高频实战题
Todo清单看着简单,实际上是一个微型CRUD应用。Create、Read、Update、Delete四个操作全部覆盖。面试官让你现场写一个TodoList的概率非常高,十分钟就能看出你对DOM操作、事件绑定、数组方法、状态管理的熟练程度。
let todos = [{ id: 1, text: '学完JS基础', completed: false },{ id: 2, text: '写5个项目', completed: false }];添加任务用 todos.push(),删除用 todos.filter(),标记完成用 todos.map(),每次数据变了就重新调用render()函数渲染整个列表。这就是最简单的"数据驱动视图"模式,也是Vue和React的核心思想雏形。

最常踩的坑:给每个删除按钮绑事件时,别用for循环加addEventListener引用循环变量——经典的闭包陷阱。用事件委托,把click绑在父容器上,通过event.target判断点了哪个按钮。
进阶方向:localStorage持久化(刷新不丢数据)、拖拽排序、分类标签筛选。一个小Todo能衍生出五六种练手方向。
三、计算器——理解eval的边界和输入校验
计算器是前端面试里仅次于TodoList的第二高频实战题。表面上就是加减乘除,实际在考察对输入校验、状态机、边界条件处理的理解。不能连续输入两个运算符、小数点不能出现两次、除以零要有提示——每一个边界都是扣分点。
最偷懒的实现是用 eval() 直接计算表达式。但要知道eval的安全风险——面试官很可能追问。更好的做法是写一个简单表达式解析器,或至少用 Function('return ' + expression)() 代替。
基础版(30行)
eval计算 + 简单按钮绑定 + 显示结果。一天搞定。
进阶版(100行)
输入校验 + 键盘支持 + 历史记录 + localStorage。面试加分。
四、随机名言生成器——第一次接触fetch和API
这个案例是你的"API初体验"。调用免费名言API,用fetch拿到数据,解析JSON,渲染到页面。整个过程不超过40行代码,但让你第一次理解了"前端怎么跟服务器通信"。
async function getQuote() {const res = await fetch('https://api.quotable.io/random');const data = await res.json();document.getElementById('quote').textContent = data.content;document.getElementById('author').textContent = data.author;}document.getElementById('btn').addEventListener('click', getQuote);关键认知:async/await不是魔法,只是Promise的语法糖。建议先写.then()版本再改成async/await,对比着理解。
延伸练习:换成天气API、翻译API、新闻API——本质一模一样,只是换URL和解析字段名。
五、图片轮播——CSS过渡动画和JS逻辑的第一次配合
轮播图是几乎所有网站都有的组件,也是"前端必会"的标配技能。技术骨架就两样:CSS transition做滑动动画,JS控制索引切换和定时器。
实现方式主要有两种:transform: translateX()位移方案(所有图片排成一排横向移动),和opacity渐变方案(图片叠在一起,切换时改变透明度)。位移方案更常见,因为能配合手指滑动做触摸交互。
最容易被忽略的细节是首尾无缝过渡。从最后一张切到第一张直接跳回去会很突兀。解法是在首尾各克隆一张图片,过渡动画结束后悄悄把位置重置回真实图片索引。
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| translateX位移 | 图片横向排列,整体translate移动 | 支持触摸滑动 | 需克隆首尾元素 |
| opacity渐变 | 图片叠放,切换改透明度 | 实现简单 | 不支持滑动 |
六、天气查询——API调用 + 用户输入 + 错误处理三合一
天气查询是前面案例能力的综合应用:输入框获取城市名 → 拼接API URL → fetch请求 → 解析JSON → 渲染结果。但它多了一个关键环节——错误处理。用户可能输入不存在的城市、网络可能断开、API可能限流,每种情况都要有对应的UI反馈。
推荐用 OpenWeatherMap 免费API,注册就能拿key,每天1000次免费调用。返回数据包含温度、湿度、风速、天气描述、图标代码——正好练习嵌套JSON解析。
async function getWeather(city) {try {const key = '你的API_KEY';const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${key}&units=metric&lang=zh_cn`;const res = await fetch(url);if (!res.ok) throw new Error('城市不存在或网络错误');const data = await res.json();renderWeather(data);} catch (err) {showError(err.message);}}进阶方向:根据天气动态换背景图(晴天暖色、雨天冷色)、添加搜索历史localStorage、用IP定位自动获取当前城市天气。
七、购物车——状态管理和数组方法的终极练习场
购物车会让你真正体会到"数据一变,页面就变"的复杂度。它不是简单的增删改查——加了数量要算小计、删了商品要更新总价、同一商品加第二次要合并数量而不是新增一条。
核心数据
1
个数组管理全部状态
5种操作
5
add/remove/update/count/total
计算属性
3
小计/总价/数量
购物车最核心的设计:不要把总价存成独立变量,而是每次从cart数组实时计算。用cart.reduce((sum, item) => sum + item.price * item.qty, 0)算出总价。这保证总价永远准确,不会因为某个操作忘了更新而出bug。这个思想就是Vue computed和React useMemo的底层逻辑。
八、Markdown实时预览器——第三方库和防抖的实战

这个案例让你第一次接触npm生态。引入 marked.js(CDN直接引用,不需要构建工具),左边textarea输入Markdown,右边div实时渲染HTML。核心代码只有一行:document.getElementById('preview').innerHTML = marked.parse(mdText);
但真正的学习点不在marked本身,而在防抖(debounce)。用户每敲一个字符就重新解析整个Markdown——文章长了性能会明显卡顿。用防抖函数把渲染推迟到用户停止输入300毫秒之后:
function debounce(fn, delay) {let timer;return function(...args) {clearTimeout(timer);timer = setTimeout(() => fn.apply(this, args), delay);};}textarea.addEventListener('input', debounce(updatePreview, 300));防抖和节流是前端面试必考题。做完这个案例,你能清楚说出"防抖是延迟执行,连续触发只执行最后一次;节流是固定频率执行"——比死记硬背印象深十倍。
九、无限滚动加载——Intersection Observer + 分页加载
刷抖音往下滑永远有内容——这就是无限滚动。核心是 Intersection Observer API,监听某个元素是否进入视口。在列表底部放一个哨兵元素,进入视口时触发加载下一页。过去用scroll事件性能很差,Observer是浏览器原生异步检测,不阻塞主线程。
const observer = new IntersectionObserver((entries) => {if (entries[0].isIntersecting) loadMoreData();});observer.observe(document.getElementById('sentinel'));可以搭配JSONPlaceholder的/posts接口模拟分页加载。加载时显示骨架屏或loading动画,数据到底了显示"没有更多了"——完整的社交媒体信息流体验就出来了。
十、Canvas粒子动画——从DOM操作跨越到像素级控制
Canvas是从"操作元素"到"操作像素"的分界线。之前的案例全在操作DOM节点——增删改div、p、button。Canvas完全不同:拿到画布,在上面画点、线、圆,每帧清空重绘,像用代码放电影。
基础思路:创建100个粒子,每个有x、y坐标和vx、vy速度,每帧用requestAnimationFrame更新位置,碰到边界反弹。加交互——鼠标移动时粒子被吸引或排斥,粒子之间连线形成网状结构。
class Particle {constructor(canvas) {this.x = Math.random() * canvas.width;this.y = Math.random() * canvas.height;this.vx = (Math.random() - 0.5) * 2;this.vy = (Math.random() - 0.5) * 2;this.radius = Math.random() * 3 + 1;}update() {this.x += this.vx; this.y += this.vy;if (this.x < 0 || this.x > canvas.width) this.vx *= -1;if (this.y < 0 || this.y > canvas.height) this.vy *= -1;}draw(ctx) {ctx.beginPath();ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);ctx.fill();}}性能提醒:粒子不是越多越好。200个+每帧连线计算=性能灾难。控制在80-120个,连线只连距离小于100px的粒子对,用距离平方比较避免开根号运算。
粒子动画是作品集里的加分项。面试官看到你能用Canvas写动画,至少说明你对JS的理解不限于表单验证和按钮点击。
十一、音乐播放器——Audio API和面向对象编程
音乐播放器是你第一个真正意义上的"面向对象"项目。不是几个函数拼在一起,而是封装一个Player类,包含play()、pause()、next()、prev()、setVolume()等方法,内部管理播放列表、当前索引、播放状态。
核心用HTML5的 Audio API:
class MusicPlayer {constructor(playlist) {this.audio = new Audio();this.playlist = playlist;this.currentIndex = 0;this.isPlaying = false;}load(index) {this.currentIndex = index;this.audio.src = this.playlist[index].url;}play() { this.audio.play(); this.isPlaying = true; }pause() { this.audio.pause(); this.isPlaying = false; }}再加上进度条(监听audio的timeupdate事件)、音量控制、播放模式切换(单曲循环/列表循环/随机)、歌词同步——每加一个功能都在加深对事件驱动编程的理解。
十二、贪吃蛇——Canvas + 键盘事件 + 游戏循环,全部串起来
贪吃蛇作为收尾案例,因为它把前面的能力全串起来了:Canvas绘图(粒子动画学的)、定时器/游戏循环(时钟学的)、键盘事件监听、碰撞检测、数组操作(蛇的身体是坐标数组)、得分系统。300行代码,每一行都在用之前积累的知识。
核心逻辑:蛇是一个坐标数组 [{x:5, y:5}, {x:4, y:5}, {x:3, y:5}],每次移动时头部unshift新坐标,尾部pop。吃到食物时不pop,蛇就变长了。撞墙或撞到自己就game over。
| 关键逻辑 | JS实现方式 | 知识点 |
|---|---|---|
| 蛇的移动 | unshift头部 + pop尾部 | 数组操作、方向向量 |
| 碰撞检测 | 头部坐标 === 墙/身体坐标 | 数组遍历、坐标比较 |
| 游戏循环 | setInterval + requestAnimationFrame | 定时器、帧率控制 |
| 方向锁定 | 不能直接反向(右→左) | 状态校验、边界判断 |
做这些案例的时候,四个习惯比代码本身更重要
第一,先自己写再看答案。每个案例拿到手先关掉浏览器,纯凭记忆写一遍。卡住了再去查MDN文档,实在写不出来才看别人的代码。这个"痛苦回忆"会在大脑里形成最强的神经连接,比直接抄代码记住的概率高五倍不止。
第二,每做完一个就加一个新功能。做完时钟加暗黑模式,做完Todo加localStorage,做完轮播加触摸滑动。主动加功能的过程才是真正的"工程思维"训练,按教程走一遍那叫打字练习。
第三,把代码push到GitHub。不只是存本地——建一个仓库,每个案例一个文件夹,写好README说明用了什么技术、学到了什么。面试的时候直接把GitHub链接扔过去,比简历上写"熟练掌握JavaScript"有说服力一百倍。
12个案例知识点全覆盖一览
| 案例 | 核心知识点 | 适合阶段 |
|---|---|---|
| 动态时钟 | Date、setInterval、DOM更新 | 刚学完基础语法 |
| Todo清单 | CRUD、事件委托、闭包、localStorage | 学完DOM操作后 |
| 计算器 | 输入校验、eval、状态机、边界处理 | 和Todo一起做 |
| 名言生成器 | fetch、Promise、async/await、JSON | 开始学异步时 |
| 图片轮播 | CSS transition、定时器、无缝衔接 | CSS+JS融合阶段 |
| 天气查询 | API调用、try/catch、嵌套JSON解析 | 掌握fetch后 |
| 购物车 | 数组reduce/map/filter、计算属性 | 学完数组方法后 |
| Markdown预览 | 第三方库、debounce防抖 | 开始接触npm生态 |
| 无限滚动 | IntersectionObserver、分页、性能优化 | 进阶阶段 |
| 粒子动画 | Canvas、requestAnimationFrame、OOP | 准备作品集时 |
| 音乐播放器 | Audio API、类封装、事件驱动 | 学完面向对象后 |
| 贪吃蛇 | 综合实战:Canvas+定时器+键盘+碰撞 | 综合复习阶段 |
三种不同的学习路径,对号入座
求职突击路线
Todo → 购物车 → 天气查询 → 粒子动画。重点覆盖面试高频题+作品集加分项,2-3周完成。
体系构建路线
时钟 → 名言 → 计算器 → Todo → 轮播 → 天气 → 购物车 → Markdown预览 → 无限滚动 → 粒子 → 播放器 → 贪吃蛇。按知识依赖顺序做,4-6周。
兴趣驱动路线
粒子动画 → 贪吃蛇 → 音乐播放器。从最酷的开始做,保持热情。之后再回头补基础案例,理解底层原理。
说穿了,JavaScript和游泳、开车一样——看一百个教程也不如自己写一百行代码。这12个案例每个都能在一天到三天内做完,加起来花不到一个月。但做完之后你对JS的理解会从"知道这些语法"变成"知道什么时候用这个语法、为什么用这个语法、不用会怎样"。
别纠结从哪个开始——直接挑一个最感兴趣的,现在打开编辑器,先写再说。第一个案例可能会写得乱七八糟,第二个会顺很多,到第六个的时候你会发现大部分代码不用查文档也能敲出来了。
