Skip to content
数组类型判断的安全边界
概述
判断一个值是否为数组,JavaScript 提供了多种途径。但除了 Array.isArray 之外,其他方式都可能被绕过。
关于
typeof、Object.prototype.toString、instanceof、constructor、Array.isArray五种类型判断的完整对比,参见 js中判断对象的方法。
判断数组的常用方法
常用的数组判定手段主要有四种:
Object.prototype.toString.call(obj)—— 返回"[object Array]"obj instanceof Array—— 检查原型链上是否存在Array.prototypeobj.constructor === Array—— 通过构造函数引用判断Array.isArray(obj)—— ES5 提供的静态方法
下面逐一说明它们的工作原理与局限性。
Object.prototype.toString 与 Symbol.toStringTag
js
Object.prototype.toString.call([]); // '[object Array]'这个方法并不直接读取内部 [[Class]] 槽位,而是读取对象的 @@toStringTag well-known symbol 属性。该属性可写,因此可以被伪造:
js
const fake = { [Symbol.toStringTag]: 'Array' };
Object.prototype.toString.call(fake); // '[object Array]'在 V8 中,Array.prototype[Symbol.toStringTag] 被定义为一个 getter:
js
Object.getOwnPropertyDescriptor(Array.prototype, Symbol.toStringTag);
// { get: [Function], set: undefined, enumerable: false, configurable: true }该 getter 返回 'Array'。由于 set: undefined,直接执行 Array.prototype[Symbol.toStringTag] = 'x' 并不会覆盖这个 getter,而是会在原型上创建一个新的数据属性。但在 Object.prototype.toString 的读取路径中,自定义对象自身的 @@toStringTag 属性会遮蔽原型链上的 getter,从而改变输出结果。
instanceof 与原型链
js
[] instanceof Array; // trueinstanceof 沿 [[Prototype]] 链向上查找,只要在链上出现 Array.prototype 即返回 true。通过原型继承可以轻易构造出通过 instanceof 检查的非数组对象:
js
function LikeArray() {}
LikeArray.prototype = Object.create(Array.prototype);
const a = new LikeArray();
a instanceof Array; // true
a.push(1); // 可以正常调用(push 返回新数组长度)
Array.isArray(a); // falseObject.create(Array.prototype) 创建了一个以 Array.prototype 为原型的对象,将其赋给 LikeArray.prototype 后,new LikeArray() 产生的实例原型链上必然存在 Array.prototype。这种对象继承了数组方法,但不是真正的数组。
跨 Realm 场景(例如 iframe 或 vm 模块)中,instanceof 还会因为比较的是不同执行上下文里的 Array.prototype 引用而失准。具体分析在 js中判断对象的方法 中有展开讨论。
constructor 属性可覆盖
js
[].constructor === Array; // trueconstructor 是 prototype 上的一个普通自有属性,可以被随意改写:
js
function MyArray() {}
MyArray.prototype.constructor = Array;
const a = new MyArray();
a.constructor === Array; // true —— 但 a 不是数组任何依赖 constructor 进行类型判断的代码都应该替换为 Array.isArray。
Array.isArray 的工作原理
Array.isArray 无法被绕过,是因为它在引擎内部检查的是堆对象的类型标记,这一标记在对象创建时便被设定,JS 层面没有 API 可以修改。
V8 中的实现(简化):
cpp
// src/builtins/builtins-array.cc
TF_BUILTIN(ArrayIsArray, ArrayBuiltinsAssembler) {
Node* value = Parameter(1);
Label is_array(this), not_array(this);
GotoIf(IsJSArray(value), &is_array);
GotoIf(IsJSProxy(value), &call_proxy);
Goto(¬_array);
BIND(&is_array);
Return(TrueConstant());
BIND(&call_proxy);
TailCallRuntime(Runtime::kArrayIsArray, context, value);
BIND(¬_array);
Return(FalseConstant());
}宏 IsJSArray 检查的是 V8 内部 Map 对象的 instance_type 字段。这个字段在 JSObject::New 分配时由引擎写入,代表了对象的结构类型。JS 中没有暴露修改该字段的途径,因此 Array.isArray 是唯一能确认真实数组的方法。
Proxy 是唯一需要特殊处理的例外:Array.isArray(new Proxy([], {})) 返回 true。规范要求 Array.isArray 对 Proxy 递归解包,最终判断目标对象(target)是否为数组。
类数组对象的检测
类数组(array-like)是指具有 length 属性和数字索引键的非数组对象,常见的有 arguments、NodeList、jQuery 对象、字符串等。
判断一个对象是否为类数组,需要组合多个特征进行排除:
js
function isArrayLike(obj) {
if (obj == null) return false;
const len = obj.length;
// length 必须是 number 且在合法范围内
if (typeof len !== 'number' || len < 0 || len > Number.MAX_SAFE_INTEGER) return false;
// 排除函数(函数有 length 属性但不是类数组)
if (typeof obj === 'function') return false;
// 排除真正的数组
if (Array.isArray(obj)) return false;
// 其余有合法 length 的对象视为类数组
return true;
}函数拥有自身的 length 属性(形参个数),因此需要在逻辑中明确排除。
在实际工程中,大多数需要处理类数组的场景已被 ES6 的解构和 rest 参数取代。arguments 对象在现代代码中几乎不应出现——箭头函数没有 arguments,推荐使用 rest 参数 (...args) 代替。
注意点
- 非数组的类数组对象无法使用
push、pop等数组原型方法,除非通过Function.prototype.apply或Array.prototype上的方法借用,但这样操作的仍然是对象自身。 - 跨执行上下文(iframe、
vm模块等)传递数组时,instanceof会失效,因为不同上下文的Array.prototype是不同的引用。 Array.isArray对 Proxy 的行为符合规范,但旧的 polyfill 可能没有正确处理这种情形。- 在极其少见的
--allow-natives-syntax等 V8 调试模式下,内部标记可以被手动操纵,但这不属于标准 JS 运行时。
参考链接
- js中判断对象的方法 —— 五种类型判断方式的完整对比与跨 Realm 分析
- ECMAScript 规范:Array.isArray ( https://tc39.es/ecma262/#sec-array.isarray )
- V8 源码
builtins-array.cc:相关逻辑实现
