很多人学JavaScript基础的时候,知识点一个接一个能背出来,但真到了要自己动手做一个小功能的时候,又不知道从哪儿下手。这其实是“输入”和“输出”之间的鸿沟——你听懂了某个语法,不代表你会用它来解决一个具体问题。我写这个系列,就是打算用一个个小而完整的前端JS案例,把变量、函数、DOM操作、事件监听这些东西串起来。今天是系列的第二篇,我们做一个几乎没有UI负担、但知识点密度很高的TODO列表应用。
这个案例适合这几类人:刚学完JS基础语法、想在真实场景里练手的新手;准备前端面试、需要快速过一遍DOM操作和事件机制的人;以及觉得自己“看得懂代码但写不出功能”的零基础转行者。整个项目纯前端实现,不需要脚手架,不需要打包工具,一个HTML文件加一个JS文件就能跑起来,把注意力完全集中在JavaScript本身。你把它做完,再回头看那些八股文式的面试题,会发现理解完全不一样。
1. 项目整体设计与思路拆解
1.1 为什么选“TODO应用”作为基础案例
很多教程喜欢用“点击按钮弹窗”“鼠标悬停变色”这种小例子来讲解DOM操作。这种例子不是没用,而是太碎,每个例子只覆盖一个知识点,学完以后你依然不知道该在什么时候用哪个API。TODO列表不一样,它是完整的功能闭环:有输入、有渲染、有交互、有状态变化,还有数据的增删改查。
我选这个案例,核心考虑就三条。第一,它天然需要一个数据结构来存放任务(数组),这就能把数组方法、对象属性、遍历操作全部带出来。第二,它必须依赖DOM操作才能把数据渲染到页面上,createElement、appendChild、textContent这些高频API全部要过一遍。第三,它需要事件监听来驱动交互,而且涉及“按钮点击”和“回车提交”两种触发方式,事件对象、事件绑定的细节就藏在这些交互里。
另外,TODO应用有天然的扩展空间。做完基础版本之后,你还可以加本地存储(localStorage)、加筛选按钮(全部/未完成/已完成)、加编辑功能。这意味着这个案例不是一次性的,随着你JS水平提升,可以反复回来给这个项目加东西,每次都有新收获——这是很多“一次性小例子”给不了的。
1.2 技术方案选择的背后逻辑
这个案例我刻意没有引入任何框架,也没有用构建工具,而是选了一个原生JavaScript方案,甚至不拆HTML和JS文件。可能有朋友觉得,现在前端都用Vue、React了,谁还手写原生JS?但我的观点很直接:框架解决的是复杂应用的状态管理和组件复用问题,而不是帮你学会JavaScript本身。如果你原生JS不够扎实,一上来就学框架,你写的代码大概率是“框架语法+一知半解的JS”,出了问题根本不知道是框架层的问题还是语言层的问题。
原生方案还有一个实际好处:调试链路最短。浏览器打开HTML文件就能跑,不用启动开发服务器,不用配置打包器,console一开就干活。对于基础学习来说,消消乐式的环境搭建才是最劝退的。而且,面试时很多题目考的就是原生JS的理解,这些眼力是要靠一次次写原生代码练出来的。
我在设计这个项目时特意做了分层:数据层(tasks数组)和视图层(渲染函数renderTasks)分离。这其实是在用最简单的形式演示一个非常重要的工程思想——不要让数据和界面乱成一锅粥。以后你接触任何前端框架,核心思路也都是这一条:数据变了,界面跟着变。能在小项目里有这个意识,后面学框架会轻松非常多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心API与关键技术点详解
2.1 获取元素的几种方式
DOM操作的第一步永远是“拿到元素”。这个案例里我用到了两种获取方式:getElementById和querySelector。很多新手会疑惑,这两个到底什么区别?简单说,getElementById只认id,返回的是一个元素对象;querySelector接受任何CSS选择器,返回的是匹配的第一个元素。
javascript复制const inputBox = document.getElementById('task-input');
const addButton = document.querySelector('#add-btn');
const taskList = document.querySelector('#task-list');
实操中我更推荐在需要精确定位单个元素时优先用getElementById,因为它语义清晰、性能稍好。querySelector的优势在于灵活,比如选中某个元素下的第一个子元素,用querySelector('.item')一下就能做到。两种方式都要熟练,因为看别人的代码时经常两种混着用。
还有一个高频考点需要特别注意:getElementsByClassName返回的是HTMLCollection(实时集合),而querySelectorAll返回的是NodeList(静态集合)。这俩在很多面试题里被拿来对比,核心区别在于前者是“活的”,DOM变化会自动反映到集合里,后者不会。这个案例里我会刻意不用getElementsByClassName,就是希望大家先把querySelectorAll的NodeList记住,后面进阶再去理解“实时集合”的特殊性。
2.2 createElement动态创建节点
拿到元素之后,下一步是往页面上添加内容。很多人第一反应是改innerHTML,用字符串拼出一段HTML再塞进去。这个方法快是快,但有几个隐患:一是字符串拼接容易漏引号、漏标签闭合,写起来很痛苦;二是如果插入的内容包含用户输入,用字符串拼HTML容易出XSS安全问题;三是每次赋值innerHTML都会让浏览器重新解析整个子节点,频繁操作时性能不好。
所以这个案例我选择用createElement系列API来构建节点
