YAOTU INSIGHTS

jQuery杂项方法深度解析:从工具函数到避坑指南

jQuery杂项方法深度解析:从工具函数到避坑指南
做前端这些年jQuery 的“杂项方法”一直是我觉得最被低估的一块。大家一提到 jQuery第一反应是$(document).ready、动画、AJAX、选择器好像这套库的全部价值就这些。但实际写业务代码、维护老项目、甚至在新项目里借鉴它的一些思路时杂项方法反而成了我敲得最频繁的东西——$.each、$.extend、$.inArray、$.trim、$.param每一个单拎出来都很不起眼组合起来却几乎撑起了日常脚本的半边天。这篇文章我不打算面面俱到地把每个方法都抄一遍官方文档而是把我这些年实际用下来、觉得值得讲透的部分拆开聊哪些方法容易用错、哪些场景有替代方案、哪些坑是文档不会告诉你的。适合刚接触 jQuery 但已经能写简单交互的同学也适合那些明明用了好几年却始终没留意这些工具函数的老手。1. 杂项方法到底是什么为什么值得单独拎出来讲1.1 先给“杂项”正个名jQuery 官方文档里有个分类叫 “Utilities”中文常被翻译成“工具方法”或者“杂项方法”。这批方法有一个共同特征它们不依赖 DOM 操作也不直接绑定到某个元素集合上大部分挂在jQuery这个全局对象上形如$.xxx()。换句话说它们是纯 JS 层面的通用工具跟你选中了几个元素、要不要操作页面结构基本没关系。这一点恰恰是它们容易被忽略的原因。刚接触 jQuery 的人脑子里全是“选元素、改样式、绑事件”这个套路很少会停下来想如果我想把一个数组里的每个值都翻个倍、把一段字符串两头的空格去掉、把用户填的表单拼成查询串jQuery 能帮上什么忙实际上这些场景用的正是杂项方法。它们看起来不酷但写业务逻辑时几乎每天都要用。1.2 哪些方法被归进了杂项为了方便后面展开我先按用途把常见杂项方法分个组后文会挑重点逐个展开数组与对象遍历类$.each、$.map、$.grep、$.inArray、$.makeArray、$.merge、$.uniqueSort类型判断类$.type、$.isArray、$.isFunction、$.isNumeric、$.isEmptyObject、$.isPlainObject对象与数据处理类$.extend、$.param、$.trim、$.parseJSON老版本、$.nowDOM 关系判断类$.contains函数辅助类$.proxy、$.noop这些方法里有一部分在原生 ES5/ES6 里已经有了对应实现比如Array.prototype.forEach、Array.prototype.map、Object.keys、String.prototype.trim。但这不代表杂项方法就失去意义——在跨浏览器兼容需求仍然存在的老项目里在需要统一处理类数组对象和 NodeList 的场景里$.each和$.map的健壮性依然是很多原生写法比不上的。2. 数组与对象处理each、map、grep 的适用边界2.1$.each()与$().each()的本质区别这是我在实际开发中被问过最多的问题$.each和$().each到底有什么区别一个最直观的区分方式是看调用形式$.each(array, fn)是静态工具传什么就遍历什么传数组遍历数组传对象遍历对象$(li).each(fn)是实例方法它内部拿到的是当前 jQuery 对象里那一串 DOM 元素本质上是针对元素集合的便捷封装。真正值得留心的是$.each对对象和数组的“一视同仁”。普通数组用 for 循环遍历没问题但如果你拿到的是一个类数组对象比如arguments、getElementsByTagName返回的 HTMLCollection直接用Array.prototype.forEach往往要先做一次转换而$.each直接就能处理$.each(arguments, function(index, value) { console.log(index, value); }); $.each({ name: 张三, age: 30 }, function(key, value) { console.log(key, value); });它内部会根据obj.length是否存在来决定走数组式循环还是键值对循环这个自动判断帮我省去了大量“先判断是不是数组”的样板代码。有一点必须提醒$.each里的回调如果返回false会立即终止遍历。这个行为在新手代码里经常被误用——有人想在回调里“跳过当前项”就写return false结果整个循环全停了。跳过一项应该写return true或者什么都不返回只要不是 false终止整个循环才用return false。我第一次接手别人的代码时就见过因为这种误用导致列表只渲染了一半的 bug排查的时候很绕。2.2$.map()与$.grep()转换与过滤的分工$.map(array, fn)做的事情是“映射”把每个元素经过回调处理后收集成一个新数组返回。这里有个和原生Array.prototype.map不太一样的地方jQuery 版的$.map如果回调返回null或undefined对应位置会被直接删掉而不是像原生 map 那样保留一个空洞。这个特性有时候是好消息有时候是坏消息。好消息是你想“一边转换一边过滤”的时候特别顺写var nums [1, 2, 3, 4, 5]; var result $.map(nums, function(n) { if (n % 2 0) { return n * 10; } // 不返回奇数项自动被剔除 }); // result: [20, 40]坏消息是如果你按照原生 map 的习惯以为回调里不 return 会得到一个undefined占位那结果长度会跟你预期不一致。这个坑我踩过一次后来总结出的规律是纯粹的“变换”用$.map但要在回调理清楚“不返回就删除”这层语义如果需要严格保长建议先$.map再$.grep或者直接改用原生Array.prototype.map。$.grep(array, fn, invert)是专门做过滤的第三个参数可以控制是否反向筛选——传true时返回“不满足条件”的元素。这个参数我用得不多但偶尔写“排除列表”时确实方便可以省掉临时变量。2.3$.inArray()的使用细节$.inArray(value, array)返回的是找到的下标找不到返回-1。用法本身很简单但有个经典的“新手陷阱”if ($.inArray(苹果, list) ! -1) { // 存在 }问题在于很多刚从别的语言转过来的人习惯把判断写成if ($.inArray(value, list))这在英语语境下比如 PHP 的in_array是对的但在 jQuery 里下标为 0 时会被当成 false。我见过因为这个判断导致第一个元素永远“不存在”的案例排查了半天才发现是布尔判断的问题。所以写判断宁可啰嗦一点也要带上! -1或者用 0。另外$.inArray内部用的是严格相等不会做类型转换1和数字1是两个东西做表单值匹配时要注意源头数据的类型。2.4$.makeArray()与$.uniqueSort()$.makeArray(obj)可以把类数组对象转成真正的数组。以前转arguments最常用的写法就是Array.prototype.slice.call(arguments)在老浏览器上偶尔会有性能问题$.makeArray的语义更直白而且对 null、undefined 也做了容错——传进去会得到空数组至少不会直接抛错。$.uniqueSort()现在的名字叫的是 sort以前叫$.unique()专门用来给 DOM 元素数组去重并排序。注意它只对 DOM 元素有意义对普通字符串数组、数字数组使用不会有预期的去重效果。如果你要给普通数组去重还是用new Set()或者自己写循环更靠谱。这个适用范围的区别文档里写得不显眼但用错场景的人真不少。3. 类型判断那一摊子事type、isArray、isNumeric 背后的门道3.1$.type()比typeof强在哪typeof的短板大家都熟typeof null是objecttypeof []也是object遇到正则、日期这些内置对象时更是分不清。$.type()用内部Object.prototype.toString配合正则解析能给出更精细的结果$.type(null); // null $.type([]); // array $.type(new Date()); // date $.type(/abc/); // regexp $.type(function() {}); // function $.type(undefined); // undefined说实话这个工具在原生 ES6 环境下已经可以被Array.isArray、Object.prototype.toString直接替代但在兼容老浏览器的项目里$.type依然是写类型分支判断最省事的一招。我自己写代码的习惯是判断是不是数组用$.isArray或原生Array.isArray判断一个对象是“什么特殊类型”时才用$.type。这里顺便提一个和$.type互补的方法$.isPlainObject()它判断的是“这个对象是不是纯粹由字面量或new Object()创建出来的普通对象”。这个判断在传参校验里特别有用——很多时候我要区分“用户手动构造的对象”和“类的实例、DOM 节点、window 这类特殊对象”$.isPlainObject可以直接帮我把后几种排除掉。3.2$.isNumeric()的边界情况$.isNumeric(value)这个方法名看起来很直白判断是不是数字但它的实际行为比字面意思宽不少。它内部做的是把传入值转成数字再判断转换结果是不是有限数。所以$.isNumeric(123); // true $.isNumeric(123); // true字符串数字也算 $.isNumeric(0xAF); // true十六进制字符串也能解析 $.isNumeric(); // false $.isNumeric(false); // false $.isNumeric(null); // false我踩过的一个坑是用$.isNumeric(input.val())判断输入框内容是不是数字时发现负数、小数、带空格的情况表现都正常但如果输入框是空的返回 false这块没问题真正容易出问题的是用户输入了科学计数法或者像1e3这种$.isNumeric(1e3)是 true。做金额校验时如果没留意会被这种输入带偏。另外一个暗坑是$.isNumeric([1])、$.isNumeric(new Number(1))结果都是 true因为这些都能成功转成数字。文档不会把每个边界都列给你批量处理数据前最好用几组脏数据实测一下。3.3$.isEmptyObject()的真实场景$.isEmptyObject({})判断对象是否没有自身可枚举属性。有人会问这不就是Object.keys(obj).length 0吗大部分场景确实等价但$.isEmptyObject额外处理了toString这类不能被 for-in 枚举的属性并且对节点对象、window 对象的行为做了特殊修正。在老项目中写“当前对象是否为空配置”时直接用这个方法最稳。另外一个常用搭配是$.isFunction()。用它判断回调参数是否传了函数比typeof fn function多一层跨窗口iframe的容错——不同 window 之间函数原型不同typeof不会出错但instanceof Function会$.isFunction走的是$.type的路径在这类场景下更稳。以前做多 iframe 的入口脚本时这个差异是真实存在过的。4.$.extend()对象合并的利器与深坑4.1 深拷贝与浅拷贝一个参数之差$.extend(target, source1, source2, ...)把后面的对象依次合并到第一个对象上。如果第一个参数传入true就会变成深拷贝模式var a { name: 张三, info: { age: 30, city: 北京 } }; var b {}; $.extend(true, b, a); b.info.age 31; console.log(a.info.age); // 30深拷贝后互不影响浅拷贝模式下b.info和a.info指向同一个对象改其中一个另一个也会变。这个区别我讲过很多次但每次还是要提醒业务里凡是把配置对象做备份、传给后端的逻辑一律用深拷贝不然改来改去改到源对象上排查半天都不知道哪来的副作用。$.extend也是经典的 options 默认值合并方案。写插件时最常用的套路是function createWidget(options) { var settings $.extend({}, defaultOptions, options); // settings 就是合并后的最终配置 }把空对象放第一个既不改动 defaultOptions也不改动用户传入的 options避免污染默认配置让后续调用出问题。这个习惯比直接往默认对象里塞值要安全得多。4.2 必踩的坑第一个坑是深拷贝在遇到循环引用时会爆栈。比如a.self a;这样的结构$.extend(true, {}, a)会无限递归下去最终报栈溢出。实际业务中很少构造循环引用但解析一些 JSON 图结构数据时对象互相指向的情况还是存在这时候要提前判断别让数据把页面卡死。第二个坑是深拷贝对函数和正则的处理。$.extend(true)不会递归函数函数是引用直接拷贝的这是符合预期的但正则对象的处理在不同版本里有过差异如果你要拷贝的对象里带着 RegExp建议单独写处理逻辑别完全依赖 deep extend。第三个坑是$.extend会改变第一个参数对象本身。有时候我只是想生成一份合并后的副本结果一不留神把某个源头对象给改了。所以再次强调那个空对象占位习惯这个习惯能挡掉大部分这类问题。5. 工具函数里的扫地僧param、noop、trim、contains5.1$.param()把对象变成查询字符串$.param(obj)做的事是把对象序列化成keyvaluekey2value2格式的字符串。以前做 AJAX GET 请求拼接参数时手写字符串拼到崩溃后来发现其实一行就能搞定var params { name: 张三, tags: [前端, jQuery], age: 30 }; $.param(params); // name%E5%BC%A0%E4%B8%89tags%5B%5D%E5%89%8D%E7%AB%AFtags%5B%5DjQueryage30它自动处理里 URL 编码数组会生成带[]后缀的键。这个序列化规则和后端常见的解析方式是对得上的所以拿它做表单多字段提交相当省心。如果用的是$.ajax直接传 data 对象也是一样的效果内部走的就是$.param。不过$.param对嵌套对象的序列化比较扁平比如{ user: { name: 张三 } }会变成user%5Bname%5D...也就是方括号语法。大部分后端框架能解析这种格式但也有少数接口只认简单键值对对接前最好先确认一下对方协议。5.2$.noop()空函数也讲究$.noop()返回一个什么都不做的函数。很多人不理解一个空函数有什么好封装的但实战里它太常用了。最典型的场景是给回调参数设置默认值插件里写options.onComplete options.onComplete || $.noop;之后后续就可以放心直接调用options.onComplete()而不用每次判断是否存在了。还有做 A/B 测试开关时把“不启用”分支的回调直接设为$.noop代码结构能保持统一。5.3$.trim()老项目里的顺手工具$.trim(str)去掉字符串首尾空白。现在原生String.prototype.trim已经全面普及这个方法的必要性大大降低但有两种情况我还是会用它一是代码风格统一、不想在个别老文件里混用新旧 API二是在低于 IE9 的兼容修复场景现在基本碰不到。写原生.trim()遇到null、undefined会直接报错这一点上$.trim(null)会返回空字符串而不是抛错容错性更好所以批量清洗数据时反而比原生写法更省事。5.4$.contains()判断 DOM 的包含关系$.contains(parent, child)返回布尔值判断一个节点是不是另一个节点的后代。这个看起来小众但做下拉菜单“点击外部关闭”的时候非常有用——我判断点击目标是否在菜单容器内直接$.contains(menu, target)或者menu target一起判断比一层层parentNode往上跑干净得多。还有一个容易混淆的地方$.contains只判断 DOM 节点关系不判断两个 jQuery 对象使用时需要传原生节点用$el[0]取出来再传进去。6.$.proxy()绑定上下文的正确姿势6.1 它到底解决了什么问题$.proxy(fn, context)返回一个新函数这个函数无论被谁调用内部的this都指向 context。说直白点它就是把函数和对象“绑死”的胶水。最常见的场景是事件回调。比如有个对象var widget { name: 计数器, init: function() { $(#btn).on(click, function() { this.name; // 这里的 this 是按钮 DOM不是 widget }); } };如果我想在点击回调里访问widget.name传统写法是var self this;存一下或者用箭头函数或者用$.proxy。$.proxy的好处是语义清晰$(#btn).on(click, $.proxy(function() { console.log(this.name); }, this));。在箭头函数普及之前这是 jQuery 插件代码里最常见的绑定方式之一至今在老代码里看到$.proxy也非常正常。6.2 什么时候要用什么时候别用需要留意的是$.proxy的绑定是永久的。如果你做一个可复用的回调函数在不同元素上都绑这个函数但 context 不同千万注意别互相覆盖。另外在解绑事件时$.proxy出来的新函数和原函数不是同一个引用所以绑定和解绑必须用同一个$.proxy返回的函数否则off()解不掉。我自己习惯的做法是先把代理函数存成变量var handler $.proxy(this._onClick, this); $(#btn).on(click, handler); // 后面解绑 $(#btn).off(click, handler);现在写新代码我更多是用箭头函数替代$.proxy但读老项目时还是得认得出这套写法别看到$.proxy一头雾水。7. 常见坑排查速查表与实操心得7.1 高频问题速查我把这些年遇到比较典型的杂项方法问题整理成一个表方便以后遇到类似情况直接对照现象常见原因处理方案$.each只遍历了一半就停了回调里误用了return false把跳过写成了终止想跳过当前项用return true确认是业务层需要再用 false 终止$.map返回的数组长度比原数组短回调里某些分支没 returnjQuery 会把 null/undefined 项剔除需要保长时改用原生Array.prototype.map判断$.inArray结果时第一个元素永远不成立直接拿返回值当布尔判断下标 0 被转成 false统一写成! -1或 0深度合并大对象时页面卡死对象存在循环引用$.extend(true)无限递归合并前检测循环引用或只序列化无环的部分修改副本对象后原对象也变了浅拷贝只复制了引用内嵌对象还是同一份需要独立数据时用$.extend(true, {}, source)$.contains判断不生效传入的是 jQuery 对象而不是原生节点用$el[0]取出原生节点再传绑定事件后解绑无效$.proxy返回新函数直接传原函数解绑对不上代理函数存变量绑定和解绑用同一引用$.isNumeric对“学计数法字符串”误判内部能把1e3之类的字符串转成数字严格校验时先做正则或额外类型判断7.2 几条实操心得写到这里我想把个人经验里最值的几条心得收个尾。第一杂项方法不是只是老古董的遗产。即使现在原生 API 已经很强大$.extend的深拷贝、$.isPlainObject的语义、$.param的序列化规则在今天写工具函数时仍然有参考价值。我在新项目里写自己的deepClone、isPlainObject时参考的框架逻辑很多就是从 jQuery 里演化来的。第二读库源码是提升调试能力的好路径。$.type的实现只有几行$.extend的核心循环也不复杂。某次我排查线上问题时卡了很久最后就是翻开 jQuery 源码顺着$.each的具体分支逻辑才找到根因。源码比文档更能回答“为什么”。第三用之前先确认数据形状。杂项方法大多容错性不错但容错不等于正确。处理用户输入或接口返回的数据前先打印一次$.type(data)看清它是数组、字符串还是类数组对象再决定用哪个方法。这一步能省下后面大量的console.log调试时间。最后想说做技术排查时别急着给 jQuery 判死刑。这套库虽然时代感强了些但它沉淀下来的工具函数设计放到现在依然是很好的参考教材。理解这些方法背后的边界和取舍远比单纯背 API 列表有价值。