Appearance
JavaScript 类型判断:每种方法都有盲区
JS 里判断一个值的类型,看起来是基本功,实际工程中踩坑的点非常多。五种常见方法——typeof、Object.prototype.toString、instanceof、constructor、Array.isArray——每一种都有自己的安全边界和绕过方式。关键不是"选哪个",而是理解每种方式的底层机制和适用场景。
typeof:实现路径与 null 的著名 bug
typeof 在 V8 中的执行路径:
text
typeof x
→ 字节码 TypeOf
→ C++ JSObject::TypeOf( isolate, value)
→ 根据 value 的 instance_type 返回对应的类型字符串instance_type 的映射简化:
| instance_type | typeof 返回值 |
|---|---|
| ODDBALL (null) | 'object' ← 著名 bug |
| ODDBALL (undefined) | 'undefined' |
| ODDBALL (true/false) | 'boolean' |
| HEAP_NUMBER, SMALL_INTEGER | 'number' |
| STRING | 'string' |
| SYMBOL | 'symbol' |
| BIGINT | 'bigint' |
| JS_FUNCTION | 'function' |
| 其余所有对象 | 'object' |
typeof null === 'object' 的原因追溯到 JS 第一版(1995 年),值在内存中用类型标签 + 实际值表示。对象标签是 0,null 的机器码是 0x00(全零),typeof 读到标签 0 → 返回 'object'。这是实现层面的遗留问题,不是"设计缺陷"——当时没人预料到 JS 会活这么久。
修复提案出现过多次,但都因为向后兼容被否决了。实际工程中,如果需要判断 null,加显式比较 x === null。
typeof 还有一个盲区:它不能区分不同类型的对象。数组、Date、RegExp、Error、Map 全部返回 'object'。函数返回 'function' 是特例——规范中 typeof 对实现了 [[Call]] 内部方法的对象返回 'function'。
Object.prototype.toString:可篡改的标签
Object.prototype.toString.call(value) 比 typeof 更精确的原因是它读取对象的内部 [[Class]] 槽位(ES6 后改为通过 @@toStringTag well-known symbol 获取):
规范行为:
- 如果 value 是
undefined→'[object Undefined]' - 如果 value 是
null→'[object Null]' - 对 value 调用
ToObject - 读取
@@toStringTag属性(通过Symbol.toStringTag) - 如果该属性存在且是 string →
'[object ' + tag + ']' - 否则 → 使用内置的
[[Class]]名称
@@toStringTag 是可写的,所以可以被篡改:
js
const fakeArray = {
[Symbol.toStringTag]: 'Array'
};
Object.prototype.toString.call(fakeArray); // '[object Array]'
// 也可以用来伪装成任何类型
const fakePromise = {
then() {},
[Symbol.toStringTag]: 'Promise'
};
Object.prototype.toString.call(fakePromise); // '[object Promise]'防御策略:对需要精确判断类型的场景,不要只依赖 toString。对数组用 Array.isArray,对 Promise 用 instanceof Promise 配合 thenable 检测(typeof x.then === 'function'),对原始类型用 typeof。
大部分内置类已经为 Symbol.toStringTag 设置了 getter:
js
Object.getOwnPropertyDescriptor(Array.prototype, Symbol.toStringTag);
// { get: ..., set: undefined, enumerable: false, configurable: true }注意 configurable: true 意味着你可以在原型上覆盖它——但由于是 getter,直接赋值 Array.prototype[Symbol.toStringTag] = 'NotArray' 会创建一个新的数据属性而不是覆盖 getter。需要通过 Object.defineProperty 才能覆盖。这说明框架作者有意让它"可被覆盖但不会意外覆盖"。
instanceof:原型链遍历,不是类型检查
obj instanceof Constructor 的逻辑是:判断 Constructor.prototype 是否出现在 obj 的原型链上。 这是原型链查找,不是真正意义上的"类型判断"。
js
function Foo() {}
const obj = {};
Object.setPrototypeOf(obj, Foo.prototype);
obj instanceof Foo; // true —— 没有经过 new Foo(),但 instanceof 返回 trueinstanceof 的三个盲区:
1. 跨 Realm 失真。 不同 iframe/worker/vm context 有各自独立的内置构造函数和原型对象。Realm A 创建的数组,在 Realm B 里 instanceof Array 返回 false——因为比较的是两个不同的 Array.prototype 引用。
js
// iframe 场景
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iframeArray = iframe.contentWindow.Array;
const arr = new iframeArray(1, 2, 3);
arr instanceof Array; // false —— 这是 window.Array.prototype,不是 iframe 的 Array.prototype2. 原型链污染。 攻击者可以修改 Constructor.prototype,让不该匹配的对象通过检测:
js
function LikeArray() {}
LikeArray.prototype = Object.create(Array.prototype);
const obj = new LikeArray();
obj instanceof Array; // true —— 原型链上有 Array.prototype3. Proxy 干扰。 Proxy 可以拦截 getPrototypeOf,从而改变 instanceof 的判断结果。
constructor:比 instanceof 更脆弱
obj.constructor === Constructor 比 instanceof 更不可靠:
js
function Foo() {}
Foo.prototype = { method() {} }; // 替换 prototype
const foo = new Foo();
foo.constructor === Foo; // false —— constructor 是 prototype 上的属性,被替换后丢失constructor 可以被任意修改、可以被覆盖、在手动替换原型后丢失。不能作为可靠的类型判断手段。
Array.isArray:唯一不可绕过的方案
Array.isArray 是判断数组最可靠的方法。不可绕过的原因是它在引擎内部通过 [[IsArray]] 抽象操作实现,不依赖原型链、不读取可被篡改的属性。
V8 中 Array.isArray 的实现路径:
text
Array.isArray(value)
→ C++ Builtin ArrayIsArray
→ 检查 value 的 Map(hidden class)的 instance_type
→ 如果 instance_type === JS_ARRAY_TYPE → true
→ 否则 → falseinstance_type 是引擎内部的对象分类标识,在对象创建时确定,运行时不可变。没有 JS 层面的 API 可以修改它。
跨 Realm 也安全:iframe 中的数组在 iframe 的 V8 isolate 中创建,它的 instance_type 就是 JS_ARRAY_TYPE——Array.isArray 检查的是这个内部标识,不是 Array.prototype 引用。
ES5.1(不是 ES6)就引入了 Array.isArray(ES5.1 §15.4.3.2)。在 IE9+ 和所有现代浏览器中都可用。
类型判断的工程策略
按场景选择:
| 场景 | 推荐方法 | 原因 |
|---|---|---|
| 判断数组 | Array.isArray | 唯一不可绕过 |
| 判断 null | x === null | typeof null 返回 'object' |
| 判断对象(排除 null) | typeof x === 'object' && x !== null | 最简方案 |
| 区分 object/function | typeof | typeof fn === 'function' 是可靠的 |
| 跨 Realm 对象 | Object.prototype.toString.call | 比 instanceof 稳定 |
| 类数组对象 | 检查 length 是否为数字 + 是否有 callee(arguments) | 按特征判断 |
| TypeScript 类型守卫 | typeof + instanceof + 自定义 type guard | 编译时 + 运行时互补 |
最重要的原则是:不要用单一方法覆盖所有场景,应根据需要判断的具体特征选择最稳定的方法。
