前端知库

怎样准确判断一个值的类型?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。

三种方法各自的结果

下面把容易出错的情况放在一起,重点看每种方法在哪里给出“意外”的答案。

JavaScript
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 模块创建一个新的上下文来模拟。

JavaScript
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 的值类型与相等比较应该怎样理解?》和《隐式类型转换有哪些容易踩坑的规则?》。