先记住这个答案
instanceof Array 检查的是原型链:沿着对象的 __proto__ 往上找,看有没有碰到当前环境里的 Array.prototype。而 Array.isArray 检查的是对象本身是不是真正的数组,和原型无关。每个 iframe 都有自己独立的全局环境,也就有自己的 Array 构造函数和 Array.prototype,iframe 里创建的数组,原型链上是 iframe 的 Array.prototype,父页面的 instanceof Array 自然找不到,返回 false,而 Array.isArray 仍然返回 true。反过来,一个普通对象只要把原型设成 Array.prototype,就能骗过 instanceof,但骗不过 Array.isArray。
instanceof看原型链,Array.isArray看对象本身- 每个 iframe 有独立的
Array,所以instanceof跨过去就失效 - 原型是数组原型的普通对象,
instanceof为true但不是数组 - 判断数组一律用
Array.isArray
原型能改,数组身份改不掉
先不用 iframe,在同一个环境里就能看出两者的差别。
const fake = Object.create(Array.prototype) // 原型是数组原型,但不是数组
console.log(fake instanceof Array, Array.isArray(fake))
const real = [1, 2]
Object.setPrototypeOf(real, null) // 真数组,原型被改掉了
console.log(real instanceof Array, Array.isArray(real))
console.log(Array.isArray(new Uint8Array(2)), Array.isArray(Array.prototype))查看输出与解释
true false
false true
false truefake 能调用 push、map,看起来像数组,但它是普通对象创建的,给它设置下标时 length 不会自动更新,Array.isArray 就认出来了。real 被改掉了原型,连 map 都调不了了,但它的「出身」还是数组,Array.isArray 照样返回 true。
可以把 instanceof 想成看对方的工牌,工牌可以借、可以换;Array.isArray 是查身份证,出生时就定了。规范里的说法是,数组是一种特殊的对象(exotic object),它的 length 会随下标自动变化,Array.isArray 检查的就是这个内部特征。
最后一行有两个冷知识:TypedArray 不算数组;Array.prototype 本身却是一个真数组,所以 Array.isArray(Array.prototype) 是 true。
跨 iframe 为什么会失效
realm 可以理解成一套独立的 JavaScript 全局环境,里面有自己的 Object、Array、Function 等内置对象。每个 iframe、每个 Worker、Node.js 里每个 vm 上下文都是一个 realm。
const iframe = document.createElement('iframe')
document.body.appendChild(iframe)
const IframeArray = iframe.contentWindow.Array
const arr = new IframeArray(1, 2, 3) // 在 iframe 的环境里创建
console.log(IframeArray === Array) // false,两个不同的构造函数
console.log(arr instanceof Array) // false,原型链上是 iframe 的 Array.prototype
console.log(Array.isArray(arr)) // true两个页面的 Array 是两个不同的函数,各自的 prototype 也是两个不同的对象。arr 的原型链上只有 iframe 那一份,父页面拿自己的 Array.prototype 去找当然找不到。实际项目里,微前端、富文本编辑器、老的 iframe 嵌套页面,都会遇到从另一个 realm 传过来的数组。postMessage 传过来的数据不会有这个问题,因为它会用结构化克隆在当前环境里重新创建一份。
容易答错的地方
- 以为
Object.prototype.toString判断万无一失 Object.prototype.toString.call(x) === '[object Array]'是 ES5 之前的跨 realm 方案,但它能被Symbol.toStringTag伪造。给普通对象设置[Symbol.toStringTag]: 'Array',返回的也是[object Array]。现在直接用Array.isArray。- 以为
instanceof只在 iframe 里才会出错 - 只要原型被改过,同一个页面里也会出错。上面的
fake和real就是例子。instanceof本质上只回答「原型链上有没有某个对象」,它不回答「这是不是某种类型」。 - 以为类数组、TypedArray 也会返回
true arguments、NodeList、Uint8Array都有length和下标,但Array.isArray全部返回false。需要统一处理时,先用Array.from转成真数组。
面试官还会怎么问?
Array.isArray 对 Proxy 包装的数组返回什么?
返回 true,因为 Array.isArray(new Proxy([], {})) 会穿过 Proxy 去检查它包装的目标对象。Vue 3 的响应式数组就是用 Proxy 包装的,所以 Array.isArray(reactive([])) 是 true。
其它内置类型跨 realm 怎么判断?
没有通用方案。Array 有 Array.isArray;Promise 可以判断是不是 thenable;Date、RegExp 这类常用 Object.prototype.toString 的返回值判断,接受它可以被伪造的前提。能避免跨 realm 传对象,就尽量在边界上转换成当前环境的对象。
继承 Array 的子类实例用两者判断结果是什么?
定义 class MyList extends Array {} 之后,new MyList() 用 instanceof Array 和 Array.isArray 都是 true。子类的构造过程最终由 Array 创建对象,所以它是真数组,原型链上也有 Array.prototype。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。