jQuery 这个东西,说实话在新项目里已经不太常见了,但你要是接手过 2015 年前后的老系统,或者在一些企业后台、外包项目里待过,大概率还是能见到它的影子。每次有人问我“这都什么年头了,还有必要看 jQuery 吗”,我的回答都一样:如果你吃透它的设计思路,再回头去看现在的前端框架,会顺很多。
这篇文章不打算给你讲一堆“历史故事”,也不会照抄文档。我想站在实际使用的角度,把 jQuery 的核心机制、真正高频的 API、常见的坑和性能问题,以及它和现代前端的衔接方式,完整地梳理一遍。无论是还在维护老项目的开发,还是刚入门想补上这块知识的同学,都能从这里拿到可以直接上手的东西。
1. 为什么 jQuery 曾是“唯一解”?
在拆 API 之前,值得先搞清楚一件事:jQuery 当初到底解决了什么问题,为什么它能统治前端那么多年。只有理解了它的设计动机,你才能明白它不少 API 为什么长那样。
1.1 那个年代的痛点:选择器与兼容性
在 jQuery 出现之前,JavaScript 操作页面的方式用“原始”来形容一点不过分。想拿到一个元素,原生方法就getElementById、getElementsByClassName、getElementsByTagName这几个,而且getElementsByClassName在 IE 里还不支持。想在文档里找一个满足多个条件的元素?套循环。想统一监听事件?不同浏览器的事件对象、事件绑定方式都不一样。
jQuery 把这些差异全部抹平了。它把选择器做成了一套通用语法,底层会根据浏览器能力去动态选择对应的原生方法,开发者只需要写$('#app .item'),剩下的兼容问题它全兜了。在 IE6-IE8 还大规模存在的年代,这个能力就是刚需。
1.2 链式调用与隐式迭代的巧妙设计
jQuery 最让人觉得“好用”的两个设计,一个是链式调用,一个是隐式迭代。
链式调用意味着你在拿到一个元素集之后,可以连续调用多个方法,比如$('.item').css(...).addClass(...).on(...)。它的原理其实很简单:每个方法内部最终都会把当前 jQuery 对象 return 出去,所以可以一直点下去。这个设计在代码可读性上优势极其明显,逻辑是顺着一条线往下走的。
隐式迭代则解决了一个很烦人的问题:你拿到的往往不是一个元素,而是一组元素。在原生环境里,你把一组元素每个都改一遍样式,得写循环。jQuery 里$('.item').addClass('active'),它会自动帮你遍历集合里的每一个元素去执行操作。这直接改变了前端编码思维的复杂度,人们开始把“一组元素”当成一个整体来操作。
1.3 适用场景的边界在哪里
放回现在的环境里,我的判断是这样的:
- 老项目维护:项目里全是 jQuery 代码,你不能因为“新框架更好”就重组一遍,维护成本和风险太高,这类场景 jQuery 依然是统计代码的主流选择
- 轻量静态页面:活动页、落地页、纯展示页面,不需要复杂状态管理,引入一个几十 KB 的库能快速解决 DOM 操作和交互问题,比硬上框架更轻
- 快速原型/教学:想快速验证一个交互逻辑,jQuery 的语法和直觉很接近,写起来少,调试快
- 但如果你在做的是长期维护的大型业务系统,或者需要复杂数据流驱动的应用,那还是建议用现代框架
原因在于,jQuery 把所有东西都扁平化处理了,页面的状态散落在全局变量、DOM 属性、data 缓存里,项目一大,就很难追踪。
2. jQuery 的核心能力拆解与实操细节
光看文档会觉得 jQuery API 很多,实际高频使用的就那么几个方向。这一章拆开讲。
2.1 选择器:别只会写$()
$()本质上是创建了一个 jQuery 对象,它会去匹配传入的字符串。底层用的是 Sizzle 引擎,但在现代浏览器里,很多选择器会直接交给原生querySelectorAll去处理,所以性能并不差。
不过使用时有几个细节值得注意:
- 优先用 ID 查找:
$('#app')的性能会显著好于$('div[id=app]'),因为前者走的是getElementById的快捷路径 - 避免过度修饰:
$('ul li a')这种写法会做多层遍历,能简化的就简化成$('.nav-link'),除非真的有嵌套干扰需要排除 - 上下文参数:
$('.item', '#container')的意思是只找容器内部匹配的元素,这相当于先定位容器,再缩小搜索范围,性能上和语义上都更清晰
需要注意的是,jQuery 选择器返回的不是数组,是 jQuery 对象。你直接用console.log($('.item'))看到的是这个对象的内部结构,需要拿某个具体元素用.eq(index),拿第一个用.first(),拿原生 DOM 用[0](下标访问)。
2.2 链式调用技术要点:拆着写并不会让代码“更差”
链式调用虽然顺手,但不是所有场景都适合。
链式调用的前提是每个方法都返回 jQuery 对象。但.text()、.html()、.attr('id')这类 getter 方法返回的是字符串,不是对象,所以链式调用断在它们身上。比如$('#title').text('你好').css('color','red')没问题,但$('#title').text()后面接.css()就会报错,因为.text()返回了字符串而不是对象。
另外,如果链式调用写得太长,浏览器报错时定位问题反而麻烦。我的建议是:一组逻辑关联紧密、不超过 3-4 个方法时链式写,遇到复杂逻辑先单独解析一层再操作。看代码的人心智负担会小很多。
2.3 核心 DOM 操作 API 的取舍
经常有同学分不清.attr()和.prop()。一句话解释:
.attr()操作的是 HTML 属性(attribute),比如自定义属性>$('#container').on('click', '.btn', function() { // 只有点击的 target 匹配 .btn 时才执行 console.log($(this).text()); });原理是事件冒泡到
#container,jQuery 在内部判断 event.target 是否匹配选择器。用委托的好处有两个:一是动态创建的元素天然有事件响应;二是如果你有成百上千个同类按钮,直接逐个绑定会占用大量内存,委托只需要一个 handler,内存占用大幅下降。注意事项:委托的层级越接近目标元素越好,如果挂到 document 上,每次点击都会执行一次匹配判断;如果页面里存在 shadow DOM,这种委托也会失效。
on()方法还可以一次性绑定多个事件,用空格分隔:$('.input').on('focus blur', function() { // focus 和 blur 都触发 });需要注意
blur、focus、change这类事件本身不冒泡,委托时不生效,只有原生支持冒泡的事件才适合放上去。如果你需要处理这类非冒泡事件,一般直接把 handler 绑到具体元素上。2.5 AJAX 封装:老项目里最流畅的异步体验
在 fetch 出现之前,原生写一个跨浏览器 AJAX 至少要十行代码,还得处理各种兼容分支。jQuery 把这块压缩成了一行。
$.ajax({ url: '/api/user/list', method: 'GET', dataType: 'json', data: { page: 2 }, success: function(res) { console.log(res); }, error: function(xhr, status, error) { console.error(status, error); } });简单场景用
$.get、$.post也行,但说实在的,写$.ajax完全体会有更多掌控力,比如设置超时时间、自定义请求头、处理跨域时的 jsonp。几个容易被忽略的参数:
timeout:不设置的话,一个请求卡住时页面会一直等待,体验很差,建议显式设置一个合理值,比如 10000cache: 处理 GET 请求时,IE 和部分代理会缓存响应,导致更新不生效,设为false可以避免这个问题,它会自动在 URL 后面加时间戳dataType: 'json':如果后端返回的是字符串,但你没指定这个字段,jQuery 就会把它当成字符串来接收,你后面res.xxx直接就是 undefined
还有一点,如果你在开发环境里前后端跨域,而且后端没配 CORS,那 jQuery 的 jsonp 也不是万能的。jsonp 的原理是动态加载 script 标签,需要后端配合返回特定格式的
callback包裹数据,很多后端并不支持。这种场景下,现代一点的方案是走开发代理或者让后端配 CORS 头,比在前端硬想要稳得多。3. 老项目实战:高频功能怎么落地
很多人觉得看文档没问题,到了真项目里写的时候却反应不过来。这里我按真实项目里最常见的几类页面逻辑,给你拆几个可以直接套的写法。
3.1 列表数据的局部刷新
老系统最常见的场景:用户在页面上筛选条件,然后点击查询,列表区域刷新,但页面整体不能跳转。
我的习惯是:列表区外层套一个
id="listWrap",查询按钮id="searchBtn",然后逻辑如下:$('#searchBtn').on('click', function() { var params = $('#searchForm').serialize(); $.get('/api/list', params, function(res) { if (res.code !== 0) { alert(res.msg || '查询失败'); return; } var html = ''; $.each(res.data, function(i, item) { html += '<tr>' + '<td>' + item.name + '</td>' + '<td>' + item.statusText + '</td>' + '<td><button class="btn-edit">$('#listWrap').on('click', '.btn-edit', function() { var id = $(this).data('id'); // 继续打开编辑逻辑 });3.2 表单校验与异步提交
jQuery 官方有个 validate 插件,功能挺全,但老项目里很多时候不用引插件,手写几个校验函数就够了。
我一般会在页面上定义一个校验规则对象:
var validators = { required: function(value) { return $.trim(value) !== ''; }, number: function(value) { return /^\d+$/.test(value); }, email: function(value) { return /^[\w.-]+@[\w-]+(\.[\w-]+)+$/.test(value); } };然后统一校验所有带
>function checkForm() { var isValid = true; $('input[data-rule]').each(function() { var field = $(this); var rule = field.data('rule'); if (validators[rule] && !validators[rule](field.val())) { field.addClass('error'); isValid = false; } else { field.removeClass('error'); } }); return isValid; }提交时先调用
checkForm(),通过以后再用$.post提交。提交按钮在请求过程中应该禁用,防止用户重复点击产生重复订单或重复提交数据。$('#submitBtn').on('click', function() { if (!checkForm()) { return; } var btn = $(this); btn.prop('disabled', true).text('提交中...'); $.post('/api/save', $('#mainForm').serialize(), function(res) { if (res.code === 0) { location.reload(); } else { alert(res.msg); btn.prop('disabled', false).text('提交'); } }).always(function() { // 防止成功时如果没刷新页面,按钮状态也恢复 btn.prop('disabled', false).text('提交'); }); });需要注意:如果请求成功后的代码里用
location.reload()刷新页面,.always()里的代码可能来不及执行,页面就刷新了,这并不会带来副作用。但如果你刷新前还有后续操作,需要考虑顺序,必要时在success里 return,避免走向always。3.3 选项卡和轮播,代码不复杂但容易写乱
选项卡功能几乎每个后台系统都有,核心就是一个“切换 + 联动”:
$('.tab-item').on('click', function() { var index = $(this).index(); $(this).addClass('active').siblings().removeClass('active'); $('.panel').eq(index).show().siblings().hide(); });.index()拿到元素在同级中的序号,这在联动时非常常用,而且比自己去维护索引变量靠谱很多,因为它直接由 DOM 结构推导。给
.panel的显示隐藏用.show()和.hide(),jQuery 会恢复元素原来的display状态,比直接用 CSSdisplay: block要省心。轮播图的话,如果只是做基础功能,建议不要自己写动画调度器,直接用 jQuery 的
fadeIn/fadeOut或者animate,代码量更少也更稳:var current = 0; var total = $('.slide-item').length; function goTo(index) { if (index < 0) { index = total - 1; } if (index >= total) { index = 0; } $('.slide-item').eq(current).fadeOut(); $('.slide-item').eq(index).fadeIn(); current = index; } setInterval(function() { goTo(current + 1); }, 4000);这里有一个常见问题:
fadeIn和fadeOut同时执行时,元素可能在某个瞬间同时处于两个动画状态,视觉上会出现重叠闪动。解决办法是先stop(true, true)再执行动画,其中第一个参数会清空队列,第二个参数会直接跳到终点状态,这样不会因为连续点击造成动画堆积。3.4 前后端联调中 dataType 的细节坑
后端接口如果返回的是 JSON 字符串,但 Content-Type 设置成了
text/html或text/plain,jQuery 在dataType: 'json'时会先按字符串接收,再做 JSON.parse。这个过程也隐藏着一个兼容老 IE 的问题:老 IE 对 JSON.parse 支持不完整,严格模式的 JSON 字符串可以通过window.JSON.parse处理,但 jQuery 老版本里为了保证兼容,有时会用eval,如果 JSON 字符串里含有不规范内容(比如 undefined、NaN、注释),解析就会报错。所以遇到解析错误时,先检查后端输出:JSON 字符串必须是合法的
key: value对,不能有多余逗号,不能有 HTML 标签包裹,不能有 BOM 头。这类问题在浏览器控制台里看到“Unexpected token <”时,八成是响应里混了 HTML。联调时我习惯先
console.log(res),确认拿到的到底是什么,再决定下一步操作,而不是直接猜自己的代码写错了。4. 性能优化:老项目最容易忽略的四个瓶颈
很多人说 jQuery 慢,其实大部分项目里不是 jQuery 本身慢,而是用的人把性能写崩了。几个典型的性能问题都在下面。
4.1 选择器性能:慢在选择器写法,不是慢在 jQuery
前面说过,现代浏览器里
$()会走原生选择器,瓶颈往往在于你给它的表达式。比如
$('#list tr td a')会遍历多级结构,哪怕你用querySelectorAll执行,它也是一个层级一个层级去匹配。更快的方式是$('#list a.link'),直接锁定目标类型。再比如 jQuery 的伪类选择器,像
:eq(1)、:odd、:last这些,没法被原生 API 直接处理,jQuery 需要逐个遍历元素来过滤,开销比常规选择器大得多。这种场景我的方案是:先用原生选择器拿到集合,再用.eq()或 filter 的方式处理。4.2 重复绑定事件导致 handler 堆积
页面里如果用了类似“复用弹窗”的组件,每次打开弹窗前如果都做一遍
$('#btn').on(...),同一个按钮会被绑定 N 次 handler。点击一次,事件触发 N 次,弹窗或者请求就会执行多遍。防坑方式:
- 把绑定动作放在页面初始化时,只做一次
- 如果必须动态绑定,先
.off()再.on():
$('#btn').off('click').on('click', fn);.off()会把之前绑定的同名事件清掉,再挂新的事件,有效防重。特别是在写 ajax 动态渲染的列表,并且每条列表里有按钮时,如果用委托挂在最外层的容器上,就不会出现重复绑定问题,因为事件 handler 永远只有容器上的一个。
4.3 data() 缓存与内存泄漏
jQuery 的
.data()会把数据存在内部的缓存对象里,而不是直接挂在 DOM 属性上,这个设计效率很高、也不污染 DOM。但副作用是:如果你用.data()存了数据,然后直接remove()元素,数据并没有自动清干净,如果元素被反复创建销毁,缓存就会越来越大,形成内存泄漏。正确的做法是:
// 先移除 data,再移除元素 $(el).removeData(); $(el).remove();不过这个方法也有点讲究:
remove()实际上不会删除用$.data方式以外挂载的数据,比如直接el.foo = 1这种就不归它管。所以最佳习惯是,所有需要长期保存的数据,都统一用.data()存取,后续清理时也统一用.removeData()。页面里如果有定时器(
setInterval),在弹窗或组件销毁时一定要clearInterval。很多人写的轮播图,弹窗关闭了定时器还在跑,后台数据会一直请求或者 DOM 一直在操作,这在移动端会让你明显看到耗电。4.4 插件冲突与全局变量污染
jQuery 本身只暴露了
$和jQuery两个全局变量,但很多老插件会往$上挂一堆自定义方法。如果页面同时引用了多个插件,它们的命名恰好撞了,后引入的会覆盖先引入的。使用
jQuery.noConflict()可以释放$的控制权,但问题在于,后面你写的代码里就不能再用$了,得用jQuery,而别人的插件代码如果不自适应这个变化,一样会报错。实际项目里,我很少去动 noConflict,更多是把所有页面内代码统一包在一个 IIFE 里,并且用参数显式接收 jQuery:
(function($) { // 这里面的 $ 是局部变量,肯定指向 jQuery $(function() { // 页面初始化 }); })(jQuery);这样既不会和页面上其它全局变量冲突,也能保证
$是可用的。如果你的页面同时有多个版本的 jQuery(老插件遗留问题),可以给其中一个版本改名,比如 jQuery 3 保持为jQuery,老插件引用的版本用 noConflict 改为$jq1,但这种情况建议尽早升级插件和 jQuery 版本,多版本共存只是在拖延问题。4.5 版本选择:该用 1.x 还是 3.x
目前项目里常见的 jQuery 版本大致分三条线:
- 1.x:兼容 IE6/7/8,体积最大,很多老系统为了兼容老旧浏览器坚持使用
- 2.x:放弃了 IE6-8 兼容,属于过渡版本
- 3.x:现代版本,API 更清晰,体积小一些,不兼容老 IE
如果你维护的项目就是运行在现代浏览器里,直接升到 3.x 就好。如果客户的浏览器还停留在 IE8,那只能守住 1.x,千万别把 3.x 直接换上去,很多老代码会失效,特别是事件处理和属性选择器上的细节差异。
升版本时的注意点:先看项目里是否引用了 jQuery UI 或老插件,很多插件只兼容 1.x,升级后要逐个验证。
5. jQuery 与现代前端并存:迁移有策略
就算决定要用现代框架,也不意味着“一夜之间”把页面全部重写。绝大多数老系统是没办法整体重构的,业务逻辑复杂、测试覆盖低、人手不够,这时候小步迁移是更现实的选择。
5.1 判断哪些页面值得迁移
我通常按这几个标准筛选:
- 页面内 DOM 操作量极大,jQuery 代码出现了大段的事件绑定和状态同步,这类页面在 Vue/React 里会简单得多
- 页面有大量 ajax 回调嵌套,已经出现“回调地狱”的苗头
- 页面有复杂的表单联动逻辑,比如选择城市、切换选项需要联动刷新多个区域
- 页面不常改动,功能稳定,能不碰就不碰
低风险迁移从简单页面开始,比如以前的查询列表页、报表页,数据展示为主,交互轻,做起来快,也能积累迁移经验。
5.2 混用场景:不要让框架和 jQuery 抢 DOM
混用的核心原则是:jQuery 负责“非框架管理的区域”,框架负责“核心视图层”。
比如你可以在 Vue 的 mounted 里调用一段独立的 jQuery 代码,去初始化一个已有的轮播插件,但你不能在 Vue 的模板里写
$('.item').text()去改内容,那样 Vue 的虚拟 DOM 比较会被破坏,数据更新时你会看到页面闪一下或者渲染错乱。更稳的做法是加一个“隔离层”:
// 在页面初始化时,把 jQuery 初始化的部分放在 Vue mount 之后,并且不再让 Vue 管理这段逻辑 mounted() { this.initLegacyWidget(); }, methods: { initLegacyWidget() { $(this.$refs.wrap).find('.legacy-box').each(function() { // 这里照常使用 jQuery }); } }5.3 原生 API 替代方案
如果你想在项目里逐步去掉 jQuery,可以从这几个替代开始:
- 选择器:
document.querySelector/querySelectorAll - 事件:
addEventListener - 类名操作:
classList.add/remove/toggle - HTML 结构:
innerHTML - 异步:
fetch
举一个直接的对照例子:
// jQuery $('#btn').on('click', function() { $('#list li').addClass('active'); }); // 原生 document.querySelector('#btn').addEventListener('click', function() { document.querySelectorAll('#list li').forEach(function(li) { li.classList.add('active'); }); });你会在对比中感受到,jQuery 的“隐式迭代”帮我们省了大量 forEach,但这用原生也不是写不了,就是没那么简洁。
需要注意的是,原生
innerHTML拼接如果在循环里进行,会有一定的性能开销,推荐的做法是先用数组收集再 join,或者用 DocumentFragment。6. 常见问题排查实录
这部分整理的是我在真实项目中遇到最多的问题,每一条都有人踩过,排在前面的是优先级最高的。
6.1
$ is not defined的完整排查链路遇到这个报错,大多数人都知道是 jQuery 没引进来,但实际场景里原因可能有几种:
- 脚本引入顺序问题,你的代码跑在前面,jQuery 还没加载到
- CDN 地址写错或者被拦截,浏览器网络面板能看到请求失败
- 页面里引入了多个 JS 库,某个库调用 noConflict 把
$占用了 - 代码被包在某个模块作用域里,
$传不进来自定义函数
排查方法:先打开浏览器控制台输入
typeof jQuery,看看到底有没有 jQuery。有的话,再用typeof $比较。如果typeof $是undefined而typeof jQuery是function,说明确实被释放了,你要么改用jQuery,要么把代码包进jQuery(function(){...})里。6.2 动态添加的元素点击无效
这个问题几乎每周都会遇到一次。临时渲染到页面上的按钮,点击时 handler 不执行。原因就是我之前说的,直接绑定只在绑定那一刻生效。
排查时先确认是不是用了委托,如果已经用了委托但依然无效,检查一下选择器:委托的绑定元素
$('#container')本身是不是动态生成的?如果容器也是动态加的,那就把委托挂到更上层的静态祖先上。确保委托层级里,点击的 target 确实能匹配到选择器。最后一个容易忽略的点:元素上如果有
disabled属性,点击事件不会触发,而样式上看起来可能并不明显。用$('button').prop('disabled')检查一下是值得做的排查。6.3 返回数据明明有值,但
res.data是 undefined这种情况十有八九是数据类型判断错误。你拿到的是字符串,却按照对象在取属性,所以
res.data是 undefined。一个快速验证的办法:
console.log(typeof res, res);如果你看到
typeof res是 string,那就先JSON.parse,或者确认 ajax 的dataType是不是设置成了json。有时候后端返回的 JSON 带有 BOM 头,JSON.parse 会报错,处理办法一般需要后端配合去掉,前端也可以用
.replace(/^\uFEFF/, '')先清理。6.4 动画队列积累导致交互不跟手
连续 hover 或点击切换的动画元素,如果用户快速操作多次,元素会排队,明明只点了两次,动画却连续播三遍。前面说过,用
stop(true, true)解决,这里再补充一个经验:$(el).stop(true, true).fadeIn(400);注意第一个参数是
clearQueue,即清空队列,第二个参数是jumpToEnd,即直接跳到当前动画最终的终点状态。对于悬停展开/收起的菜单、轮播切换按钮这类场景,这个写法非常稳。如果你不想补完动画,跳过跳转直接清队列就不会出现闪烁的目标态。6.5 ready 与 load 混用导致元素拿不到
jQuery 的
$(function(){})等价于$(document).ready,它会在 DOM 构建完成后触发。但是如果你在脚本里用到了外部图像或者 iframe 的加载完成事件,就要用$(window).on('load', fn)。偶尔会遇到一个情况:在
$(function(){})里去读取了某个由图片库异步渲染的区域,结果拿到空节点,因为图片库还没执行完。排查时把读取逻辑移到load事件里,或者用setTimeout延后一次,多半能解决。但这只是权宜之计,正确做法还是等对应模块初始化完成再取节点。7. 写在最后的小经验
说点我自己这几年维护老项目的体会。开始接触 jQuery 时,总觉得它太“老派”,想赶紧用新框架重写一切。真做下来才发现,很多“老旧”的系统之所以还能稳定跑着,正是因为 jQuery 没有过多复杂的抽象层,代码虽然直白,但调试起来思路清晰。后来我慢慢习惯了在新项目中用框架,但遇到需要快速验证交互、写几个人项目工具页、处理一段历史遗留代码时,jQuery 还是我最先想到的工具。
最后分享一个非常实用的小技巧:当你拿到一个页面,想知道里面已经绑定了哪些事件时,jQuery 老版本可以直接用
$._data($('#btn')[0], 'events')查看事件队列,这在排查重复绑定问题时能省下大量时间。当然,3.x 环境就可能不行了,这时候就用浏览器 DevTools 的 Elements 面板,选中元素后查看 Event Listeners,同样能看到当前挂载的 handler。这个操作,我敢说很多写了五六年 jQuery 的人都未必注意过,但排查“使了没反应”“点了两次执行”之类的问题,几乎一摸一个准。