怎样准确判断一个值的类型?typeof、instanceof 和 Object.prototype.toString 各有什么局限?
typeof 适合判断原始类型和函数,但 typeof null 是 "object",数组也是 "object"。instanceof 检查原型链,判断不了原始值,跨 iframe 或 realm 会失效。Object.prototype.toString.call 能区分 Array、Date、Null 等内置类型,但会被 Symbol.toStringTag 改写。数组用 Array.isArray 最可靠。
简答
写一个工具函数,参数可能是字符串、数组、日期或者
null,要分别处理。只用typeof会发现数组、日期和null都返回"object",分不出来,所以实际判断类型要几种方法配合。
typeof适合判断原始类型和函数,typeof null返回"object"是语言早期遗留下来的问题。instanceof检查构造函数的prototype在不在对象的原型链上,适合判断自定义类的实例,但对原始值总是false,跨 iframe 时也会失效,因为每个 iframe 有自己的一套Array、Object。
Object.prototype.toString.call(value)返回"[object Array]"、"[object Null]"这样的字符串,能区分大部分内置类型,常用来写通用的getType。它的局限是自定义类的实例都返回"[object Object]",而且对象可以通过Symbol.toStringTag改写结果。所以实践中按场景选:原始类型用
typeof加=== null,数组用Array.isArray,自定义类用instanceof,内置对象用toString。
三种方法各自的结果
下面把容易出错的情况放在一起,重点看每种方法在哪里给出“意外”的答案。
console.log(typeof null); // object
console.log(typeof []); // object
console.log(typeof NaN); // number
console.log(typeof function () {}); // function
console.log(typeof undeclaredVariable); // undefined
const bare = Object.create(null);
console.log(bare instanceof Object); // false
console.log(1 instanceof Number); // false
console.log(new Number(1) instanceof Number); // true
function getType(value) {
return Object.prototype.toString.call(value).slice(8, -1);
}
console.log(getType(null)); // Null
console.log(getType([])); // Array
console.log(getType(new Date())); // Date
console.log(getType(/a/)); // RegExp
console.log(getType(new Map())); // Map
class Point {}
console.log(getType(new Point())); // Object
const fake = { [Symbol.toStringTag]: 'Array' };
console.log(getType(fake)); // Array
console.log(Array.isArray(fake)); // false
为什么 typeof null 是 "object"?MDN 的解释是,JavaScript 最早的实现里,值由类型标签和值组成,对象的类型标签是 0,而 null 用空指针表示,标签也是 0,于是被当成了对象。这个结果一直保留到现在,所以判断 null 要直接写 value === null。
typeof 对没声明过的变量返回 "undefined",不会报错,这是它的一个方便之处。可是 MDN 也提醒,用 let、const、class 声明的变量在声明之前用 typeof 访问,会因为暂时性死区抛出 ReferenceError。另外函数(包括 class)返回 "function",NaN 虽然叫“不是数字”,类型仍然是 "number"。
instanceof 的规则是看构造函数的 prototype 是否出现在对象的原型链上。Object.create(null) 创建的对象没有原型,链上找不到 Object.prototype,所以是 false。原始值 1 不是对象,没有原型链,结果也是 false,只有用 new Number(1) 包装出来的对象才是 true。
getType 里的 slice(8, -1) 去掉的是前面的 "[object " 和最后的 "]"。它能认出 Null、Array、Date 等,但自定义的 Point 返回 Object。更大的问题在最后两行:MDN 说对象可以定义 Symbol.toStringTag 来改变 toString 的结果,所以一个普通对象就能伪装成 Array,而 Array.isArray 不受影响。
跨 realm 时 instanceof 为什么失效
MDN 把每个窗口、iframe 称为一个 realm,每个 realm 有自己的全局对象和内置构造函数。iframe 里创建的数组,原型是那个 iframe 的 Array.prototype,和当前页面的 Array.prototype 不是同一个对象。下面用 Node 的 vm 模块创建一个新的上下文来模拟。
const vm = require('node:vm');
const otherArray = vm.runInNewContext('[1, 2]');
console.log(otherArray instanceof Array); // false
console.log(Array.isArray(otherArray)); // true
console.log(Object.prototype.toString.call(otherArray)); // [object Array]
instanceof Array 返回 false,因为这个数组原型链上的是另一个上下文的 Array.prototype。MDN 推荐判断数组时优先用 Array.isArray(),正是因为它能跨 realm 工作。toString 也能认出来,因为 ECMAScript 规范里它判断 Array 用的是和 Array.isArray 相同的 IsArray 检查,不看原型链。
常见追问
instanceof 的结果能被改变吗?
能。构造函数如果定义了静态方法
Symbol.hasInstance,instanceof会优先调用它。另外修改对象的原型,或者修改构造函数的prototype属性,也会改变结果。所以instanceof判断的是原型关系,不是“对象由谁创建”。
怎样判断一个值是不是普通对象?
先排除
null和非对象,再看原型:Object.getPrototypeOf(value)是Object.prototype或null时,可以认为是普通对象。不同库对“普通对象”的定义不完全一样,跨 realm 的对象原型也不是当前的Object.prototype,写的时候要明确自己需要哪种定义。
typeof 和相等比较、类型转换有什么关系?
typeof只是读出值的类型,不会转换值。和==、隐式转换相关的规则可以看《JavaScript 的值类型与相等比较应该怎样理解?》和《隐式类型转换有哪些容易踩坑的规则?》。