ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaScript六种数组判断方法详解:原理、坑点与最佳实践

JavaScript六种数组判断方法详解:原理、坑点与最佳实践 先别急着往下翻代码说个我早先遇到的场景。在做某个云平台控制台前端的时候我需要维护一段公共工具函数库里面有个功能是把接口返回的各种“疑似数组”的数据统一转成标准数组。本以为就是判断一下类型再调map、filter那么简单结果真上线之后后台工单系统里连续出现了好几个诡异的bug都是“明明传的是数组却走错分支”的问题。后来一查才发现真正的问题不是数据格式而是我在代码里判断数组的方式覆盖得不够全。所以这篇东西我打算把JavaScript里判断数组值的6种方法从头到尾整理一遍连同每种方法的原理、失效场景、生产环境建议都讲透。这篇文章适合正在写业务逻辑的前端开发也适合被“这个变量到底是不是数组”这种问题坑过的人。你不需要把6种方法全背下来但至少要明白它们之间的差异以及为什么大多数现代项目里会优先推荐Array.isArray()。看懂了这些以后不管是在node环境、浏览器控制台还是老旧代码里碰到数组判断你都能在几秒钟之内找到合适的解法。1. 为什么“判断数组”在JavaScript里会成为一个问题1.1 数组本身就是一个“伪装成对象的对象”在JavaScript里数组的底层实现本质上是一种特殊的对象。它和普通对象最大的区别在于下标是字符串形式的数字键比如arr[0]和arr[0]是同一个东西有一个length属性会随着元素的增删自动更新原型链上挂着push、pop、splice、join等一系列数组方法。正因为数组是对象的一种当我们用最基础的typeof去判断时结果会让人非常尴尬。typeof []返回的是objecttypeof {}返回的也是object连typeof null都照样返回object。你要是靠typeof判断数组等于把所有对象都混在一起根本没法区分。我刚入行那会儿就干过这种事当时写了一个函数function isArray(value) { return typeof value object value ! null; }结果是普通对象、日期对象、正则表达式、DOM节点全都能被它判定成数组惨不忍睹。后来才明白JavaScript里“判断一个值是不是数组”这个需求之所以能单独成一篇文章是因为数组作为对象家族的“特例”必须借助原型链、内部标识或构造器线索来识别而不能靠表面类型。1.2 真数组、伪数组和类数组傻傻分不清除了“数组本身是对象”这个原因之外另一个容易出问题的点在于实际业务里我们经常会碰到底层不是数组、但用法上像数组的数据。最常见的是函数内部的arguments对象还有通过document.querySelectorAll拿到的NodeList。它们都有length属性也支持用下标访问看起来很像数组但既不继承Array.prototype也不具备数组原型上的push、map等方法。这类对象通常被叫作“类数组”或“伪数组”。在HoRain云这类后台管理系统的前端里类数组出现频率很高。表格组件选中行、图表库传入坐标点、实时日志接口返回的日志列表都有可能以类数组或者普通对象的形式到达前端。如果不先确认“它到底是不是真数组”后续调用arr.map()、arr.filter()时就会直接报“arr.map is not a function”这类报错在工单里占了不小的比例。所以判断数组的问题本质上要拆成两个层面来看严格判断这个值是不是真正的数组也就是它是否由Array构造器创建或者是否继承了Array.prototype宽松判断这个值能不能当数组来用也就是它是否具备数组的基本特征。第2章要讲的6种方法正好分别覆盖这两个层面。开头先记住这个划分后面看具体方法时会清晰很多。2. 六种JavaScript数组值判断方法逐个拆解2.1 Array.isArray()——官方标准优先使用Array.isArray(value)是ES5引入的静态方法也是目前最正统的数组判断方式。它的返回值是布尔值如果是真数组就返回true否则返回false。Array.isArray([]); // true Array.isArray(new Array(3)); // true Array.isArray([1, 2, 3]); // true Array.isArray({ 0: a, length: 1 }); // false Array.isArray(abc); // false Array.isArray(null); // false这个方法最核心的优势是跨执行环境安全。JavaScript里有个历史包袱叫“多全局对象”最常见的就是iframe。父页面里的数组是父页面的Array构造器创建的子页面里的数组是子页面自己的Array构造器创建的。如果直接用后文要讲的instanceof判断跨iframe时结果会出错。而Array.isArray()在规范层面按数组特有的内部标识去判断不依赖当前环境的Array构造器所以即使数组来自另一个iframe也能正确识别。在实际项目中Array.isArray()唯一的妥协是兼容性。ES5之前的浏览器不支持这个方法比如IE8。不过现在主流的浏览器和Node.js环境都已经全覆盖了除非你在维护特别老旧的项目否则可以直接放心用。我给个明确建议普通业务代码里凡是判断“是不是真数组”第一优先就是Array.isArray()。不要想太多它就是默认答案。2.2 instanceof Array——基于原型链的经典判断value instanceof Array可能是很多JS初学者最早接触的数组判断写法。它的原理是沿着value的原型链一路向上查找看看能不能找到Array.prototype。[] instanceof Array; // true new Array(3) instanceof Array; // true ({}) instanceof Array; // false abc instanceof Array; // false看起来很好用对吧但它有两个明显的坑。第一个坑是跨iframe失效。前面说过每个iframe都有自己独立的Array构造函数。子页面里创建的一个数组它的原型链上挂的是子页面的Array.prototype而不是父页面的。你在父页面的代码里写childValue instanceof Array拿父页面的Array.prototype去对比结果就是false。这种问题在后台系统里非常容易出现因为很多后台会动态嵌入报表iframe、编辑器iframe你拿到的数据源可能来自另一个窗口。第二个坑和ES6的Symbol.hasInstance有关。实际上instanceof的行为可以被自定义。如果一个对象的Symbol.hasInstance方法被改写instanceof的结果就会跟着变。虽然不常见但对于一个“判断类型”的操作来说这种可篡改性始终是个隐患。我的建议是instanceof Array在单窗口、不涉及跨应用的场景下可以应付简单判断但别把它当成万能方案。遇到复杂前端项目时它能坑你一次就能坑你很多次。2.3 constructor Array——从构造器入手判断每个对象身上都有一个constructor属性它指向创建该对象的构造函数。数组是由Array构造器创建出来的所以理论上value.constructor Array也能用来判断数组[].constructor Array; // true new Array(3).constructor Array; // true ({}).constructor Array; // false abc.constructor Array; // false但在实际代码里我特别不推荐把它当成主要判断手段原因在于constructor属性可以被轻易覆盖。比如下面这段代码const fake { constructor: Array }; console.log(fake.constructor Array); // true一个普通对象只要挂上constructor: Array就能骗过这种判断。更有意思的是如果某段业务代码改了数组实例的constructor属性比如const arr []; arr.constructor Object; console.log(arr.constructor Array); // false本来是数组也可能被误判成“不是数组”。另外跨iframe时constructor同样会失效。子页面数组的constructor指向的是子页面的Array构造器和父页面里的Array不是同一个引用比较结果也是false。如果你在维护第三方依赖或者老代码时看到这种写法能看懂就行自己新写逻辑建议避开。它更多是用来展示“对象从哪来”的一种线索不是严格的类型判断工具。2.4 Object.prototype.toString.call()——最稳定的“身份证”读取方式接下来是很多框架源码里爱用的方法Object.prototype.toString.call([]); // [object Array]它的原理比较底层Object.prototype.toString并不直接查看typeof的结果而是会去读取对象内部的[[Class]]标识在ES6之后具体实现有所调整但行为上类似然后返回[object 类型名]这样的字符串。因为读取的是内部标识而这个标识是语言引擎按值的真实类型给出的所以它不会因为对象来自哪个iframe而变化const iframe document.createElement(iframe); document.body.appendChild(iframe); const arr iframe.contentWindow.eval([]); Object.prototype.toString.call(arr); // [object Array]在跨窗口判断场景下Object.prototype.toString是目前最稳定的兜底方案之一。很多工具库会把判断逻辑封装成一个内部方法function isArray(value) { return Object.prototype.toString.call(value) [object Array]; }这个方式的坑也有但比较特殊。ES6引入了Symbol.toStringTag允许对象自定义Object.prototype.toString返回的标签。换句话说如果某个对象上定义了自己的Symbol.toStringTag就能伪造出[object Array]字符串const fake { [Symbol.toStringTag]: Array }; Object.prototype.toString.call(fake); // [object Array]所以严格来说它判断的是“对象是否声称自己是Array”而不是100%的“根正苗红的数组”。日常开发遇到这种伪造的几率极小但在安全敏感的场景中还是要在心里留根弦。2.5 Array.prototype.isPrototypeOf()——原型链上的探针isPrototypeOf是挂在Object.prototype上的方法用来检测某个对象是否存在于另一个对象的原型链上。用法是Array.prototype.isPrototypeOf([]); // true Array.prototype.isPrototypeOf({}); // false它和instanceof Array在原理上是同一个思路都是从原型链查找。区别在于书写形式instanceof是运算符isPrototypeOf是方法可以通过call或apply灵活绑定到任意值上。这里要特别提醒Array.prototype.isPrototypeOf()同样不能解决跨iframe问题。一个来自子iframe的数组它的原型链上挂的是子iframe的Array.prototype而不是父页面上的Array.prototype所以判断结果还是false。它的适用场景更偏“扩展方法”。举个例子如果你想给所有真正继承自数组的对象统一加一层自定义判断逻辑或者想在函数式工具里把判断数组的方法作为参数传来传去用isPrototypeOf就比用instanceof运算符顺手一些。但在普通的业务代码里能用Array.isArray()就不要绕道走这个。2.6 鸭子类型综合判断——管你是不是数组能当数组用就行“鸭子类型”不是哪门语言里的黑魔法它来自一句流传很广的话如果一只鸟走起来像鸭子、叫起来像鸭子那就可以把它当鸭子。放到数组判断上思路就变成了“只要这个值有length属性、能按下标访问、还有数组相关方法那就可以把它当数组处理”。典型实现长这样function isArrayLike(value) { return value ! null typeof value ! function typeof value.length number value.length 0 value.length Math.floor(value.length) value.length 4294967295; }这个判断并不检查对象是否真的继承Array.prototype它只检查“数组该有的外形”。所以它真正识别的是“类数组对象”比如arguments、NodeList甚至一个普通的{ length: 0 }。鸭子类型的最大价值在于它足够宽松。在需要兼容“伪数组”的业务场景里这种判断能避免很多无谓的报错。最大问题也在于宽松{ length: 99, name: test }这种对象也会被判定为类数组后续如果真拿数组方法去处理它仍然可能出问题。因此在生产环境里鸭子类型一般不会被用来替代“严格数组判断”而是作为“能不能先把数据转换成数组”的预检查。比如在做表格数据处理时我经常先判断“值是不是类数组”是的话就用Array.from(value)转成真数组再走后续逻辑。这一步能统一处理arguments、NodeList和部分接口返回的数据结构非常实用。3. 六种方法横向对比与选型建议3.1 一张表看透它们的差异光看单点解释还不够我把这6种方法放一起做个对比方便你收藏后随时翻看判断方式判断的是真数组跨iframe可靠结果可被篡改兼容性适用场景Array.isArray(value)是可靠基本不可ES5全局推荐业务代码首选value instanceof Array是不可靠可被Symbol.hasInstance改写全版本单窗口简单判断value.constructor Array一定程度上不可靠可被覆盖全版本看源码时能看懂即可Object.prototype.toString.call(value)是理论上可伪装可靠可被Symbol.toStringTag伪装全版本框架底层、跨窗口兜底Array.prototype.isPrototypeOf(value)是不可靠很少被改写全版本需要灵活绑定判断函数的场景鸭子类型综合判断否判断的是“类数组”不涉及本身依赖特征谈不上篡改全版本兼容arguments、NodeList等伪数组从这张表能直接得出一个经验结论严格判断真数组时先看Array.isArray()涉及跨iframe再补一层Object.prototype.toString.call()兜底涉及类数组识别才考虑鸭子类型。这基本覆盖了日常开发中绝大多数的场景。3.2 不同项目阶段的选型建议如果你是做业务页面开发项目里没有历史包袱直接把Array.isArray()当成默认选择就好看到老代码里的instanceof Array也没必要恐慌知道它只适合单窗口环境就行。如果你在做组件库、工具库、SDK这类要给别人用的代码情况会复杂一些。因为你没法控制使用者的运行环境它们可能在iframe场景里工作也可能遇到各种伪造的Symbol.toStringTag。这时候我会建议用“先Array.isArray再toString兜底”的双保险写法function isStrictArray(value) { if (Array.isArray) return Array.isArray(value); return Object.prototype.toString.call(value) [object Array]; }如果你是在维护需要兼容到ES3的老系统那Array.isArray都还不一定存在就老老实实从toString方案起步再配合必要的typeof和空值检查。3.3 关于性能说点我的实测感受网上很多人喜欢给这几种方法做性能测试结论几乎都一样Array.isArray()最快toString次之instanceof再次鸭子类型因为要检查好几个字段通常最慢。实际项目里如果你不是在一秒内对数百万个值做判断性能差异基本可以忽略不计。真正值得关注的不是性能而是“判断的是真数组还是类数组”这个语义差异。选错语义比选错方法要严重得多。比如某个数据校验逻辑要求“必须是真数组”你却用了鸭子类型判断结果一个NodeList偷偷混进来后面调用Array.prototype.flatMap这种方法时就直接挂掉。反过来如果你只是想统一处理“可以转成数组的数据”却用了严格的Array.isArray那arguments对象漏掉以后后续的转换分支全都走不到。4. 生产环境实战封装一个靠谱的数组判断工具函数4.1 从工具库需求出发回到我在HoRain云控制台项目里维护公共工具库的场景。当时最核心的需求不是“写个判断函数”而是“不管数据来自哪个窗口、是数组还是类数组都要尽可能安全地归一化成真数组”。基于这个目标我把判断逻辑分成了三层第一层空值判断。null和undefined一定要提前过滤不然所有判断方法都会直接报错。第二层严格数组判断。优先用Array.isArray()如果环境不支持就降到Object.prototype.toString.call()。第三层类数组判断。并不是所有“有length属性”的值都能无脑转换比如函数身上也有length属性但它代表的是参数个数转换成数组毫无意义。所以我在类数组判断里额外排除了函数。封装出来的核心函数大概是这个形态function normalizeArray(value) { if (value null) { return []; } if (Array.isArray(value)) { return value; } const tag Object.prototype.toString.call(value); if (tag [object Array]) { return value; } if ( typeof value object typeof value.length number value.length 0 value.length Math.floor(value.length) value.length 4294967295 ) { return Array.prototype.slice.call(value); } return []; }这段逻辑里有个细节值得展开为什么类数组判断里要限制length小于4294967295因为数组的最大长度在JavaScript规范里就是2^32 - 2也就是4294967294左右。如果length超过这个值即便是一个对象它也不可能被当作合法数组处理。这个边界在接口数据异常的情况下能拦住不少脏数据。另外Array.prototype.slice.call(value)是经典的老式类数组转换写法。现在更推荐直接用Array.from(value)它语义更清晰对可迭代对象的支持也更好。之所以在工具函数里保留老写法是考虑部分运行环境可能不支持Array.from。如果你没有这种兼容性压力直接写Array.from(value)完全没问题。4.2 实战中碰到的三个典型场景第一个场景是表格分页组件接收选中行数据。接口在部分情况下返回的不是数组而是{ 0: row1, 1: row2, length: 2 }这样的对象。一开始代码里直接data.map(...)上线后部分页面报错。换成normalizeArray以后数据统一成了真数组后续操作就顺了。第二个场景是iframe报表里的回调数据。报表子页面通过postMessage把选中指标传回父页面父页面在业务代码里用instanceof Array判断是不是数组结果永远是false。最后在消息处理入口统一走normalizeArray这个问题才算彻底解决。第三个场景是日志流。实时日志接口返回的数据偶尔会出现若干个NodeList对象严格来说它们是类数组不是真数组。如果不做归一化日志列表的渲染函数里调用list.filter()就会炸。利用上面的工具函数转换之后前端再没出现同类报错。4.3 polyfill与ESLint两个容易被忽视的细节如果你需要兼容老浏览器可以自己给Array.isArray写一个polyfillif (!Array.isArray) { Array.isArray function (arg) { return Object.prototype.toString.call(arg) [object Array]; }; }这个polyfill在绝大多数场景下够用唯一需要注意的是前面提到的Symbol.toStringTag伪造问题。不过老浏览器连Symbol都没有所以这个担忧对老环境来说反而不存在。ESLint方面有一个规则叫no-instanceof-array就是强制要求用Array.isArray()代替instanceof Array。开启以后可以避免项目里的人在新代码中写出跨窗口判断失效的代码。如果你在维护团队公共规范建议把这条规则开起来。写法很简单{ rules: { no-instanceof-array: error } }加上这条规则后团队成员写instanceof Array时编辑器会直接标红这能在代码评审之前就拦掉一大半潜在问题。5. 常见问题与排查实录5.1 速查表遇到这些现象先查哪一步下面这张表是我在排查项目问题时的经验浓缩你可以直接作为“病历本”用现象大概率原因排查/解决方法typeof []返回object和对象分不清JS语言设计如此数组是对象家族成员不要用typeof判断数组改用Array.isArrayiframe内传入的数组父页面判断为false跨多全局对象原型链和构造器都不同用Array.isArray或Object.prototype.toString兜底一个对象设置了constructor: Array被误判为数组constructor属性可被篡改别依赖constructor做类型判断arguments、NodeList调用map()报错它们是类数组不是真数组先转换再操作Array.from或Array.prototype.slice.call自定义对象[Symbol.toStringTag]Array被toString方案误判ES6允许自定义类型标签对安全性有要求时组合多种判断判断函数传入null或undefined直接报错空值没提前过滤判断前先验证value null5.2 一个真实的iframe排查过程印象很深的一次排查是这样的。运营后台嵌了一个数据报表iframe报表页面通过postMessage把当前选中的行数据发给父页面。父页面收到消息后有一段老代码写的是if (payload.data instanceof Array) { // 处理数组 } else { // 按对象处理 }结果所有来自iframe的数组都走了“按对象处理”的分支导致选中行数据一直加载不出来。一开始我还以为是postMessage的数据结构在传输过程中被序列化成了字符串后来在控制台里手动对比才发现payload.data确实是一个数组但它是在iframe环境里创建的。instanceof在跨窗口场景下就是会失效。修法很简单把判断条件换成Array.isArray(payload.data)问题立刻消失。这件事之后我在团队里立了一个规矩新代码里不允许出现instanceof Array统一用Array.isArray()。历史代码可以逐步替换但不允许再新增。5.3 Symbol.toStringTag伪装一个更冷门的坑Symbol.toStringTag是在ES6之后出现的能力。对象可以通过它自定义Object.prototype.toString.call()返回的字符串。所以从严格意义上说toString方案也不能保证100%识别真数组。我知道这听起来很绕但真实场景里确实有人会这么写。比如在做数据模拟时有人用Proxy包装了一个对象故意让它对外表现得像一个数组结果一些依赖toString判断的校验逻辑被轻松绕过。如果你所在的项目对数据安全要求比较高我的建议是别把“判断数组”的希望全部寄托在单一方法上。可以先做一层Array.isArray如果返回true就直接认定是数组如果返回false但你又怀疑对方可能伪造了Symbol.toStringTag可以再检查一下value instanceof Array或者直接尝试调用数组本地方法观察是否报错。层层叠加虽然看起来绕但对恶意输入能多一道防线。5.4 调试时最有用的一个小技巧最后分享一个调试小技巧。在控制台里排查数据时经常能看到某个对象的结构长得像数组但就是不确定它是不是真数组。你可以同时打印两个值console.log(value, Array.isArray(value));这样控制台里会直接显示出当前值的长相以及判断结果一眼就能看出“看起来像数组”和“真的是数组”之间差了多少。尤其在处理接口返回数据时这种打印能快速帮你定位是后端数据结构问题还是前端判断逻辑问题。还有一个更直观的验证方法Array.isArray结果为true的数据在控制台里通常会用数组样式高亮显示比如(3) [1, 2, 3]而普通对象只会显示成{0: 1, 1: 2, 2: 3, length: 3}。看到这两种不同的展示样式你就能理解为什么“转移成数组”这类操作对后续渲染如此重要了。结尾我的个人体会写到这里这6种方法算是讲完了。说句实在话判断数组这件事看起来简单但每一种写法背后都有它自己的“失效边界”。Array.isArray()跨窗口稳但老环境不认instanceof书写简洁但经不起跨窗口和Symbol.hasInstance改写toString足够底层但有可能被伪造鸭子类型最灵活但判断的已经不是“真数组”了。我现在的习惯是把Array.isArray()当成默认入口把Object.prototype.toString.call()作为兜底把鸭子类型留给明确的类数组转换场景。这样既不会在正常业务里埋雷也不至于在复杂环境里被各种历史问题绊倒。如果你之前在代码里用过某个方法被坑过不用紧张那很正常只要把这里的边界都记住了下次再碰到就能一眼看穿原因。
返回列表