1. 先说清“杂项”到底是些什么方法
第一次翻 jQuery 文档的人,多半会在侧边栏里看到“杂项方法”那个归类,里面的方法看起来不像.on()、.ajax()那样目标明确,倒像是一堆“不知道往哪放”的工具函数。比如$.each、$.map、$.extend、$.grep、$.trim、$.type,还有$.inArray、$.makeArray这些。我当初刚接触时也犯过嘀咕:这些也能算一类?用多了之后才意识到,正是这些不起眼的工具函数,构成了整个 jQuery 生态的“基础设施”。
所谓“杂项方法”,我们通常叫它们 jQuery 工具函数,英文名是 Utilities,也有叫静态方法的。它们挂载在$或jQuery这个全局对象上,不依赖 DOM 节点,不负责事件绑定、动画或 AJAX,核心作用就是处理数组、对象、类型判断、字符串、伪数组转换这类常规操作。你平时写业务代码的很多沟坎,比如判断一个变量到底是不是数组、想把类数组对象变成真数组、想合并一堆配置对象、想在数组里找元素但不影响原数据结构,都可以在这些方法里找到答案。
这篇文章适合谁看?两类人。一类是长期维护老项目的开发者,项目里还在用 jQuery 2.x 甚至 1.x,每天面对一大堆业务代码,但认知停留在“jQuery 就是查 DOM 和发 AJAX”的水平,遇到这些工具函数只能现翻文档;另一类是那种想系统梳理底层工具、把代码写得更简洁干脆的新手。我会把这批方法的用法、坑、设计逻辑一次性说透,顺便给出一套“什么时候该继续用、什么时候该换成原生 API”的判断标准,免得你越用越纠结。
2. 集合处理三件套:$.each、$.map、$.grep 的真正用法
2.1 $.each:不仅是数据的迭代器,还要学会“提前退出”
$.each(collection, callback)接受两种集合:数组(或类数组)和普通对象。它和原生forEach的差别,很多人用了一年也没搞清——最核心的一点是,$.each能提前退出循环。
$.each([1, 2, 3, 4], function (index, value) { if (value === 3) { return false; // 相当于 break } console.log(index, value); }); // 只会输出 0 1 和 1 2,3 和 4 不再处理回调里的return false是终止循环;而return true、return undefined或者干脆不写 return,都表示继续下一个。这个语义很容易和普通循环搞混,我见过有人想跳过当前元素时写return false,结果整个循环提前结束,后续数据全部丢失。
还有一个细节:$.each遍历对象时,回调参数顺序是(key, value),而遍历数组时顺序是(index, value)。对象场景下想拿第一个参数当成“下标”大概率会踩坑。
$.each({ name: "tom", age: 18 }, function (key, value) { console.log(key + " = " + value); }); // name = tom // age = 18遍历类数组对象时,比如{ 0: "a", 1: "b", length: 2 },$.each会按 length 递归遍历,和原生forEach不同,forEach遇到这种对象根本不能调用。这个特性在做数据兼容处理时很有用。实际操作中我推荐一条铁律:只要回调里可能提前退出,就直接用$.each;如果只是简单跑一圈,用原生forEach也行,但要记得处理 this 指向问题。
2.2 $.map 与原生 map 的差别,以及 NULL 元素处理
$.map(arrayOrObject, callback(value, indexOrKey))最反直觉的地方在于返回值处理。原生Array.prototype.map要求回调必须返回一个值,每个元素都会保留在结果里;$.map则把返回null或undefined的元素直接丢弃,回收的数组长度会变短。
var arr = [1, 2, 3, 4]; var result = $.map(arr, function (value) { if (value % 2 === 0) { return value * 10; } return null; // 这个元素从结果里消失 }); // result 是 [20, 40]这个设计当初是为了方便筛选,但新手常把它当成普通 map 用,结果发现结果数组莫名其妙少了几项。需要注意的是,回调里返回空字符串""、数字0、布尔false时,这些值不会丢弃,仍然保留在结果里。只有null和undefined会被过滤。如果你不希望任何值被吞掉,就必须保证每条分支都有 return。
$.map好用还有一个地方:遍历对象时能轻松转成数组。比如拿到一个配置对象,想把它转成[{name: 'x', value: 1}]结构,直接$.map(obj, function(value, key) { return {name: key, value: value}; })就能做到。原生方案需要Object.keys加map,两行代码变三行,区别不算大,但写起来更一气呵成。
2.3 $.grep:过滤数组的高效姿势
$.grep(array, callback(value, index), invert)的作用是筛数组,和原生filter类似。第三个参数invert大多数人没用过,传true时逻辑会反转,即保留那些回调返回 false 的元素。
var scores = [60, 80, 45, 90]; var pass = $.grep(scores, function (value) { return value >= 60; }); // pass: [60, 80, 90] var failed = $.grep(scores, function (value) { return value >= 60; }, true); // failed: [45]如果你既要拿到通过项又要拿到未通过项,用 invert 参数能少写一个回调。原生filter每次都得写完整判断,有时候两者实在各有取舍。我的建议是:如果项目里已经用了 jQuery,配合$.grep + $.map做数据清洗那是一套非常顺手的组合拳,胜过调用好几次原生方法。
2.4 $.inArray 与 $.merge 的边界行为
$.inArray(value, array)返回数组中的索引,找不到时返回-1,很多新手会把判断条件写反。正确的姿势是if ($.inArray(value, arr) >= 0)或!== -1,而不是if ($.inArray(value, arr))——因为当元素正好在第一个位置时返回0,0会被当成 false。
$.merge(first, second)是把第二个数组内容追加到第一个数组上,直接修改第一个数组并返回它。它支持类数组对象,比如可以合并arguments对象。这个效果用原生concat也能实现,但concat返回新数组,不改变原数组,两者行为不同。
var arr1 = [1, 2]; var arr2 = [3, 4]; var result = $.merge(arr1, arr2); // result === arr1,arr1 变成 [1, 2, 3, 4]潜在隐患是它有去重行为吗?没有。合并时如果前后存在相同引用,会原样保留两份。想合并并去重,得配合$.inArray自己加判断,或者用 ES6 的Set。
3. 对象和数据类型判断的细节:$.extend、$.type、其他类型工具
3.1 $.extend 的深浅拷贝与覆盖策略
$.extend(target, obj1, obj2)是杂项方法里出场率最高的方法,处理配置合并、对象克隆都靠它。不传true时是浅拷贝,传true作为第一个参数时是深拷贝。
var defaults = { name: "demo", options: { size: 1, color: "red" } }; var userSettings = { options: { size: 2 } }; var result1 = $.extend({}, defaults, userSettings); // result1.options 是 { size: 2 } // 浅拷贝:userSettings.options 直接覆盖 defaults.options, // defaults.options 里的 color 直接丢了 var result2 = $.extend(true, {}, defaults, userSettings); // result2.options 是 { size: 2, color: "red" } // 深拷贝:子对象一层层合并浅拷贝和多层对象合并是绝大多数配置类 bug 的来源。我踩过一次很典型的坑:表单 A 有一组默认参数,用户只修改了其中一部分,直接$.extend({}, defaults, formData),结果嵌套的options被整体覆盖。后来我统一调整成深拷贝,后续再没犯过。需要提醒的是,深拷贝也不是万能的,循环引用对象会直接报错,函数、正则、Date 实例在某些旧版本里处理得也不完美。
$.extend还有几个隐藏能力:如果只传一个参数,会把内容合并到 jQuery 全局对象上,这可以用来扩展工具函数,类似写插件时的$.fn.extend的表兄弟;如果目标对象是true,那它的第一参数位置要注意偏移,因为布尔值被当作深浅拷贝标志了。
3.2 $.makeArray 与伪数组转换
$.makeArray(obj)把类数组对象转成真正的数组。最常见的场景是拿arguments、批量取到的 DOM 节点集合,或者以索引和 length 为骨架的对象。
function demo() { var args = $.makeArray(arguments); console.log(args); // 真正的数组 } var fakeArr = { 0: "a", 1: "b", length: 2 }; var realArr = $.makeArray(fakeArr); // ["a", "b"]这个东西在原生 ES6 里基本可以用Array.from替代,但两者有一点不同:Array.from(fakeArr)也支持类数组对象,但Array.from还能传第二个 map 映射参数,这点反而更灵活。所以如果你维护的代码不需要兼容 IE8 那种环境,转换这步其实交给原生更干净。只有确实在远古项目里做兼容时,$.makeArray才有绝对优势。
3.3 类型判断的组合拳:$.type、$.isArray、$.isEmptyObject
$.type(obj)会返回变量的精确类型,和原生typeof有很大不同。typeof []返回"object",而$.type([])返回"array";$.type(null)返回"null",$.type(null)在其他工具判断里容易翻车,但这里很稳。它还能识别"date"、"regexp"、"error"等类型。
$.type([]); // "array" $.type(null); // "null" $.type(new Date()); // "date" $.type(undefined); // "undefined"$.isArray(value)本质是对$.type(value) === "array"的封装;$.isFunction判断函数;$.isEmptyObject(obj)判断是否有可枚举属性,注意它不关心原型链上的属性,也不区分对象是{}还是 new 出来的实例。
$.isNumeric(value)比较有意思,它把数字字符串也算作数字。$.isNumeric("12")返回 true,$.isNumeric("12px")返回 false,$.isNumeric("")返回 false。这个方法和typeof value === "number"的习惯不一样,接收表单输入时反而更实用。
$.isNumeric("12"); // true $.isNumeric("12.5"); // true $.isNumeric("0x10"); // true,十六进制字符串也算 $.isNumeric(""); // false一个实际体会:判断一个参数是否为“非空对象”,用!$.isEmptyObject(value)会非常直观。但要小心$.isEmptyObject(new Date())返回 true,因为 Date 实例默认没有自身可枚举属性,它的时间是内部槽位,枚举不出来。
3.4 兼容旧代码时要留意的历史包袱
jQuery 这些工具函数诞生的年代,原生 JS 的功能还很弱,Object.keys、Array.isArray、Array.prototype.map都还没普及。因此很多工具函数本质上是为了“抹平浏览器差异、补齐缺失能力”而存在的。今天再用它们,有些纯粹是冗余,有些则仍然有价值。
举个例子,$.trim(str)在 IE8 环境能替代原生String.prototype.trim,但现代浏览器已经原生支持,$.trim(str)执行效率反而不如原生。$.parseJSON(str)在 jQuery 3.0 里已被废弃,因为原生JSON.parse早已全面普及。$.isArray同样可以用原生Array.isArray完全替代。
因此我整理了一套判断标准:如果你面向的浏览器最低版本是 IE10 以下,那这些兼容函数还是留着的理由;如果项目最低已经到 IE11,或者干脆是内部系统用现代浏览器,那优先用原生。老方法有时候反而会掩盖错误——$.parseJSON在传入非法 JSON 时会返回 null,而原生JSON.parse会直接抛异常。后者虽然“脾气差”,但更容易暴露数据问题。
4. 一份实际案例:用杂项方法处理表单提交数据
4.1 场景设定
假设我在维护一个后台商品管理页面,页面上有一堆筛选条件,包括关键词、价格区间、类目、多选标签,另外还有分页、排序字段。提交到后端之前,需要把表单数据先做一轮清洗:去掉空值,转换数字格式,合并默认配置,再把最终数据交给 AJAX。这个场景天然适合杂项方法组合出场。
4.2 第一步:收集并清洗表单数据
把表单控件手动收集成对象,筛掉空字符串和 null。
var rawData = { keyword: "充电器", minPrice: "19.9", maxPrice: "", category: "数码", tags: ["a", "b"], page: 1 }; var cleaned = $.grep($.map(rawData, function (value, key) { if (value === "" || value === null || value === undefined) { return null; } var obj = {}; obj[key] = value; return obj; }), function (item) { return !!item; });这里我用了$.map先遍历对象,把空值字段变成 null,再转成{key: value}的微型对象,然后用$.grep过滤掉空项。第一次写的时候我也觉得多此一举,后来发现这样得到的结果非常好用:直接$.extend.apply($, [{}].concat(cleaned))就可以把多个对象合并成一个。如果只是纯过滤,直接$.grep(Object.keys(rawData)...)来也行,但做成对象数组的形态后,后续扩展很方便。
4.3 第二步:数值字段的格式化
价格、数量这些表单数据天然是字符串,提交前最好转成数字,并过滤非法值。这里$.isNumeric就派上用场了。
function normalizeNumeric(input, radix) { if ($.isNumeric(input)) { return parseFloat(input); } return null; } rawData.minPrice = normalizeNumeric(rawData.minPrice); rawData.maxPrice = normalizeNumeric(rawData.maxPrice);我见过有人用parseInt直接硬转,结果输入是 “12abc” 竟然变成 12,这种脏数据会带着错误价格提交到后端。$.isNumeric+parseFloat组合能有效避免这种问题。
4.3 第三步:合并默认配置
每个查询接口都有一些后端要求的固定参数,比如页面大小、排序方向、是否需要返回明细。这些参数不该由页面一一写死,应该放在默认配置里合并。
var searchConfig = { pageSize: 20, sortField: "default_sort", order: "desc", needDetail: 1 }; var finalParams = $.extend({}, searchConfig, cleanedData);合并时用深拷贝还是浅拷贝?在这个场景里,cleanedData中可能有嵌套的tags数组,如果用浅拷贝,finalParams.tags会直接引用表单里的数组,后续如果对数组做排序或修改,可能导致原始表单状态被污染。刻意避开这种隐患后,我统一改成$.extend(true, {}, searchConfig, cleanedData)。
4.4 第四步:把类数组 DOM 集合转成可操作数组
页面上还有一组复选标签,用document.querySelectorAll拿到的结果是NodeList,在老浏览器里它不支持 forEach。借助$.makeArray转成真数组后,配合$.each收集被选中的值。
var selectedTags = $.makeArray(document.querySelectorAll(".tag-checkbox:checked")) .map(function (checkbox) { return checkbox.getAttribute("data-tag-id"); });这里其实没有用$.each,因为NodeList转成数组后,原生map完全可用。这种“能用原生就用原生”的思路,会让代码在兼容性和可读性之间保持平衡。
4.5 完整串接
把上面几步串起来后,整个提交前的数据处理流程是这样:
function collectAndPrepare(pageNum) { var formData = { keyword: getValue("#keyword"), minPrice: getValue("#minPrice"), maxPrice: getValue("#maxPrice"), category: getValue("#category"), page: pageNum }; Object.keys(formData).forEach(function (key) { if (formData[key] === "") { delete formData[key]; } }); formData.minPrice = formData.minPrice ? parseFloat(formData.minPrice) : undefined; formData.maxPrice = formData.maxPrice ? parseFloat(formData.maxPrice) : undefined; var finalParams = $.extend(true, {}, searchConfig, formData); finalParams.tags = selectedTags; return finalParams; }这套流程后来我在好几个项目里复用,不断简化成模板。关键是认识到$.extend合并配置 +$.map/$.grep清洗数据 +$.makeArray转换集合,这三类需求覆盖了前端表单处理的大部分底层操作。
5. 常见问题与排查技巧实录
下面这些是我在实战项目里反复踩过、或者看到别人反复踩的坑,整理成速查表,方便你直接对照。
5.1 $.each 回调的返回值陷阱
注意:
$.each中return false是终止循环,不是跳过当前项。想跳过当前项,应该return true或return undefined。
这个错误隐蔽性很高。很多人从原生each或for循环转过来后,写if (condition) return;以为只是跳过,结果循环直接断了。
正确写法:
$.each(params, function (key, value) { if (value === null) { return true; // 跳到下一个 } if (key === "stopKey") { return false; // 终止整个循环 } console.log(key, value); });5.2 $.extend 浅拷贝导致子对象覆盖
典型事故现场:默认配置里有嵌套对象,用户传入的配置里只有部分字段,因为浅拷贝,子对象整体被替换,未传字段消失。
var defaults = { filter: { status: 1, dateRange: "last7d" } }; var input = { filter: { status: 2 } }; var merged = $.extend({}, defaults, input); // merged.filter.dateRange 直接没了排查建议:在控制台直接打merged看结果,如果嵌套层级超过一层但输出结果和预期不符,立刻想到深浅拷贝问题。修法就是$.extend(true, {}, defaults, input)。
5.3 $.type 对 null 和 NaN 的判断结果
$.type(NaN)返回"nan",不是"number",这一点可能跟你预期不一样。想判断数字的 NaN,用$.isNumeric(NaN)会返回 false,因为NaN不是 numeric。判断“是不是 NaN”还是要回归原生Number.isNaN或者isNaN。
$.isEmptyObject对没有可枚举自有属性的对象返回 true,包括new Date()、new RegExp()、new Map()这类实体内置对象的实例。如果业务上认为 Date 实例也算“有内容”,就需要额外判断类型。
5.4 老版本 jQuery 方法的废弃与移除
jQuery 3.x 正式移除$.parseJSON、$.isArray和$.isFunction等部分方法在文档中也被标记为废弃。虽然老项目不升级就不会有影响,但如果你混用新老两种写法,容易出现“页面明明引用了 jQuery 3.x,但调用$.parseJSON直接报错”的问题。
排查技巧:升级 jQuery 前先全局搜索这些方法名,批量替换成原生方案,其实工程量不大。我处理过一个老项目升级时顺手用grep -rn '\$\.parseJSON' src/把所有出现点找出来,一次性手动改完。遇到$.type需要替换时,可以用一套简单映射表覆盖常见类型。
5.5 杂项方法性能与体积的现实取舍
jQuery 完整包体积不小,如果只为了用几个工具函数而引入整个 jQuery,在只看性能和加载体积的现代前端体系里不划算。我们可以把$.each、$.map换成原生方法,但有一类场景例外:你已经在项目里引用了 jQuery 做 DOM 操作和 AJAX 了,再多用几个工具函数属于零成本。
另一个取舍点是代码的易读性。$.grep配合自定义回调,比多次filter更加紧凑,尤其当一个数组要经过多轮清洗时,链路一眼就看明白。我以前在某个后台项目里把数组数据清洗写成了“一串 jQuery 链式工具调用”,队友起初不熟悉,但看懂之后反而说比一堆中间变量好维护。
5.6 反直觉的数组索引与参数顺序
$.each、$.map回调参数顺序是(index, value),$.grep也是(value, index)反过来,这种不一致很容易写错变量名。如果回调里用到 index,先打印一下确认方向。
$.each([10, 20], function (index, value) { console.log(index, value); // 0 10 / 1 20 }); $.grep([10, 20], function (value, index) { console.log(value, index); // 10 0 / 20 1 });避免低级的参数错位,我会在写工具方法前先明确约定:一律把 value 放前面、index 放后面,不依赖官方具体顺序,这样回调函数以后迁移到原生方法时也更自然。
6. 杂项方法背后的设计哲学与我的使用习惯
我自己用这些工具函数五年多,最后形成的习惯可以概括为三点:
第一点,动态类型判断用$.type做兜底,尤其接手别人代码、不确定变量是不是数组时,$.type(x) === "array"比x instanceof Array更稳定,因为它不受跨 iframe 或跨 window 影响。原生Array.isArray也有这个优点,所以现在我会首选Array.isArray,只有在处理极其老的环境时退回到$.type。
第二点,所有配置合并都先想清楚“浅拷贝还是深拷贝”,再决定是否用$.extend。我写完任何一处 extend,会主动检查目标对象会不会在后续被修改。如果存在这种可能,无脑深拷贝。
第三点,工具函数的本质是“降低认知负担”。当你看到一串$.map(...).filter(...)和一堆for循环嵌套时,前者显然更容易让人理解意图。但前提是团队成员都对这批方法有共识。团队里如果有新手,我会先简单做一次方法分享,把$.each的 return 语义和$.extend的深浅拷贝讲清楚,后面写代码顺手很多。
如果你正在优化老项目的前端代码,我个人的建议是:不要因为“能用原生就绝不使用库函数”的教条而把每个工具函数都改掉。杂项方法的真正价值在于统一封装、语义清晰,以及和现有 jQuery 代码风格保持一致。你只需要记住哪些方法是“时代遗留”、哪些仍然实用,然后在具体场景里灵活选择。
最后分享一个偏技巧的用法:想在$.each循环结束后拿到统计结果,可以在回调外层声明一个变量累加,或者用$.map先转换再聚合。我自己常用$.map生成一个统计数组,再用原生 reduce 计算总和,这种组合比在循环里憋一个复杂逻辑清晰得多。这类小工具函数的利用方式,其实就是你日常写代码时顺手总结、不断沉淀出来的,用多了自然会有自己的套路。