
1. 先聊几句为什么又要翻出 jQuery 来复习前一阵子接了个老后台系统的维护任务项目里的交互层基本全是$开头的代码。一开始我还挺自信毕竟前端做了这么多年结果改第一个筛选表单就被绊住了——我想取 checkbox 的选中状态顺手写了attr(checked)结果拿到的一会是undefined一会是checked折腾了十几分钟才反应过来应该用prop()。那一刻我意识到jQuery 对我来说已经从老朋友变成了最熟悉的陌生人。于是花了一整周的碎片时间系统过了一遍 jQuery 3.x 的核心 API把平时用得着、容易忘、容易出错的内容重新整理了一遍。这篇博文就是那次复习的笔记主要解决三件事第一把选择器、事件、属性操作、Ajax、动画这些高频能力重新过一遍整理成可以直接照着写的清单第二把容易踩坑的知识点比如attr和prop的区别、事件委托的绑定规律、动画队列的失控场景用对比的方式讲清楚第三给出一些 jQuery 与原生 JavaScript 的取舍建议方便在越来越强调原生能力的现代浏览器环境里做选择。适合看的读者大概有两类一类是跟我一样好久不用 jQuery的前端开发者基础还在但很多 API 已经记不清另一类是刚接触 jQuery、想系统理解常见写法的新手。这篇文章不会逐条讲入门语法而是把高频、易错、核心的部分挑出来讲透每一节都尽量搭配可直接运行的代码片段你在浏览器控制台里就能验证。有一点要提前说明文中示例基于 jQuery 3.x。如果你还在维护 jQuery 1.x / 2.x 的老项目个别 API 行为可能不完全一样比如 1.x 时代的live()、delegate()、$.parseJSON等我在后面相关位置会单独提醒。2. 选择器、遍历和链式调用——日常用得最多的三块jQuery 的上手体验很大程度上来自那套选择器机制。现在原生有querySelectorAll可以做类似的事但 jQuery 的查询结果是一个类数组对象不仅支持下标访问还挂了大量方法这是它和原生NodeList差异最大、也最好用的地方。2.1$()的各种调用方式最常见的用法是$(#id)、$(.class)、$(li.item)这不用多讲。但$()里除了选择器字符串还能放别的东西有几个点容易被忽略$(document)/$(window)构造 jQuery 包装对象用于绑定文档或窗口级事件。$(htmlString)传入 HTML 字符串比如$(li标题/li)会直接创建 DOM 节点。注意如果字符串看起来像普通文本而不是 HTMLjQuery 可能把它当成选择器处理。$(element)原生 DOM 对象可以直接包成 jQuery 对象这在混写原生代码时特别方便。$(function () {})等价于DOMContentLoaded之后执行回调比把脚本放在 body 底部更稳。实际项目里最常见的组合是这样// 创建节点并插入 const $item $(li, { text: 新增项, class: list-item, data-id: 1, click: function () { console.log(点击了, this); }, }); $(#myList).append($item); // 把原生节点包成 jQuery 对象 const rawNode document.getElementById(myList); const $list $(rawNode);这里有个实用建议如果一段代码里要反复使用同一个选择器结果尽量缓存成一个变量用$开头的命名来提醒自己这是 jQuery 对象。比如const $list $(#list)后续都操作$list而不是每次写$(#list).find(...)。这样既避免重复查询也让代码清晰不少。2.2 在 DOM 树里移动find、children、closest 等 API我复习时发现最容易想不起来的是parent()和parents()的区别、closest()和parents()的区别。简洁记法parent()只往上找一层parents()往上找所有祖先节点直到document。closest()从当前元素自身开始向上查找直到匹配选择器的第一个祖先它返回的是 0 个或 1 个元素适合做找最近的某个容器。parents()返回所有符合选择器的祖先可能是一串。还有个容易混的是children()和find()。children()只查直接子元素find()查所有后代匹配元素。这个差别很大写的时候要想清楚需求。const $list $(#list); // children 只查直接子元素 const $directLis $list.children(li); // find 查所有后代 const $allHighlightLis $list.find(li.highlight); // 从当前元素向上找最近的容器 const $nav $(#currentItem).closest(.nav-item); // parents 一直往上收集 $(this).parents(.container);filter()和not()在整理列表时很好用。filter()支持选择器、函数、DOM 元素甚至 jQuery 对象用函数时可以拿到索引和当前元素。比如想筛出所有偶数行且不是disabled的项$tableRows.filter(function (index) { return index % 2 0 !$(this).hasClass(disabled); }).addClass(stripe);2.3 链式调用背后的返回逻辑jQuery 方法大多返回自身这是链式调用的基础。但有几个方法会改变返回集合进而影响后续操作find()/filter()/not()/eq()/first()/last()返回新的、筛选后的集合。add()会把新元素合并进当前集合。end()能回到上一个筛选状态这对链式操作非常有用。$(#list) .find(li) .addClass(highlight) .eq(0) .addClass(first-item) .end() // 回到 find(li) 的集合 .end() // 回到 $(#list) .addClass(has-highlight);我第一次看到.end()时觉得有点玄其实就是操作栈的弹出。它适合在链式调用里临时切换集合范围写完还能退回来继续操作原集合。熟悉这个机制后很多需要嵌套循环的代码能写成一句链式表达读起来反而不难。复习到这一块我的感受是选择器和遍历 API 不用死记在脑子里建立按关系找元素有哪几类方法、filter和end能改变当前集合这个框架就够了具体写法查一下文档就能想起来。3. 事件系统绑定、委托、命名空间一次理清事件模块是 jQuery 的老本行。现在原生addEventListener已经很成熟但 jQuery 的事件系统多了一些顺手的能力尤其是事件委托和命名空间事件写业务代码时能省不少事。3.1 on/off/one 的基本用法从 1.7 之后jQuery 统一用on()绑定事件、off()解绑、one()绑定只执行一次的事件。基本写法$(#saveBtn).on(click, function () { console.log(保存); }); // 绑定多个事件 $(#saveBtn).on(click mouseenter, function (e) { console.log(e.type, this); }); // 传给处理函数的数据 $(#saveBtn).on(click, { userId: 7 }, function (e) { console.log(e.data.userId); });off()解绑时要注意绑定用的是具名函数时解绑也要传同样的函数引用否则解不掉匿名函数相当于新的引用直接写off(click)才能把 click 对应的处理函数全部清掉。one()记住执行一次自动解绑。但如果你在回调里又调用one()去绑定别的逻辑要小心会不会造成多次绑定。比如在展开菜单的回调里绑定了别的 click 事件下一次展开时又绑一次最后可能导致旧监听器残留这是老项目里常见的隐患。3.2 事件委托为什么推荐用 on 而不是 delegate事件委托的原理是让父容器监听事件通过判断event.target是否匹配选择器来触发处理函数。比如$(#orderTable).on(click, .del-btn, function () { $(this).closest(tr).remove(); });这样即使.del-btn是表格渲染之后动态添加的也能被正确处理。jQuery 早期有live()和delegate()live()在document上做统一委托性能差且不好控制。现在都建议用on()的第二参数直接写委托选择器。live()在 jQuery 1.9 起已经移除升级老代码时要处理掉。委托有两个注意点一是如果监听器绑在父容器上而子元素的事件处理函数在冒泡过程中调用了stopPropagation()事件不会冒泡到父容器委托就不会触发二是事件委托不能用在完全不冒泡的事件上比如focus和blur本身不冒泡但多数浏览器支持focusin/focusoutjQuery 里用on(focusin)才能可靠地实现委托。3.3 命名空间事件和自定义事件命名空间是 jQuery 特有的一层机制写法是click.namespace$(#panel).on(click.open, handler1); $(#panel).on(click.close, handler2); // 只解绑 click.open不影响 click.close $(#panel).off(click.open);这在插件内部很常用解绑时不会误伤其他逻辑。自定义事件则靠trigger()手动触发$(#app).on(refresh.list, function () { loadData(); }); // 某个回调里触发 $(#app).trigger(refresh.list);配合自定义事件可以做到模块间的松耦合通信。比如列表组件和筛选组件之间不用互相引用只要约定事件名一方trigger另一方on。老项目里这种模式比到处传回调函数更简洁不容易把代码耦合成一团。还要提一个细节trigger()会触发原生默认行为吗答案是不会除非事件处理函数返回false。所以你trigger(click)时浏览器不会把它当成真实用户点击去跳转链接这个在模拟用户操作时要记住。4. 属性、样式和类名这些 API 里藏着不少细节属性操作是 jQuery 出错最多的地方之一因为attr()和prop()看起来都能取属性但在布尔型属性上有本质差别。4.1 attr 和 prop 不能乱用jQuery 官方建议对于 HTML 属性用attr()对于 DOM 属性property用prop()。实际开发中最典型的是 checkbox 的checked、select 的selected、input 的disabledattr(checked)返回的是 HTML 属性字符串比如checked或undefined它不会跟随用户交互实时变化。prop(checked)返回的是布尔值跟随用户勾选状态实时变化。写代码时判断复选框是否选中应该用prop(checked)初始化选中态用prop(checked, true)处理比较稳妥。用attr()判断用户交互状态就很容易出现页面看起来全选了但提交的数据不对这种问题。// 推荐读取选中状态 if ($checkbox.prop(checked)) { ... } // 推荐设置选中状态 $checkbox.prop(checked, true); // 不要用 attr(checked) 判断用户交互状态再比如href、src、value这些一般用attr()或prop()都行但拿到的值可能有差异jQuery 对attr(href)会尽量保留 HTML 里写入的文本而原生 DOM 的.href属性通常是补全后的完整 URL。比较链接值时这个差异很容易出 bug。4.2 css()、宽高测量和无回流的读取方式css()方法简单但要注意它读取的是计算样式不是内联样式。css(margin-top)拿到的是计算后的值通常带px或百分比。设置样式时多个属性可以传对象$el.css({ display: flex, gap: 8px, color: #333 });jQuery 的width()/height()和css(width)的区别在于前者默认返回不带单位的数值方便加减计算后者返回的是字符串可能带px。做尺寸计算时优先用width()/height()省去parseInt的麻烦。复习时我还重点看了innerWidth()/outerWidth()这一系width()content 宽度innerWidth()content paddingouterWidth()content padding borderouterWidth(true)content padding border marginconst box $(#box).outerWidth(true);这在计算两个元素的水平间距、判断布局是否超限时很实用比原生offsetWidth/getBoundingClientRect多了语义层次对新手尤其友好。但必须提醒一句反复读取布局属性width()、offsetHeight等会强制浏览器回流。如果在一个循环里从头到尾读取同一批元素尺寸最好先缓存// 不太建议循环里多次触发布局读取 for (let i 0; i items.length; i) { const w $(items[i]).outerWidth(); } // 更稳先收集后计算 const widths $items.map(function() { return $(this).outerWidth(); }).get();4.3 类名处理与 data() 的存储行为addClass/removeClass/toggleClass的基础用法不展开讲了有个小技巧是它们都支持函数形式$(li).toggleClass(function (index, oldClass) { return level- (index % 3); });做斑马纹或交替样式时这种写法省代码。data()值得多留意jQuery 的data()会把数据缓存到内部对象里不直接写进 DOM 的>$(#card).data(price, 299); console.log($(#card).data(price)); // 299数字类型 console.log($(#card).attr(data-price)); // undefined因为并没有写到 DOM 上5. Ajax 和异步接老接口时离不开的能力新项目基本都用fetch或axios了但老后台、老页面里 jQuery Ajax 仍然常见。复习一次重点看配置项和 Promise 行为。5.1 $.ajax 的配置项快速回顾$.ajax的核心配置项记住这些就够了url、method、data、dataType、contentTypesuccess、error、complete回调timeout超时时间headers自定义请求头cache是否走缓存processData是否序列化 data常用的 GET 写法$.ajax({ url: /api/users, method: GET, data: { page: 2, size: 10 }, dataType: json, timeout: 5000, success: function (res) { console.log(res); }, error: function (xhr, status, error) { console.error(status, error); } });如果dataType写的是jsonjQuery 会尝试自动JSON.parse响应。响应格式不对时会在error回调里给你一个parsererror状态这个别忘。还有个容易被忽略的数组参数问题data里放数组默认会序列化成namevaluenamevalue或name[]valuename[]value具体取决于 jQuery 的traditional配置。后端要求带[]的 key 时用默认的traditional: false就对了如果后端不认[]就要设traditional: true或自己在data里拼好字符串。5.2 快捷方法 get/post/getJSON$.get、$.post、$.getJSON都是$.ajax的语法糖。在 jQuery 3.x 里这些快捷方法仍然保留但需要复杂配置时我不建议用。比如接口要求 JSON 请求体就得写$.ajax手动指定contentType和data。$.getJSON适合简单读取$.getJSON(/api/config, { env: prod }, function (data) { console.log(data); });$.getJSON和$.get的区别就是dataType预设为json省得每次写。但它同样有回调嵌套的问题复杂业务逻辑里维护起来并不轻松。5.3 Deferred 对象与 Promise 行为这是复习时最值得看的一块。jQuery 1.5 之后引入了 Deferredajax 方法返回的是jqXHR对象这个对象实现了 Promise 接口支持.done()/.fail()/.always()也支持.then()。jQuery 3.x 有一个关键改动返回的 Promise 对象遵循 Promises/A 规范.then()的行为和原生 Promise 基本一致不再是 jQuery 1.x 里那种回调返回值不会透传的老逻辑。$.ajax(/api/user) .then(function (res) { // 处理并返回新值 return normalizeUser(res); }) .then(function (user) { console.log(标准化后的用户, user); }) .catch(function (err) { console.error(请求失败, err); });jQuery 3.x 里.catch()也存在写起来和原生 Promise 很像。把老代码从回调改成 Promise 链时有一点要记住success回调拿到的第一个参数是响应数据本身不是jqXHR对象别和$.ajax返回值搞混。还有个常见问题$.ajax返回的对象虽然可以.then()但它不是原生Promise实例。在async/await里直接await $.ajax(...)大部分现代浏览器能工作因为它会被当成 thenable但如果遇到值传递形态不一致的坑建议包一层Promise.resolve来统一行为const users await Promise.resolve($.ajax(/api/users));6. 动画与效果简单的 API不简单的队列逻辑jQuery 动画看起来是最友好的部分一个fadeIn()、slideUp()几行代码就有过渡效果。但用得多了就会遇到动画没按预期跑连续点击疯狂排队这类问题根子都在队列机制上。6.1 显示隐藏类效果基本方法有show()/hide()/toggle()、fadeIn()/fadeOut()/fadeToggle()、slideDown()/slideUp()/slideToggle()。每个方法都接受时长参数和完成回调$(#tips).slideDown(300, function () { console.log(展开了); });这些方法默认会修改元素的display属性配合 jQuery 的记忆机制能恢复到初始的display值。这个记忆行为对普通 div 没问题但如果元素原本的display是flex、grid等非默认值老版本的 jQuery 恢复时可能会退化成一个普通块级显示。jQuery 3.x 优化了一部分但在复杂布局里最好不要依赖show()的恢复逻辑而是手动控制 class 或 style。6.2 animate() 的核心参数animate()是 jQuery 动画的基础方法支持属性对象、时长、缓动函数、回调$(#block).animate({ left: 100px, opacity: 0.5, height: toggle }, 500, swing, function () { console.log(动画完成); });注意animate()只对数值型的样式属性有效比如left、top、width、height、opacity、margin、padding。颜色、transform这类属性不支持直接做动画。想用transform动画要么靠 CSS transition要么用插件或自己管理transform值这是很多初学者卡住的点。jQuery 还允许在属性对象里写hide/show/toggle这些关键字配合宽高变化做折叠效果比较省事。但对现代性能优化来说不如直接用 CSS 类名配合 transition 来得可控。6.3 stop()、finish() 和队列冻结问题连续触发动画时jQuery 默认把动画加进队列逐个执行鼠标快速滑过菜单时尤其明显会出现停不下来的感觉。解决方法是每次触发动画前调用stop(true, true)清空队列并立即完成当前动画$(#menu).hover(function () { $(this).stop(true, true).slideDown(200); }, function () { $(this).stop(true, true).slideUp(200); });stop(true, true)里第一个参数表示清空后续队列第二个参数表示当前动画跳转到结束状态。动画切换场景里一般都用这两个true能避免抖动和排队。finish()则把当前队列里所有动画直接跳到最终状态。它与stop(true, true)的区别是finish()不产生过渡直接落到最终值stop(true, true)会让当前正在执行的动画瞬间完成后再清空队列。动画性能方面多说一句animate()会操作内联样式比如top、left这会持续触发布局性能远不如 CSStransform。只是做位移的话优先选 CSS transition transform或者用requestAnimationFrame自己控制。jQuery 动画现在的定位更多是维护简单、快速实现不是性能最优。7. jQuery 与原生 JavaScript 对照复习顺带梳理浏览器 API复习 jQuery 时绕不开原生 API因为现在的组件和框架里很多场景已经不需要 jQuery 了。做一份高频对照有助于理解 jQuery 底层包装的逻辑。7.1 高频操作对照表场景jQuery 写法原生写法选择元素$(.item)document.querySelectorAll(.item)选择单个$(#id)document.querySelector(#id)遍历$list.each(cb)NodeList.forEach(cb)事件绑定$el.on(click, fn)el.addEventListener(click, fn)事件委托$parent.on(click, .child, fn)parent.addEventListenertarget.closest添加类$el.addClass(a)el.classList.add(a)切换类$el.toggleClass(a)el.classList.toggle(a)读写属性$el.attr(href, v)el.setAttribute(href, v)读取布尔属性$el.prop(checked)el.checked设置样式$el.css({...})el.style插入节点$container.append($el)container.append(el)Ajax GET$.get(url, cb)fetch(url).then(r r.json())等待 DOM$(function(){})DOMContentLoaded事件这张表不是让你马上把所有 jQuery 都改成原生写法而是建立一种映射遇到jQuery 表达不了或行为不符合预期的情况能快速落到原生方案。7.2 既然有原生 API为什么还要用 jQuery现在可以说句公道话现代浏览器里很多 jQuery 能力原生已经有了比如选择器、classList、fetch、事件委托。但 jQuery 仍然有三个不可替代的场景第一是存量项目维护。老后台、旧插件、第三方组件甚至一些 CMS 主题都是基于 jQuery 写的。在这些项目里大规模重写成原生成本极高用 jQuery 反而是最低成本的方案。第二是兼容需求。如果你的页面要兼容运行环境无法升级的老设备jQuery 3.x 帮你抹平了不少差异event、ajax 等模块的行为相对统一。第三是某些业务表达更简洁。动态列表渲染后的事件委托、快速封装小插件jQuery 的链式 API 确实更省代码。团队都熟悉的话开发效率并不低。但如果你是零基础开始做现代前端项目或者页面交互很少我会优先建议用原生。尤其是包体积敏感的活动页jQuery 压缩后也有七八十 KB为了几个 DOM 操作引入它并不划算。7.3 模块化项目里引入 jQuery 的注意点如果要在模块化项目里用 jQuery注意导入方式。典型写法import $ from jquery;某些环境下比如 legacy 组件依赖window.$需要在入口文件里注入全局import $ from jquery; window.$ window.jQuery $;如果你在 Vue 或 React 项目中配合 jQuery 插件使用还要在组件卸载时正确销毁事件和实例避免 DOM 监听器残留。很多引入了 jQuery 后页面出现诡异问题的情况多半是 jQuery 操作放在了模块顶层组件卸载后没有清理导致的。8. 复习完想留下的几条实战提醒8.1 attr/prop老代码最容易翻车的点建议全局搜一下项目里的attr(checked)、attr(disabled)这类写法。如果是在判断状态基本都是潜在 bug。统一改成prop()这个改动不复杂但收益很大。8.2 事件重复绑定与 off 的清理习惯动态加载组件、反复刷新局部模块时如果监听器绑在document或window上很容易出现同一个事件触发两次的问题。排查时可以看看是不是每次加载都调用了一次.on()且没有off()。好的习惯是绑定前先off()掉同名事件或者在组件销毁时做清理$(window).off(scroll.scrollHandler).on(scroll.scrollHandler, handler);8.3 版本差异升级前先看迁移说明维护老项目时如果打算从 1.x 升到 3.x至少要注意这些变化live()已移除、$.parseJSON已废弃、$.ajax的 context 行为变了、.size()被.length替代、$.trim可以直接用原生String.prototype.trim。升完级之后全局搜一遍这些 API 名称逐个确认能省掉很多线上问题。8.4 更多时候复习的意义是理解原理这次复习带给我的一点感触是jQuery 很多设计即使在今天也很有借鉴价值比如链式操作、事件委托、按需模块化。就算你不再直接写 jQuery理解这些机制也能帮助你更好地使用框架或原生 API。在 Vue 里处理列表项事件时你也会想到事件委托写原生选择器时你也会主动用closest()来定位容器。熟悉过 jQuery 的人对 DOM 和事件模型的理解通常更扎实。回到开头那个场景老后台系统里那一堆$代码我没有急着全部改写成原生。更实际的做法是趁这次复习把高频用法和易错点沉淀下来后续维护时写得更稳遇到问题也能更快定位。如果你也处在好久不用 jQuery的状态建议找个半成品小页面把这篇里的代码片段在浏览器里跑一遍会比单纯看文档管用得多。