先记住这个答案
typeof 的设计很粗:它只区分几种原始类型(string、number、boolean、undefined、symbol、bigint)和函数,剩下所有对象一律返回 'object'。数组在 JavaScript 里就是一种特殊的对象,所以 typeof [] 是 'object'。准确判断数组用 Array.isArray(value),这是 ES5 专门为这个问题加的方法。
typeof对数组返回object- 判断数组首选
Array.isArray - 跨 iframe 用
Array.isArray判数组 Symbol.toStringTag可伪造类型标签
typeof 为什么分不出数组
typeof 看的是值属于哪一大类。数组、普通对象、日期、正则、null 都在「对象」这一类里(null 是历史 bug),所以它们的 typeof 都一样。
const values = [[], {}, null, new Date(), { length: 0 }]
for (const v of values) {
console.log(typeof v, Array.isArray(v), Object.prototype.toString.call(v))
}
const fake = Object.setPrototypeOf({}, Array.prototype) // 原型改成数组的原型
console.log(fake instanceof Array, Array.isArray(fake))查看输出与解释
object true [object Array]
object false [object Object]
object false [object Null]
object false [object Date]
object false [object Object]
true false前五行 typeof 全是 object,没有区分度。{ length: 0 } 这种类数组对象,Array.isArray 也不会误判。最后一行最能说明差别:把普通对象的原型改成 Array.prototype,instanceof 就被骗了,Array.isArray 不受影响,因为它检查的是对象本身是不是真正的数组,而不是原型链。
iframe 场景下 instanceof 为什么失效
每个 iframe 都有自己独立的一套全局对象,包括自己的 Array 构造函数和 Array.prototype。父页面拿到子 iframe 里创建的数组,它的原型链指向的是子 iframe 的 Array.prototype,和父页面的 Array 不是同一个对象。所以在父页面里写 arr instanceof Array 会得到 false,但它明明就是数组。
Array.isArray 没有这个问题,它不依赖原型链。postMessage 传过来的数据会被结构化克隆,重新在当前窗口创建,反而不受影响,真正会踩坑的是直接访问 iframe.contentWindow 里的对象。
容易答错的地方
- 用
arr.constructor === Array判断 constructor只是原型上的一个普通属性,谁都能改,跨 iframe 也会失效。对象如果是Object.create(null)创建的,访问constructor直接得到undefined。- 以为
Object.prototype.toString绝对可靠 - ES2015 以后,对象可以通过
Symbol.toStringTag自定义这个结果,比如给普通对象设[Symbol.toStringTag]: 'Array',就会输出[object Array]。它仍然适合区分内置类型,但判断数组还是Array.isArray更准。 - 以为有
length就是数组 arguments、NodeList、字符串都有length,它们是类数组,不能直接调map、filter。需要数组方法时用Array.from转一下。
面试官还会怎么问?
typeof null 为什么也是 'object'?
这是第一版 JavaScript 留下的 bug。当时值的底层用类型标签加数据存储,对象的标签是 0,而 null 用全零表示,于是也被当成了对象。为了不破坏已有网页,这个行为一直没改。判断 null 直接用 value === null。
怎么判断一个值是普通对象?
常见写法是 Object.prototype.toString.call(v) === '[object Object]',再加上原型判断:Object.getPrototypeOf(v) 是 Object.prototype 或 null。lodash 的 isPlainObject 就是这个思路。
TypeScript 里 Array.isArray 能收窄类型吗?
能。它的类型声明是类型守卫,if (Array.isArray(v)) 之后,分支里的 v 会被收窄成数组类型。不过对于 readonly 数组的联合类型,收窄结果有时不够精确,需要自己写类型守卫补一下。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。