Skip to content
null 返回
JavaScript 类型判断的机制与边界
概述
判断 JavaScript 中的值类型,可用的工具有 typeof、Object.prototype.toString、instanceof、constructor 以及 Array.isArray。它们各自依托不同的内部机制,没有哪一种能覆盖所有边界情况。只有理解了每种工具的实际行为,才能在不同场景下做出稳妥的取舍。
typeof 运算符
基本概念
typeof 是一元运算符,对任意值返回一个表示其类型的字符串。
工作原理
在 V8 中,typeof 的执行路径可以简化为:
text
typeof x
→ 字节码 TypeOf
→ C++ JSObject::TypeOf(isolate, value)
→ 根据 value 的 instance_type 返回类型字符串instance_type 是 V8 内部用来区分对象类别的枚举值。部分映射关系如下表。
| instance_type | typeof 返回值 |
|---|---|
| ODDBALL (null) | 'object' |
| ODDBALL (undefined) | 'undefined' |
| ODDBALL (true/false) | 'boolean' |
| HEAP_NUMBER / SMALL_INTEGER | 'number' |
| STRING | 'string' |
| SYMBOL | 'symbol' |
| BIGINT | 'bigint' |
| JS_FUNCTION | 'function' |
| 其余所有对象 | 'object' |
null 返回 'object' 的成因
这个行为可以追溯到 JavaScript 的第一版(1995 年)。当时的实现用类型标签加实际值来表示一个值:对象标签为 0,而 null 的机器码是全零(0x00)。typeof 读取到标签 0 便给出 'object',这是一个实现上的延续,并非有意设计。后来多次修复提案都因向后兼容而被否决。在实际判断中,对 null 应使用显式比较 x === null。
限制
typeof 不能区分除函数以外的对象类型。数组、Date、RegExp、Error、Map 等全部返回 'object'。唯一例外是实现了 [[Call]] 内部方法的对象(即函数)会返回 'function',这是规范明确规定的特例。
Object.prototype.toString 方法
基本用法
Object.prototype.toString.call(value) 能够返回比 typeof 更具体的类型字符串。
工作原理
ES6 之前,该方法主要读取内部 [[Class]] 槽位。ES6 之后执行流程如下:
- 若 value 为
undefined,返回'[object Undefined]'。 - 若 value 为
null,返回'[object Null]'。 - 将 value 转换为对象(
ToObject)。 - 查找
@@toStringTag(即Symbol.toStringTag)属性。 - 若该属性存在且为字符串,返回
'[object ' + tag + ']'。 - 否则使用引擎内置的类型名称。
示例
@@toStringTag 可以被任意赋值,因此返回值可以被刻意扭曲:
js
const fakeArray = {
[Symbol.toStringTag]: 'Array'
};
Object.prototype.toString.call(fakeArray); // '[object Array]'
// 伪装为 Promise 的普通对象
const fakePromise = {
then() {},
[Symbol.toStringTag]: 'Promise'
};
Object.prototype.toString.call(fakePromise); // '[object Promise]'多数内置类已经为 Symbol.toStringTag 设置了只读的 getter 属性。例如 Array.prototype:
js
Object.getOwnPropertyDescriptor(Array.prototype, Symbol.toStringTag);
// { get: ..., set: undefined, enumerable: false, configurable: true }虽然 configurable: true 允许通过 Object.defineProperty 覆盖该 getter,但直接赋值 Array.prototype[Symbol.toStringTag] = 'NotArray' 只会新建一个数据属性,不会影响原有的 getter 行为。这种设计使得标签可以被有意识地替换,而不容易被意外修改。
注意点
对于需要精确判别的场景,不应该只依赖 toString。判断数组应使用 Array.isArray;判断 Promise 可结合 instanceof Promise 与 thenable 检测(typeof x.then === 'function');判断原始类型用 typeof 更直接。
instanceof 运算符
基本概念
instanceof 检查构造函数的原型是否出现在对象的原型链上。它本质上是原型链查找,而非类型判断。
示例
即使对象不是通过 new 创建的,只要原型链上存在对应的 prototype,结果就会是 true:
js
function Foo() {}
const obj = {};
Object.setPrototypeOf(obj, Foo.prototype);
obj instanceof Foo; // true限制一:跨 Realm 失真
不同的执行上下文(iframe、web worker、vm 模块)拥有各自独立的内置构造函数和原型对象。在 Realm A 创建的数组,在 Realm B 中 arr instanceof Array 会返回 false,因为比对的是 Realm B 的 Array.prototype。
js
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限制二:原型链污染
构造函数的 prototype 可以被替换或修改,使得不相关的对象也能通过检查。
js
function LikeArray() {}
LikeArray.prototype = Object.create(Array.prototype);
const obj = new LikeArray();
obj instanceof Array; // true限制三:Proxy 干扰
Proxy 可以拦截 getPrototypeOf 操作,从而改变 instanceof 的判断结果。
js
const target = {};
const proxy = new Proxy(target, {
getPrototypeOf() {
return Array.prototype;
}
});
proxy instanceof Array; // trueconstructor 属性
基本概念
通过 obj.constructor === Constructor 判断类型,依赖的是对象的构造函数引用。这是一种极其脆弱的判别方式。
示例
手动替换原型后,constructor 引用会丢失:
js
function Foo() {}
Foo.prototype = { method() {} };
const foo = new Foo();
foo.constructor === Foo; // false —— 原型的 constructor 已被覆盖此外,constructor 属性本身可写、可配置,随时可能被篡改。在任何需要可靠判断的场景下都不应使用它。
Array.isArray 方法
基本概念
Array.isArray 是目前唯一无法从 JavaScript 层面绕过的数组判别方法。
工作原理
它直接在引擎内部调用 [[IsArray]] 抽象操作,不依赖原型链,也不读取任何可被修改的属性。在 V8 中实现路径为:
text
Array.isArray(value)
→ C++ Builtin ArrayIsArray
→ 检查 value 的 Map(hidden class)的 instance_type
→ 如果 instance_type === JS_ARRAY_TYPE → true
→ 否则 → falseinstance_type 在对象创建时确定,没有任何 JavaScript API 可以变更。跨 Realm 场景同样安全:iframe 中的数组在创建时就会被标记为 JS_ARRAY_TYPE,Array.isArray 检查的正是这个内部标识,与 Array.prototype 的引用无关。
Array.isArray 自 ES5.1(§15.4.3.2)引入,IE9+ 及所有现代浏览器均支持。
应用:类型判断策略
不同场景下可以参考以下判断方式:
| 场景 | 方法 | 原因 |
|---|---|---|
| 判断数组 | Array.isArray | 引擎内部标识,不可绕过 |
| 判断 null | x === null | typeof null 会误判为 'object' |
| 判断对象且排除 null | typeof x === 'object' && x !== null | 直接判断非 null 引用类型 |
| 区分 function / object | typeof | 对函数判断可靠 |
| 跨 Realm 对象 | Object.prototype.toString.call | 不依赖原型链引用 |
| 类数组对象 | 检查 length 是否为数字,结合 callee 等特征 | 按特性判断 |
| TypeScript 类型守卫 | typeof、instanceof 配合自定义 type guard | 编译时与运行时互补 |
类型判断的目标并非寻找万能方法,而是针对所需特征选择最稳定、最少可被篡改的检测方式。
参考链接
- ECMAScript 规范
- The typeof Operator:https://tc39.es/ecma262/#sec-typeof-operator
- Object.prototype.toString:https://tc39.es/ecma262/#sec-object.prototype.tostring
- instanceof Operator:https://tc39.es/ecma262/#sec-instanceofoperator
- Array.isArray:https://tc39.es/ecma262/#sec-array.isarray
- @@toStringTag:https://tc39.es/ecma262/#sec-symbol.tostringtag
- V8 源码
- JSObject::TypeOf 实现:
src/objects/objects.cc - ArrayIsArray 内建函数:
src/builtins/builtins-array.cc
- JSObject::TypeOf 实现:
