Appearance
数组判断的安全边界
判断一个值是不是数组,JS 提供了四种路径。但其中三种可以被绕过,只有一种在引擎层面做了保证。这篇不重复基础用法,专门讲每种路径被绕过的具体方式和根因。
关于
typeof、Object.prototype.toString、instanceof、constructor、Array.isArray五种类型判断的完整对比,见 js中判断对象的方法。
绕过一:toString → Symbol.toStringTag
js
const fake = { [Symbol.toStringTag]: 'Array' };
Object.prototype.toString.call(fake); // '[object Array]' —— 骗过了绕过原理:Object.prototype.toString 不直接访问内部 [[Class]],而是读取对象的 @@toStringTag well-known symbol 属性。这个属性是可写的。
在 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
function LikeArray() {}
LikeArray.prototype = Object.create(Array.prototype);
const a = new LikeArray();
a instanceof Array; // true
a.push(1); // true —— 继承了 Array.prototype 的方法
Array.isArray(a); // false —— 只有这个是准的绕过原理:instanceof 沿 [[Prototype]] 链遍历,只要在链上找到 Array.prototype 就返回 true。Object.create(Array.prototype) 创建了一个以 Array.prototype 为原型的对象,将其设为 LikeArray.prototype,然后 new LikeArray() 创建的实例原型链上必然有 Array.prototype。
跨 Realm 场景下 instanceof 还会因为比较不同的 Array.prototype 引用而失准(详见 js中判断对象的方法 中的跨 Realm 分析)。
绕过三:constructor → 原型替换
js
function MyArray() {}
MyArray.prototype.constructor = Array;
const a = new MyArray();
a.constructor === Array; // true —— 但 a 不是数组constructor 是 prototype 上的一个普通自有属性,可以任意覆盖。任何依赖 constructor 做类型判断的代码都应该用 Array.isArray 替换。
为什么 Array.isArray 无法绕过
V8 中的实现(简化):
cpp
// src/builtins/builtins-array.cc
TF_BUILTIN(ArrayIsArray, ArrayBuiltinsAssembler) {
Node* value = Parameter(1);
Label is_array(this), not_array(this);
// 关键:检查对象的 Map(hidden class)中的 instance_type 字段
GotoIf(IsJSArray(value), &is_array);
GotoIf(IsJSProxy(value), &call_proxy);
Goto(¬_array);
BIND(&is_array);
Return(TrueConstant());
BIND(&call_proxy);
// 对 Proxy 递归调用 Array.isArray(proxy_target)
TailCallRuntime(Runtime::kArrayIsArray, context, value);
BIND(¬_array);
Return(FalseConstant());
}核心在 IsJSArray 宏——它检查的是 V8 内部 Map 对象的 instance_type 字段。这个字段在堆对象分配时(JSObject::New)由引擎设定,JS 层面没有任何 API 能修改它。这就是"不可绕过"的根本原因。
对 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;
// 排除真正的数组(用 Array.isArray 区分)
if (Array.isArray(obj)) return false;
// 其余有合法 length 的对象视为类数组
return true;
}在实际工程中,大多数需要处理伪数组的场景已被 ES6 解构和 rest 参数替代。arguments 对象在现代代码中几乎不应出现——箭头函数没有 arguments,建议用 rest 参数 (...args) 代替。
