news 2026/9/25 8:03:52

jQuery对象与DOM对象互转:本质差异与实战避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
jQuery对象与DOM对象互转:本质差异与实战避坑指南

写 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('你好')
读取 HTMLdom.innerHTML$obj.html()
设置 HTMLdom.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 动态创建节点、定位、销毁”。

我在实际项目里验证过,这个方案在大数据量表格下也稳定,因为用的是事件委托,单元格多也不会卡。后来为了更规范,我把浮层内容改成了从>

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 8:03:07

DeepSpeed 高级安装指南:Ops 预编译、多节点分发与 GPU 架构定制

推理引擎大模型 【免费下载链接】FlexGen Running large language models on a single GPU for throughput-oriented scenarios. 项目地址&#xff1a; https://gitcode.com/gh_mirrors/fl/FlexGen 点击查看 免费下载 DeepSpeed 在训练与推理时依赖一组 C/CUDA 扩展&#xff0…

作者头像 李华
网站建设 2026/9/25 8:00:36

自建CRM实战:DeskcommCRM部署、权限管理与数据安全指南

做销售管理的朋友&#xff0c;大概率都动过“自己搞一套CRM”的念头&#xff0c;尤其是当你发现市面上的免费CRM越用越别扭&#xff0c;收费CRM又贵得肉疼的时候。我团队之前就卡在这个点上&#xff0c;客户资料散在好几个人的微信和Excel里&#xff0c;月底统计全靠人工对表&a…

作者头像 李华