说实话,每次带新人,都会被同一个问题问到:现在前端都 Vue、React 了,还有必要花时间学 jQuery 吗?前两天朋友想加个网站动态效果,让我帮他改造一下页面,我看了一眼他的项目,Bootstrap + jQuery 的老搭档,顺手就用起来,几分钟把滑动门菜单和一个弹窗模块搞定了。他愣了半天,然后说了一句:"这玩意儿还没死啊?"
我反手给他看了几个正在线上跑的项目,然后问他:如果有一天你要接手其中任何一个,你会怎么办?前端这个行业走到今天,HTML 和 CSS 负责骨架和皮肤,JavaScript 负责肌肉和神经,而 jQuery 曾经是调用肌肉最省力的那根神经。哪怕到了现在,这套"HTML + CSS + JavaScript + jQuery"的组合拳,在存量项目、快速原型、后端顺手做页面这些场景里,依然活得很好。
这篇是这个快速入门系列的第三篇,前两篇我们打通了 HTML 标签语义和 CSS 布局样式,这一篇正式来啃 JavaScript,同时把 jQuery 这个老牌工具放进实战里去用。内容会从变量、函数、数组、DOM 这些 JavaScript 地基,延伸到选择器、事件委托、链式操作这些 jQuery 日常,再结合真实项目里常见的报错和坑,最后用一个完整的待办事项案例带你走一遍 JS 和 jQuery 双版本实现。不管你是后端顺手写页面,还是准备正式入行前端,看完这篇再动手敲一遍,你手里的家伙就齐了。
1. 先回答一个现实问题:2025 年了,学 jQuery 到底图什么
1.1 我见过 jQuery 最多的场合
如果只看招聘网站的岗位要求,jQuery 几乎可以忽略不计。但如果你真正接触过中小公司的线上项目,你会发现它压根没消失。
我这些年接手过的项目里,jQuery 出现最多的场合有这么几类:
- 企业后台管理系统,尤其是一批基于 Bootstrap + jQuery 搭起来的老后台,表格、弹窗、表单校验、权限菜单全部跑在 jQuery 上。
- 服务端渲染的页面,后端程序员自己维护的管理界面,图省事直接引 jQuery,交互逻辑用几行 $() 搞定。
- 官网、活动页、信息展示站,不需要工程化构建,打开浏览器就能跑,jQuery 插件随便挑。
- 各种开源插件生态,像轮播图、日期选择、富文本编辑器、数据表格,老牌插件底层基本都是 jQuery 写的。
前两年帮朋友维护一个公司内部后台,整个项目没有打包工具、没有前端框架,清一色静态 HTML + jQuery + Bootstrap。每次改需求,就是改选择器、调 AJAX、改 DOM 结构。这个后台承担了全公司每天上千次的数据录入和审核操作,谁要跟老板说"这东西该重构了",老板只会问一句:重构期间谁保证系统不停?
这就是 jQuery 真实的生存状态。
1.2 jQuery 本质上是一套"操作 DOM 的哲学"
我经常跟新人强调:jQuery 不是另一门语言,它是对原生 JavaScript 中 DOM 操作、事件处理、AJAX 这些重复劳动做的一层封装。它的核心套路就两件事:找到元素、操作元素。然后换成一条链写出来。
这就是 jQuery 标志性的链式操作。同样是拿到一个元素,加个样式,再改属性,原生 JS 要写好几句,jQuery 可以一路点下去。这套"查找元素 + 操作元素"的命令式思维,和现在框架里的"数据驱动"完全不同,但你不能说它没用。
有三个核心思想,即便到今天依然值得从 jQuery 里吸收:
- 选择器语法:jQuery 里拿 CSS 选择器查元素的写法,后来原生 API querySelectorAll 直接借鉴了同一套规则。
- 链式调用:方法返回自身、继续调下一个方法的设计,现在很多库的 API 依然长这样。
- 事件委托:把事件绑定到父节点,通过判断目标元素统一处理,React 的合成事件底层思路和它是同源的。
把这套思想看明白了,你会发现自己学 Vue、React 并不是从零开始,很多概念早就在 jQuery 里见过。
1.3 我给初学者的一条学习路线
关于"要不要学 jQuery",我的建议不是简单的要或不要,而是讲究顺序:
第一步,原生 JavaScript 必须先过一遍基础,变量、函数、数组、对象、DOM 增删改查、事件绑定与冒泡、AJAX 请求,这些跑不掉。第二步,用 jQuery 实现同样的功能,感受一下代码量怎么从十行变三行,同时深入了解它为什么能做到。第三步,带着 jQuery 里的选择器思维、事件委托思维去学现代框架,你会发现很多概念是相通的。
我自己带人的时候,一直坚持一个原则:先原生,后 jQuery,最后再上框架。跳过原生直接学 jQuery 的人,遇到报错往往一脸懵,因为他不清楚底层在干什么;反过来,原生基础扎实的人,学 jQuery 基本一天就能上手干活。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 原生 JavaScript 的地基:变量、函数、数组与 DOM 操作
jQuery 用得再熟,也建议先把原生 JavaScript 过一遍。原因很简单,jQuery 说到底就是一层封装,你连封装底下是什么都不知道,出了 Bug 就只能干瞪眼。
2.1 变量声明与数据类型
JavaScript 声明变量有 var、let、const 三种方式。面试里最常考的区别是 var 有变量提升和函数作用域,let 和 const 支持块级作用域。但在实战里,我给你的建议更简单粗暴:能 const 就不要 let,能 let 就不要 var。
javascript复制const PI = 3.14; // 常量,声明后不可重新赋值
let count = 0; // 可变变量,块级作用域
count = count + 1; // 正常赋值
var old = '我不建议用'; // var 函数作用域,容易出乱子
数据类型方面,JS 里有 number、string、boolean、null、undefined、object、symbol、bigint 几种基础类型。新手最容易踩的坑是 typeof null 返回的是 "object",这是 JS 诞生时遗留的 Bug,官方选择保留而不是修复。遇到判断空值的情况,别光用 typeof,要结合 === null 一起判断。
2.2 函数是 JavaScript 里的一等公民
函数在 JavaScript 里有三种常见的写法:函数声明、函数表达式、箭头函数。
javascript复制// 函数声明:会提升,可以在声明之前调用
function add(a, b) {
return a + b;
}
// 函数表达式:不会提升,必须声明后才能使用
const subtract = function(a, b) {
return a - b;
};
// 箭头函数:写法更简洁,但没有自己的 this
const multiply = (a, b) => a * b;
箭头函数在事件回调里要格外小心。jQuery 里如果用了箭头函数,内部 this 指向会和 function 写法不同。举个真实例子,$('#btn').on('click', () => { $(this)... }) 这里的 this 就不再指向按钮元素,而是指向外层上下文,新手经常在这里翻车。所以你在 jQuery 事件回调里,要么用 function 关键字,要么用 $(event.target) 替代 this。
2.3 数组的常见操作与"判断有没有数据"
数组在 JS 里太常用了。先看几个增删方法:
javascript复制const arr = [1, 2, 3];
arr.push(4); // 末尾添加 -> [1, 2, 3, 4]
arr.pop(); // 删除末尾 -> [1, 2, 3]
arr.unshift(0); // 开头添加 -> [0, 1, 2, 3]
arr.shift(); // 删除开头 -> [1, 2, 3]
还有一个高频操作:往一个空数组里添加数据之后,判断数组里到底有没有值。这个问题我在热搜里看到过,"jquery var arr=[] 添加 判断arr有没有数据"。最标准的答案永远是 length 属性:
javascript复制const list = [];
list.push('任务一');
list.push('任务二');
if (list.length > 0) {
console.log('数组里有数据,共 ' + list.length + ' 条');
} else {
console.log('数组是空的');
}
这个判断在 jQuery 和原生 JS 里都通用。还有两个常用的遍历方法 forEach 和 map,前者只管遍历,后者会返回一个新数组。需要过滤数据用 filter,需要把数组变成字符串用 join,这几个方法掌握了,日常开发基本够用。
2.4 浏览器里的 DOM 操作
浏览器把我们写的 HTML 解析成一棵节点树,JavaScript 可以在树上查找、添加、修改、删除节点。这套操作统称为 DOM 操作。
新版本浏览器推荐用下面这套组合:
javascript复制// 查找元素
const box = document.getElementById('box'); // 根据 id 找
const items = document.querySelectorAll('.item'); // 根据 CSS 选择器找
const btn = document.querySelector('#btn'); // 找单个元素
// 创建元素
const div = document.createElement('div');
div.textContent = '我是一段文本';
div.className = 'card';
// 插入元素
const container = document.getElementById('container');
container.appendChild(div);
// 修改样式和属性
div.style.backgroundColor = '#f5f5f5';
div.setAttribute('data-id', '1001');
这套操作虽然啰嗦,但是必须会。jQuery 做的事,本质上就是把上面这些步骤封装成 $() 和 .append(),让你少打几个字。
2.5 事件绑定、事件冒泡与事件委托
浏览器交互的核心是事件。比如用户点击按钮,触发 click 事件。原生 JS 绑定事件的标准写法是 addEventListener:
javascript复制const btn = document.getElementById('btn');
btn.addEventListener('click', function(event) {
console.log('按钮被点击了');
console.log(event.target); // 当前点击的目标元素
});
事件有一个重要特性叫冒泡:点击子元素时,click 事件会从触发它的最深层节点,一层一层向上传播到父节点。基于冒泡机制,我们可以做事件委托——只在父节点上绑定一次事件,所有子节点的事件都交给它统一处理。
javascript复制const list = document.getElementById('list');
// 事件委托:父节点统一监听
list.addEventListener('click', function(event) {
const target = event.target;
if (target.classList.contains('delete-btn')) {
console.log('点击了删除按钮');
}
});
事件委托还有一个好处:动态添加进去的新子元素,不需要重新绑定事件,一样能被父节点监听到。这个思想在后文 jQuery 实战里会反复用到。
3. jQuery 实战:选择器、创建标签与事件委托
这一章我们正式进入 jQuery。先说个前提:你不用把 jQuery 的每个 API 背下来,只需要掌握选择器、DOM 操作、事件绑定、AJAX 这几大块,剩下的大多是查文档的事。
3.1 引入 jQuery 的两种方式
第一种是 CDN 引入,适合联网环境:
html复制<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
第二种是下载 jQuery 文件放到本地项目里,通过相对路径引入:
html复制<script src="js/jquery.min.js"></script>
版本选择上有个实际经验:如果是新项目,直接用 3.x 最新版;如果是要维护老项目,对方用的 1.x 千万别随手升级,老项目里很多插件在 2.x、3.x 下会出兼容性问题。1.x 版本的 jQuery 对 IE6/7/8 都有兼容,这在现在基本用不上了,但老项目里跑得稳,就别乱动。
3.2 选择器与链式操作
jQuery 的核心入口是 $ 函数。传一个 CSS 选择器进去,返回的是一个 jQuery 对象,这个对象上挂了一堆操作方法。
javascript复制$('#btn') // 选 id 为 btn 的元素
$('.card') // 选所有 class 为 card 的元素
$('li.item') // 选 li 且 class 为 item 的元素
$('input[name="name"]') // 选 name 属性为 name 的 input
链式操作是 jQuery 最爽的地方。每个操作元素的方法执行完会返回当前 jQuery 对象本身,所以可以一路点下去:
javascript复制$('#myBtn')
.addClass('active') // 加样式类
.css('font-size', '16px') // 改内联样式
.attr('data-status', 'enabled') // 设置属性
.text('按钮已启用'); // 改文本内容
对比一下原生写法,这四步至少四行代码,jQuery 一行搞定。这就是"少打代码"最直观的体现。
3.3 创建标签与插入内容
有朋友搜过"jquery 创建一个标签",确实创建动态内容是前端高频操作。原生 JS 用 createElement,jQuery 更简洁,直接往 $() 里传 HTML 字符串:
javascript复制// 创建一个 li 标签,带 class 和文本
const $li = $('<li>', {
class: 'todo-item',
text: '这是一个新任务',
'data-id': 5
});
// 也可以直接写 HTML 字符串
const $li2 = $('<li class="todo-item" data-id="5">这是一个新任务</li>');
创建之后要插入到页面里,常用方法有 append、prepend、after、before:
javascript复制$('#todoList').append($li); // 添加到列表末尾
$('#todoList').prepend($li2); // 添加到列表开头
$('#todoList').after(其他元素); // 插到列表后面
这里有一个必须强调的安全习惯:如果 HTML 字符串里有用户输入的数据,一律不要直接用 $() 拼 HTML,用 text() 方案或者先做转义。这个坑在第四章专门讲。
3.4 text() 和 html() 的区别
text() 拿的是纯文本,html() 拿的是内部 HTML 结构。反过来说,用 text() 设置内容,内容会被当作纯文本渲染;用 html() 设置内容,内容会作为 HTML 解析。
有个实际场景:判断一段文本里有没有换行符。如果是通过 text() 获取的文本,换行符是 \n 字符;如果是通过 html() 获取的内容,换行可能显示成
标签。所以判断时要分情况:
javascript复制const content = $('#msg').text();
if (content.indexOf('\n') !== -1) {
console.log('文本中包含换行符');
}
const contentHtml = $('#msg').html();
if (contentHtml.indexOf('<br') !== -1) {
console.log('内容中有 br 换行标签');
}
这个细节处理不好,很多字符串判断逻辑就会神秘失效。我在真实项目里被这个坑绊过一次,后来凡是涉及"判断内容里有没有换行",都先明确我用的是 text() 还是 html()。
3.5 事件绑定的两种姿势与事件委托
jQuery 里普通事件绑定可以用 .on() 方法:
javascript复制$('#btn').on('click', function() {
console.log('按钮被点击');
});
// 等价于
$('#btn').click(function() {
console.log('按钮被点击');
});
但如果是动态生成的元素,直接对它绑事件大概率无效。比如你运行了 $('.delete-btn').on('click', ...),但 .delete-btn 是后面才加进页面的,这时绑定失败。
正确姿势是把事件委托给一个始终存在的父容器:
javascript复制$('#todoList').on('click', '.delete-btn', function() {
// this 指向被点击的 .delete-btn 元素
$(this).closest('li').remove();
});
这种写法的意思是:我监听父容器 #todoList 的 click 事件,但只有当点击目标匹配 .delete-btn 时,这个回调才会执行。无论这个元素是什么时候添加到页面里的,事件都能被捕获。这个用法是 jQuery 实战里最重要的一个,没有之一。
3.6 遍历和判断数组数据
jQuery 也提供了遍历工具 $.each()。对数组和对象都适用:
javascript复制const arr = [];
arr.push('A');
arr.push('B');
$.each(arr, function(index, item) {
console.log(index, item);
});
// 遍历 jQuery 对象集合
$('.card').each(function() {
console.log($(this).text());
});
配合前面说过的判断逻辑,经常这样用:
javascript复制const selectedIds = [];
$('.checkbox:checked').each(function() {
selectedIds.push($(this).val());
});
if (selectedIds.length > 0) {
// 说明有选中项,发请求或做其他处理
}
数组空判断依然是 length,这条建议刻进肌肉记忆。另外 $.isArray() 可以判断一个变量是不是数组,$.trim() 可以去掉字符串首尾空格,这些工具方法很常用,建议翻一眼文档。
3.7 AJAX 与常用工具方法
用 jQuery 发 AJAX 请求,比原生 fetch 和 XMLHttpRequest 更省心,基本就是给个配置对象:
javascript复制$.ajax({
url: '/api/todo/list',
method: 'GET',
dataType: 'json'
})
.done(function(res) {
console.log('请求成功', res);
})
.fail(function() {
console.log('请求失败');
});
简化写法还有 $.get()、$.post()。不过要提醒一句:如果你的新项目注定要用框架,AJAX 这块直接用 fetch 或者 axios 更合适;jQuery 的 AJAX 主要是兼容老项目时的必需品。
4. 真实项目里绕不开的坑:运行时报错、XSS 与事件陷阱
这个系列前两篇讲 HTML 和 CSS 时,我反复强调过"知道怎么排查"比"知道怎么用"更重要。JavaScript 和 jQuery 也一样,下面这几个问题和场景,是我在真实项目里遇到最多、也最值得新手花时间弄懂的。
4.1 排查 JavaScript 运行时报错的完整思路
有朋友搜过"javascript运行时报错"这类问题。JS 的报错比 Java、C++ 友好得多,因为它本身就是解释执行,浏览器会把错误信息直接打在控制台里。我一般按下面这个流程排查:
第一步,看报错信息。最常见的几类:
- SyntaxError:语法错误,写了不合法的 JS 代码,比如括号没闭合、字符串少引号。
- ReferenceError:引用了不存在的变量,比如拼错了名字,搞错了作用域。
- TypeError: Cannot read property 'xxx' of null / undefined:访问了 null 或 undefined 对象的属性。这是实践中最常见的报错,大多因为获取的 DOM 元素不存在,或者接口数据没返回对应字段。
第二步,看报错文件位置和行号。浏览器控制台会把报错信息定位到某个.js文件的某一行,点击就能跳转到 Sources 面板。如果是压缩过的代码,根本看不懂,这时就需要用 Source Map 还原源码。
第三步,加 console.log 缩小范围。在报错位置前面几行打印关键变量,确认它到底是谁、值是什么。
举个例子,新手最常见的报错:
javascript复制const btn = document.getElementById('submitBtn');
btn.addEventListener('click', handler); // 报错:btn 为 null
为什么 btn 是 null?多半是 JS 放在 head 里,执行时浏览器还没解析到按钮,getElementById 当然找不到。解决办法有两个:把 script 标签放到 body 底部,或者把代码包进 DOMContentLoaded 事件里。jQuery 写法就是经典的:
javascript复制$(function() {
// 在这里写代码,等 DOM 就绪后再执行
});
这个排查流程看着简单,但能解决掉日常至少七成的 TypeError。不要一报错就去问人,先看报错信息、再看定位、再打日志,绝大多数问题能自己解决。
4.2 "点击元素消失,点击元素内按钮不消失"到底是怎么回事
这个需求非常常见:页面上有个弹窗或下拉面板,点弹窗外部区域,弹窗关闭;但点弹窗内部的某个按钮,按钮的点击行为要生效,弹窗不能关。很多新手写出来发现,点弹窗里面的按钮,弹窗也跟着关了,怎么点都不对。
原因就是事件冒泡。点击按钮时,事件从按钮一路冒泡到弹窗、再到 body、再到 document。如果你在 document 上监听 click 来"点击外部关闭弹窗",那点按钮时,事件也会冒泡到 document,于是"关闭"逻辑被触发了。
解决方式有两种。
第一种,在弹窗内部元素的点击事件里阻止冒泡:
javascript复制$('.modal').on('click', '.modal-close-btn', function(event) {
event.stopPropagation(); // 阻止事件继续向外冒泡
// 做关闭按钮自己的事情
});
$(document).on('click', function() {
$('.modal').hide(); // 点击外部隐藏弹窗
});
第二种,在 document 的监听器里判断点击目标是否在弹窗内:
javascript复制$(document).on('click', function(event) {
const $target = $(event.target);
if ($target.closest('.modal').length === 0) {
$('.modal').hide(); // 点的不在弹窗内,才隐藏
}
});
第二种写法更稳定,因为它不依赖弹窗内部元素是否都调用了 stopPropagation。我用这种方案处理过很多弹窗关闭需求,推荐优先用它。类似的场景还有下拉菜单、右键菜单、通知气泡,逻辑完全一样。
4.3 jQuery 的 XSS 风险:html()、append() 和 text()
"jquery xss"是很多人搜过的关键词。jQuery 确实有 XSS 风险,但风险不在 jQuery 本身,而在使用方式。核心规则一句话:永远不要把用户输入的内容直接塞进 html() 或 append() 里。
举例,一个评论列表,用户提交的内容是 ,如果前端把这条内容直接当作 HTML 插入页面,浏览器会尝试执行脚本,这就是 XSS 攻击。换到 jQuery 上:
javascript复制// 危险:会把用户内容当成 HTML 解析
$('#commentList').append('<div class="comment">' + userInput + '</div>');
// 安全:当作纯文本插入
const $comment = $('<div>', { class: 'comment', text: userInput });
$('#commentList').append($comment);
text() 会把内容里的特殊字符转义成实体,浏览器只会显示文本,不会解析成标签或脚本。所以安全原则是:
- 想插入纯文本,用 text()。
- 必须插入 HTML 结构时,先确认数据源可信。如果数据来自用户,你要先做 HTML 转义,把 < 转成 <、> 转成 >。
- 不要在字符串拼接里直接塞未过滤的用户数据,不管它是不是 jQuery。
4.4 javascript:void(0) 的来历和陷阱
很多人写 a 标签跳转占位的时候,喜欢写 href="javascript:void(0)"。为什么这么写?因为 a 标签默认有跳转行为,如果不给 href,样式上不像链接;如果给了 #,点击后页面会跳到顶部,体验很怪。javascript:void(0) 的意思就是执行一个返回 undefined 的表达式,让点击后什么也不发生。
但这个写法有几个问题:
第一,把 JS 逻辑写进 HTML,违反了行为和结构分离的原则。第二,如果写成 javascript:void(o),把数字 0 错打成字母 o,浏览器会报错"o is not defined",我在热搜里看到有人问"javascript:void(o)报错,为什么",这就是典型的笔误。第三,将 href 和 onClick 混在一起,如果 JS 报错,a 标签默认跳转可能还会执行。
我的建议是,尽量别用 javascript:void(0)。如果要做一个按钮样式的点击元素,直接改用 button 标签;如果非用 a 标签不可,可以这样写:
html复制<a href="#" id="noopLink">点击</a>
然后在 JS 里阻止默认行为:
javascript复制$('#noopLink').on('click', function(event) {
event.preventDefault(); // 阻止 href="#" 跳转到顶部
// 执行你的逻辑
});
这样行为清晰、不坑人,也方便后来维护的人看懂。
4.5 顺带一提:CSS Flex 布局与 JS 的配合
搜热词列表里还有一条"css flex布局子元素宽度自适应"。这虽然不是 JS 的内容,但前端实战里经常和 JS 搭在一起,简单提一句。
Flex 布局让子元素宽度可以自适应容器,这在响应式页面里非常方便。但有个坑是:当你在 JS 里获取元素宽度时,比如用 offsetWidth、clientWidth、getBoundingClientRect(),拿到的往往是当前渲染宽度。如果元素宽度受 flex 撑开的,同时页面里还有动画、缩放、字体加载等操作,可能会出现"图还没完全加载完,JS 已经拿到了错误的宽度"这种问题。
解决方法是:等图片或者字体资源加载完成后再去取宽度。jQuery 里可以用 $(window).on('load', ...) 或者图片的 load 事件来兜底。这个坑比较细,先记住"取值前确认布局已经稳定"这个原则就行。
5. 综合实战:用原生 JS 和 jQuery 各写一遍"待办事项"
前几章的知识点分散在各个场景里,现在我们把它们串起来。我选择一个非常经典的"待办事项"小工具来做完整案例:输入框填入任务,点击添加,任务进入列表;点击任务前的复选框,任务标记为已完成;点击删除按钮,任务从列表移除。
5.1 功能拆解与准备工作
在写代码之前,先把功能拆成几个动作:
- 用户输入内容,点击"添加"按钮,列表新增一条任务。
- 新任务里有一个复选框,一个文本,一个删除按钮。
- 复选框变化时,任务文字显示删除线。
- 删除按钮点击,移除整条任务。
- 动态添加的任务依然能触发事件,所以事件要委托。
HTML 骨架很简单,一个输入框、一个按钮、一个列表:
html复制<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<title>待办事项</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="todo-app">
<h1>我的待办</h1>
<div class="input-row">
<input type="text" id="todoInput" placeholder="请输入任务">
<button id="addBtn">添加</button>
</div>
<ul id="todoList"></ul>
</div>
<script src="app.js"></script>
</body>
</html>
CSS 简单写一点,保证能看清状态变化:
css复制body { font-family: "PingFang SC", "Microsoft YaHei", sans-serif; background: #f5f5f5; }
.todo-app { max-width: 400px; margin: 50px auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.input-row { display: flex; gap: 10px; margin-bottom: 20px; }
#todoInput { flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
#addBtn { padding: 8px 16px; background: #4a90d9; color: #fff; border: none; border-radius: 4px; cursor: pointer; }
#todoList { list-style: none; padding: 0; }
.todo-item { display: flex; align-items: center; gap: 10px; padding: 8px; border-bottom: 1px solid #eee; }
.todo-item .text { flex: 1; }
.todo-item.done .text { text-decoration: line-through; color: #999; }
.delete-btn { background: #e74c3c; color: #fff; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; }
5.2 原生 JavaScript 版本
原生版本的逻辑完全按照前面讲的事件委托和第二、三章的知识来:
javascript复制(function() {
const input = document.getElementById('todoInput');
const addBtn = document.getElementById('addBtn');
const list = document.getElementById('todoList');
// 添加任务函数
function addTodo() {
const text = input.value.trim();
if (!text) {
alert('请输入任务内容');
return;
}
// 创建 li 元素
const li = document.createElement('li');
li.className = 'todo-item';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'checkbox';
const span = document.createElement('span');
span.className = 'text';
span.textContent = text;
const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-btn';
deleteBtn.textContent = '删除';
li.appendChild(checkbox);
li.appendChild(span);
li.appendChild(deleteBtn);
list.appendChild(li);
input.value = '';
input.focus();
}
// 给添加按钮绑定事件
addBtn.addEventListener('click', addTodo);
// 回车键也能添加
input.addEventListener('keydown', function(event) {
if (event.key === 'Enter') {
addTodo();
}
});
// 事件委托:监听列表里的复选框和删除按钮
list.addEventListener('change', function(event) {
if (event.target.classList.contains('checkbox')) {
event.target.parentElement.classList.toggle('done');
}
});
list.addEventListener('click', function(event) {
if (event.target.classList.contains('delete-btn')) {
const li = event.target.parentElement;
list.removeChild(li);
}
});
})();
这里面用到了一个细节:删除按钮是点击事件,复选框是 change 事件,所以分别委托给 list 监听。动态添加的 li 不需要单独绑事件,所有事件都通过 list 统一处理。
5.3 jQuery 版本
同一个功能,用 jQuery 写出来是这样:
javascript复制$(function() {
const $input = $('#todoInput');
const $addBtn = $('#addBtn');
const $list = $('#todoList');
function addTodo() {
const text = $input.val().trim();
if (!text) {
alert('请输入任务内容');
return;
}
// 创建 li 并一次性插入
const $li = $(
'<li class="todo-item">' +
'<input type="checkbox" class="checkbox">' +
'<span class="text"></span>' +
'<button class="delete-btn">删除</button>' +
'</li>'
);
// 用 text() 写入用户输入,防止脚本注入
$li.find('.text').text(text);
$list.append($li);
$input.val('');
$input.focus();
}
$addBtn.on('click', addTodo);
$input.on('keydown', function(event) {
if (event.key === 'Enter') {
addTodo();
}
});
// 事件委托:动态生成的元素也能被监听
$list.on('change', '.checkbox', function() {
$(this).closest('li').toggleClass('done');
});
$list.on('click', '.delete-btn', function() {
$(this).closest('li').remove();
});
});
注意我在生成 li 时用了字符串拼接,但用户输入的内容是通过 $li.find('.text').text(text) 写入的,没有直接拼到 HTML 字符串里。这就是第 4.3 节强调的安全实践。
5.4 两个版本对比:jQuery 到底简化了什么
我建议你亲手把两个版本都敲一遍,然后做一个对比。它们完成的功能完全一样,但代码量、可读性、心智负担有明显差异:
| 对比维度 | 原生 JS 版本 | jQuery 版本 |
|---|---|---|
| 获取元素 | document.getElementById / querySelector | $() 一个函数搞定 |
| 创建元素 | createElement 然后一个个 append | 写 HTML 字符串即可 |
| 绑定事件 | addEventListener 每个元素单独绑 | .on() 支持事件委托 |
| 修改类名 | classList.add / remove / toggle | addClass / removeClass / toggleClass |
| 插入元素 | appendChild / removeChild | append / remove |
| 新元素事件 | 需要自己在委托逻辑里处理 | .on() 天然支持委托 |
原生版不是不能写,而是同样的逻辑,jQuery 版更聚焦在"业务要做什么",而不是"浏览器 API 怎么调"。这正好体现了 jQuery 存在的意义:把底层差异和模板代码藏起来,让你专注业务逻辑。
5.5 扩展:用 localStorage 做数据持久化
上面这个案例刷新页面后数据就丢了。如果你想让任务保存下来,可以扩展一点,用 localStorage 存一个数组:
javascript复制// 读取本地存储
const savedList = JSON.parse(localStorage.getItem('todoList')) || [];
// 每次添加任务时
function saveTodo(text) {
savedList.push(text);
localStorage.setItem('todoList', JSON.stringify(savedList));
}
这个扩展足够你练习 JSON 序列化和反序列化。如果你想做得再完整一点,还可以在加载页面时把 savedList 里的数据渲染成列表;删除时从数组里剔除对应项再写回 localStorage。建议你在 jQuery 版本的基础上改,改造过程中自然就会碰到数组增删、对象序列化、DOM 重渲染这些高频知识点。
我这几年带新人的时候,发现一个规律:凡是先把原生 JS 基础打得很牢的人,学 jQuery 基本一天足够;凡是跳过原生直接上 jQuery 的,遇到报错就寸步难行。原因很简单,jQuery 再方便也是一层壳,壳下面的 DOM 模型、事件模型、作用域规则不会变。
所以这篇我在原生 JS 的地基部分花了不少篇幅,不是凑字数,而是希望你在动手敲 jQuery 的时候,心里清楚每一行 $() 到底在替你做什么。这比记住一百个 API 更有用。等你把这个待办事项案例来回改过几遍,JS 和 jQuery 的配合套路差不多就印在脑子里了。
