写 jQuery 写了两三年,见过不少新同事第一个卡壳的地方不是复杂插件,反而是最基础的三个概念:$到底是什么、document.getElementById拿到的对象和$('#id')拿到的对象差在哪、为什么有时候能直接.val(),有时候又要[0]一下。这套对象体系不搞明白,写出来的代码经常是“一半能跑一半报错”,排错都要多花一倍时间。
今天就把这套东西彻底理干净:先说$顶级对象,再说 DOM 对象和 jQuery 对象的本质差异,最后把互相转换的几种写法和典型坑一次性讲透。内容偏向实战,适合刚接触 jQuery 的新人,也适合平时写原生 JS、偶尔要维护老项目的人,看完可以直接照着抄。
1. 先把三个角色认清楚:$、DOM 对象、jQuery 对象
1.1 顶级对象 $ 到底是什么
你可以在浏览器控制台里直接敲一行代码验证一下:
console.log(window.jQuery === window.$); // true console.log(typeof $); // function$不是独立的新语法,也不是什么神秘符号,它就是 jQuery 库向外暴露的全局函数,和jQuery完全等价。你平时写的$('#id')本质是调用了jQuery('#id'),两者没有任何区别。
可以这样理解:$是 jQuery 所有功能的“总入口”。不管你是要选元素、遍历集合、绑定事件,还是调用$.ajax、$.each这类工具方法,都要通过这个入口进去。它同时具备两种身份:
- 作为函数调用,比如
$('div'),负责把选择器或 DOM 节点包装成 jQuery 对象; - 作为对象挂载属性,比如
$.ajax、$.trim、$.fn,这些是 jQuery 直接暴露的静态方法。
用个生活化类比:$就是一个服务窗口,你递上什么材料(选择器字符串、DOM 对象、函数、HTML 片段),它给你开出对应的“回执(jQuery 对象)”。窗口本身还挂着各种办事指南(静态工具方法)。
很多初学者误以为$只能用来选元素,这是最常见的误解。其实$(document).ready是在监听 DOM 就绪,$(function(){})是它的简写,$('<div>')是动态创建节点,$(window).scroll是给原生对象直接挂事件。这些都是$作为函数入口的体现。
1.2 DOM 对象与 jQuery 对象的本质差异
DOM 对象是浏览器原生的东西。你用document.getElementById、document.querySelector、element.childNodes这些原生 API 拿到的,就是 DOM 对象。它可以直接操作innerHTML、style、value、textContent这些属性,也可以用addEventListener绑定事件。
jQuery 对象则是$()把 DOM 对象“包装”之后得到的增强型对象。它内部其实是一个类数组结构,把匹配到的若干 DOM 节点按顺序塞进数字索引里,同时挂上length属性,再把 jQuery 原型链上的一大堆方法(.css、.on、.hide、.addClass这些)都暴露给你用。
两者的核心差异用一张表就能说清:
| 对比项 | DOM 对象 | jQuery 对象 |
|---|---|---|
| 获取方式 | document.getElementById、querySelector等原生 API | $('选择器')、$(DOM对象) |
| 本质类型 | 原生对象,可能是HTMLElement、Text节点等 | Object类型的类数组对象 |
| 方法体系 | 只能调用 DOM 原生方法 | 只能调用 jQuery 方法 |
| 链式调用 | 通常不支持 | 支持,一行代码连续操作 |
| 批量操作 | 需要自己循环 | 内部自动遍历匹配到的所有节点 |
| 是否带 length | 单个元素没有,NodeList有 | 有,且带数字索引 |
一个常见误用就是混着调方法。看过不少代码在写$('#id').value,结果拿到的是undefined;也有人写document.getElementById('id').val(),直接报错。原因就是两种对象的方法体系完全不同。
再补一个容易忽略的点:jQuery 对象长得像数组,但它不是Array实例。你不能直接在它身上用forEach、push、pop这些原生数组方法,它内部实现了一套自己的迭代方法(.each、.map等)。知道这一点之后,你就能理解为什么很多人转数组时要用.toArray()或者Array.prototype.slice.call()。
2. 拆开 $ 的包装:从调用过程看对象生成
2.1 $() 内部是如何“new”出来的
很多教程会告诉你“$()是一个构造函数”,这话不准确,但也不是完全错。jQuery 源码里有一段极简又很关键的逻辑,类似下面这样:
jQuery = function(selector, context) { return new jQuery.fn.init(selector, context); };也就是说,你调用$('li')的时候,内部实际执行的是new jQuery.fn.init(selector, context)。关键在于后面这几行源码的精妙配合:
jQuery.fn = jQuery.prototype; jQuery.fn.init.prototype = jQuery.fn;jQuery.fn就是jQuery.prototype,而init.prototype又指回jQuery.fn。这个循环引用让new init()创建出来的对象虽然构造函数是init,但它能沿着原型链访问到jQuery.prototype上挂载的所有方法。
这也是为什么你能写出$('li').css('color', 'red').addClass('active')这种连续调用——因为每一次方法执行完返回的仍然是 jQuery 对象,链式调用才能成立。同时,$.fn暴露给外部插件作者,大家写$.fn.myPlugin = function(){},本质上就是在往 jQuery 的原型对象上添加新方法。
理解这一步对排查问题很有帮助:如果你在控制台打印$('div'),看到的是一个看起来像数组、但构造函数不是Array的对象,这是正常的,不是写错了。
2.2 类数组结构带来的便利与限制
jQuery 对象内部结构大致是:
{ 0: div.box, 1: div.item, length: 2, selector: "div", context: document, // 原型上的各种方法... }数字索引直接指向原生 DOM 节点,这就是为什么$('div')[0]能拿到原生 DOM 对象。便利在于:你既能享受 jQuery 方法的便捷,又能随时通过索引“钻回”原生世界。
限制也同样明显。下面的代码会报错:
var $lis = $('li'); $lis.forEach(function(li) { // TypeError: $lis.forEach is not a function // ... });因为你拿到的不是真正的数组,数组方法不存在。要遍历 jQuery 对象,正确姿势是:
$lis.each(function(index, element) { // this 指向当前 DOM 元素 });或者先转成数组再用原生方法:
$lis.toArray().forEach(function(li) { // li 是 DOM 元素 });想判断一个变量到底是不是 jQuery 对象,可以用变量 instanceof jQuery,更靠谱的写法是检查它的jquery属性,jQuery 内部会为每个对象挂一个类似"3.7.1"的版本字符串:
if (obj && obj.jquery) { // 这是 jQuery 对象 }这些细节平时用不到,但一旦遇到“为什么我的对象没有 .each 方法”“为什么 forEach 不可用”这种问题,就能快速定位到类型上。
3. jQuery 对象与 DOM 对象互相转换的完整操作
3.1 DOM 转 jQuery:$(原生节点) 的注意事项
把 DOM 对象转成 jQuery 对象的写法非常统一,就是把原生对象直接丢进$()里:
var domBox = document.getElementById('box'); var $box = $(domBox); // 也可以直接传 this、事件对象里的 target、window、document $(this).addClass('active'); $(e.target).hide(); $(document).on('click', handler); $(window).scroll(function() {});转换的意义在于:包装之后就能调用 jQuery 的方法和链式操作,比如下面这段代码,用原生写法要手动维护 class,用 jQuery 就一行:
// 原生:需要自己写字符串拼接和替换逻辑 domBox.className = 'box active'; domBox.className = domBox.className.replace('active', '');// jQuery:转换后原地起飞 $(domBox).addClass('active').removeClass('active');注意几个边界情况。$(null)和$(undefined)不会报错,返回的是一个空的 jQuery 对象,length为 0。这在写通用函数时是件好事,你不需要每次做空值判断。别对已经是 jQuery 对象的变量再包一层$(),比如$( $box ),这几乎没有意义,还可能得到预料之外的结构。如果你不确定某个变量是不是已经包装过,就用前面说的obj.jquery判断一下再决定要不要转换。
3.2 jQuery 转 DOM:索引、get()、toArray()
往回转同样简单,有三种常用方式:
var $div = $('div'); // 第一种:数组索引 var dom1 = $div[0]; // 第二种:get() 方法,等价于索引 var dom2 = $div.get(0); // 第三种:toArray() 转成真正的数组 var domArray = $div.toArray();get()有个小绝活:支持负数索引,$div.get(-1)拿到的就是集合里最后一个元素,原生数组的arr[arr.length - 1]写起来繁琐多了。
var $items = $('.item'); var first = $items.get(0); var last = $items.get(-1);toArray()返回的是真正数组,拿到之后可以放心用forEach、filter、map这些原生方法,适合和现代 JS 风格混写:
$('li.visible').toArray().filter(function(li) { return li.innerText.indexOf('关键词') !== -1; }).forEach(function(li) { li.style.color = 'red'; });还有一点要注意,jQuery 对象里如果没有任何匹配项,$div[0]的结果是undefined。取索引时需要先判断一下,不然容易在后续访问dom.innerHTML时炸出Cannot read properties of undefined。
3.3 转换前后行为差异对照
转来转去容易糊涂,我把常见操作在两种对象下的写法对照放在一起,方便你照着写:
| 操作 | DOM 对象写法 | jQuery 对象写法 |
|---|---|---|
| 读取文本 | dom.textContent | $obj.text() |
| 设置文本 | dom.textContent = '你好' | $obj.text('你好') |
| 读取 HTML | dom.innerHTML | $obj.html() |
| 设置 HTML | dom.innerHTML = '<b>加粗</b>' | $obj.html('<b>加粗</b>') |
| 读取表单值 | dom.value | $obj.val() |
| 设置表单值 | dom.value = 'abc' | $obj.val('abc') |
| 设置样式 | dom.style.color = 'red' | $obj.css('color', 'red') |
| 添加类 | dom.classList.add('active') | $obj.addClass('active') |
| 删除类 | dom.classList.remove('active') | $obj.removeClass('active') |
| 绑定事件 | dom.addEventListener('click', fn) | $obj.on('click', fn) |
| 解绑事件 | dom.removeEventListener('click', fn) | $obj.off('click', fn) |
看一个混合使用的完整示例,你会更直观地感受到为什么要转换:
var $btn = $('#submit'); $btn.on('click', function() { // 回调里 this 是原生 DOM 按钮 $(this).prop('disabled', true); // 先转 jQuery,再调用 prop var inputDom = document.getElementById('username'); // 原生获取 var value = $(inputDom).val(); // DOM 转 jQuery 后取值 if (!value.trim()) { alert('用户名不能为空'); $(this).prop('disabled', false); } });这个场景很典型:事件回调里拿到的this是 DOM 对象,你需要$(this)转成 jQuery 对象才能舒服地链式操作。而页面里有原生 JS 代码时,你又要用$(inputDom)把原生对象包装起来,让代码风格统一。
4. 真实项目中为什么必须掌握互转
4.1 混写原生 JS 与 jQuery 的衔接
维护老项目时最常见的情况是:页面里既有原生 JS 写的模块,又有基于 jQuery 写的组件,两者经常要协作。比如一个列表点击高亮功能,监听部分用原生写,视觉反馈用 jQuery 写:
document.querySelectorAll('.nav-item').forEach(function(item) { item.addEventListener('click', function() { $(this).addClass('active') // 转换后进入 jQuery 链式 .siblings() .removeClass('active'); }); });如果不转换,this上压根没有addClass、siblings这些方法,只能自己手动写一堆classList操作,代码立刻膨胀。反过来,如果你在用 jQuery 组件,组件内部回调需要把数据传给原生函数,也要先取出 DOM 对象再传。
还有一种代码洁癖问题:有人会在一个项目里一会儿用 jQuery、一会儿用原生 API,互相之间还不转换,最终结果就是控制台一堆xxx is not a function。掌握互转之后,原生与 jQuery 就能无缝衔接,你完全可以根据模块需求选择最顺手的一套。
4.2 插件回调与第三方控件中的对象陷阱
jQuery 生态里很多插件的回调函数会给你一个 DOM 对象,而不是 jQuery 对象,这是最容易踩的坑。举个例子,用 jQuery 的each遍历列表:
$('li').each(function(index, element) { // 这里的 this 和 element 都是原生 DOM 元素 console.log(this === element); // true $(this).css('margin-bottom', '10px'); // 必须先转换才能调 css });很多新手拿到element之后直接写element.text('内容'),报错后才反应过来。这不是偶然,插件的回调设计通常遵循原生优先原则,确保低耦合。
再看服务端控件场景,比如老项目里 ASP.NET 的 GridView 生成出来的元素 ID 经常是ctl00_ContentPlaceHolder1_gvList_btnEdit这种带复杂前缀的名字,用$('#ctl00_...')选择特别痛苦。常规做法是给控件加ClientIDMode="Static"让 ID 固定,或者用属性选择器避开 ID:
$('[id$="btnEdit"]').on('click', function() { // $= 表示属性以指定字符串结尾,这个技巧老开发者都在用 });这些场景都说明一个道理:只要你在真实项目里和 jQuery 打交道,就得习惯“回调给你什么对象、你需要什么对象、怎么转过去”这个思维流程。
4.3 性能视角:批量操作时缓存和转换的取舍
有人担心每次$()包装都有性能损耗,于是干脆否定 jQuery。实际测试下来,现代浏览器里创建 jQuery 对象的开销并不大,真正影响性能的是频繁操作 DOM 引起的重排重绘。但循环里反复写$('#list li')这种选择器仍然不推荐,因为每次都会重新走一遍选择器查找流程。
更合理的做法是:循环外缓存好 jQuery 对象,循环里直接用;如果只改原生属性,就直接操作 DOM 对象,省去中间包装:
// 不推荐的写法:循环里反复查询 for (var i = 0; i < 100; i++) { $('#list li:eq(' + i + ')').css('display', 'none'); } // 推荐:先生成 jQuery 集合,再用索引取原生 DOM 操作 var $allItems = $('#list li'); var domItems = $allItems.toArray(); for (var i = 0; i < domItems.length; i++) { domItems[i].style.display = 'none'; }批量处理复杂样式时,原生操作更快;需要链式组合、事件绑定、动画时,jQuery 对象更方便。转换的价值就在这里:它不是让你二选一,而是让你在两种能力之间自由切换,用最合适的工具干最合适的活。
5. 常见坑与排查心得
5.1 新手最容易碰到的 4 个对象错误
我整理了平时答疑时出现频率最高的四类问题,每一条都配了正确写法。
第一类:对 jQuery 对象调用原生属性。
$('#username').value; // undefined,jQuery 对象没有 value 属性 // 正确:$('#username').val() 或 $('#username')[0].value第二类:对 DOM 对象调用 jQuery 方法。
document.getElementById('username').val('abc'); // TypeError // 正确:$(document.getElementById('username')).val('abc')第三类:直接用==比较两个 jQuery 对象。
$('#a') == $('#b'); // 永远 false,因为每次 $() 都创建新对象 // 正确:$('#a')[0] === $('#b')[0],或用 .is() 比较第四类:在each回调里忘了this是 DOM 对象。
$('p').each(function() { $(this).css('color', 'red'); // 少了$()会报错 });这四类错误报错信息各不相同,但根子都在对象类型的认知上。如果你遇到“明明方法存在,却说 not a function”或者“取到的值是 undefined”这类诡异问题,第一反应就应该是查一查对象类型。
5.2 判断对象类型的几种稳写法
写通用组件时,经常需要兼容“传入的是 DOM 对象还是 jQuery 对象”。给你几种判断方式:
function myUtil(elem) { if (!elem) return; if (elem.jquery) { // 已经 jQuery 对象,直接用 } else if (elem.nodeType) { // DOM 元素,包装一下 elem = $(elem); } else if (elem instanceof NodeList) { // 一个节点集合,可以转成数组或者直接 $() elem = $($.makeArray(elem)); } }这里的关键点是nodeType属性,DOM 元素节点都有这个属性(元素节点为 1),而 jQuery 对象没有。instanceof jQuery虽然也能用,但跨框架、多版本环境里偶尔会失灵,elem.jquery这种鸭子类型判断更稳。
5.3 老话题:jQuery 还有必要学吗
说实话,新项目选型我不会强推 jQuery,现在原生querySelector、classList、fetch已经非常强大。但“有没有必要学”要看你处在什么环境:老项目维护绕不开,许多开源项目和插件源码里全是 jQuery 的写法;就算你是新项目,理解“框架对象与原生对象之间的转换”这个思维也很有用。
Vue 里有ref和原生 DOM 的区分,React 里有event.target和合成事件的界限,Angular 里也有ElementRef与原生节点的关系。你会发现,只要框架封装了 DOM,就一定存在“包装对象和原生对象”的转换问题。jQuery 教给你的不是具体的 API 记忆,而是一套对象体系的抽象思维,这种能力是跨框架迁移的。
在这个前提下,我仍然建议新同学花时间学一学 jQuery 的对象体系,特别是今天讲的这东西。不是为了回到 jQuery 时代,而是为了在老代码、新框架之间游刃有余。
6. 两个高频场景的小实操
6.1 快速获取第一个子元素的几种写法
日常开发里“取某个元素的第一个子元素”用得非常频繁。很多同学分不清:first-child、:first和.first()三者区别,这里一次说清。
:first-child是 CSS 伪类,匹配“作为父元素第一个子节点”的元素。你可以直接放进选择器:
$('ul li:first-child').addClass('highlight');:first是 jQuery 扩展伪类,取的是整个匹配集合的第一个:
$('ul li:first').addClass('highlight'); // 相当于 $('ul li').first()区别在哪?:first-child可能匹配多个元素,比如页面里有一堆ul,每个ul的第一个li都会被命中;:first永远只返回匹配集合里的第一项。
如果已经拿到父元素,更推荐用children().first():
$('#list').children().first().addClass('active'); $('#list').children(':first-child').addClass('active'); // 效果相似实际项目中我习惯先看语义:要“所有父元素下各自的第一项”,用:first-child;要“整个集合里的第一个”,用.first()或:first。
6.2 表格单元格过长时鼠标悬浮展示完整内容
这也是网上搜得很多的场景:表格列宽固定,内容太长被截断,鼠标悬停后想看完整数据。
最简单的方案是设置title属性,但样式不可控,只能看系统默认提示,而且一点就没了。想更可控就用 jQuery 做个悬浮层。思路如下:
给要截断的单元格套一个样式:
.cell-overflow { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }然后 jQuery 绑定hover事件,动态生成一个跟随鼠标的提示浮层:
$(document).on('mouseenter', '.cell-overflow', function(e) { var fullText = $(this).text().trim(); if (!fullText) return; var $tips = $('<div class="cell-tips"></div>') .text(fullText) .css({ position: 'fixed', top: e.pageY + 12 + 'px', left: e.pageX + 12 + 'px', background: '#333', color: '#fff', padding: '6px 10px', borderRadius: '4px', fontSize: '12px', zIndex: 9999, maxWidth: '400px' }) .appendTo('body'); $(this).data('tips', $tips); }).on('mouseleave', '.cell-overflow', function() { var $tips = $(this).data('tips'); if ($tips) { $tips.remove(); $(this).removeData('tips'); } });如果你用的是 DataTables 这类插件,回调里拿到的td或cell元素经常是 DOM 对象,操作前记得先$(cell)包一层,这就又回到今天的主角——对象转换上了。整个思路不复杂,核心就是“把原生的鼠标行为转成 jQuery 事件,再用 jQuery 动态创建节点、定位、销毁”。
我在实际项目里验证过,这个方案在大数据量表格下也稳定,因为用的是事件委托,单元格多也不会卡。后来为了更规范,我把浮层内容改成了从>
Aeron Samples 实战指南:从 Basic Pub/Sub 到吞吐量与延迟测试的完整工具箱
消息队列后端通信 【免费下载链接】aeron Efficient reliable UDP unicast, UDP multicast, and IPC message transport 项目地址: https://gitcode.com/gh_mirrors/ae/aeron 点击查看 免费下载 本指南以 aeron-samples/README.md 为骨架,系统讲解 Aero…
DeepSpeed 高级安装指南:Ops 预编译、多节点分发与 GPU 架构定制
推理引擎大模型 【免费下载链接】FlexGen Running large language models on a single GPU for throughput-oriented scenarios. 项目地址: https://gitcode.com/gh_mirrors/fl/FlexGen 点击查看 免费下载 DeepSpeed 在训练与推理时依赖一组 C/CUDA 扩展࿰…
自建CRM实战:DeskcommCRM部署、权限管理与数据安全指南
做销售管理的朋友,大概率都动过“自己搞一套CRM”的念头,尤其是当你发现市面上的免费CRM越用越别扭,收费CRM又贵得肉疼的时候。我团队之前就卡在这个点上,客户资料散在好几个人的微信和Excel里,月底统计全靠人工对表&a…
键鼠映射到虚拟手柄:QKeyMapper+ViGEm实现PC模拟器手柄游戏的完整教程
键鼠映射到虚拟手柄:QKeyMapperViGEm实现PC模拟器手柄游戏的完整教程 【免费下载链接】QKeyMapper [按键映射工具] QKeyMapper,Qt开发Win10&Win11可用,不修改注册表、不需重新启动系统,可立即生效和停止。支持游戏手柄映射到键…
Nunjucks API 完全指南:从简化渲染到自定义标签的模板引擎实战
模板引擎 【免费下载链接】nunjucks A powerful templating engine with inheritance, asynchronous control, and more (jinja2 inspired) 项目地址: https://gitcode.com/gh_mirrors/nu/nunjucks 点击查看 免费下载 本篇技术指南围绕 Nunjucks(Jinja2…
Sliver 中的 Beignet:将 macOS dylib 转换为 ARM64/x86_64 PIC Shellcode 的完整技术指南
网络安全 【免费下载链接】sliver Adversary Emulation Framework 项目地址: https://gitcode.com/gh_mirrors/sl/sliver 点击查看 免费下载 Beignet 是 Sliver Adversary Emulation Framework 在 macOS 平台上生成 shellcode 的核心转换库,其作用相当于…