Skip to content

JavaScript 类型判断:每种方法都有盲区

JS 里判断一个值的类型,看起来是基本功,实际工程中踩坑的点非常多。五种常见方法——typeofObject.prototype.toStringinstanceofconstructorArray.isArray——每一种都有自己的安全边界和绕过方式。关键不是"选哪个",而是理解每种方式的底层机制和适用场景。


typeof:实现路径与 null 的著名 bug

typeof 在 V8 中的执行路径:

text
typeof x
  → 字节码 TypeOf
  → C++ JSObject::TypeOf( isolate, value)
  → 根据 value 的 instance_type 返回对应的类型字符串

instance_type 的映射简化:

instance_typetypeof 返回值
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 获取):

规范行为:

  1. 如果 value 是 undefined'[object Undefined]'
  2. 如果 value 是 null'[object Null]'
  3. 对 value 调用 ToObject
  4. 读取 @@toStringTag 属性(通过 Symbol.toStringTag
  5. 如果该属性存在且是 string → '[object ' + tag + ']'
  6. 否则 → 使用内置的 [[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 返回 true

instanceof 的三个盲区:

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.prototype

2. 原型链污染。 攻击者可以修改 Constructor.prototype,让不该匹配的对象通过检测:

js
function LikeArray() {}
LikeArray.prototype = Object.create(Array.prototype);
const obj = new LikeArray();
obj instanceof Array; // true —— 原型链上有 Array.prototype

3. Proxy 干扰。 Proxy 可以拦截 getPrototypeOf,从而改变 instanceof 的判断结果。


constructor:比 instanceof 更脆弱

obj.constructor === Constructorinstanceof 更不可靠:

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
  → 否则 → false

instance_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唯一不可绕过
判断 nullx === nulltypeof null 返回 'object'
判断对象(排除 null)typeof x === 'object' && x !== null最简方案
区分 object/functiontypeoftypeof fn === 'function' 是可靠的
跨 Realm 对象Object.prototype.toString.call比 instanceof 稳定
类数组对象检查 length 是否为数字 + 是否有 callee(arguments)按特征判断
TypeScript 类型守卫typeof + instanceof + 自定义 type guard编译时 + 运行时互补

最重要的原则是:不要用单一方法覆盖所有场景,应根据需要判断的具体特征选择最稳定的方法。