先记住这个答案
typeof 只能区分原始类型和函数,数组、null、日期、正则全部返回 'object'。Object.prototype.toString 被调用时会返回 [object Xxx] 格式的字符串,Xxx 就是值的内部类型标签,比如数组是 Array、null 是 Null、正则是 RegExp。用法是 Object.prototype.toString.call(value),必须通过 call 把值作为 this 传进去,因为数组、日期这些对象都重写了自己的 toString。拿到结果后 slice(8, -1) 截掉前后缀,就能封装成一个通用的 getType。
- 用
call借用基类的toString [object Type]可提取内置类型- 自定义类通常只得到
Object Symbol.toStringTag能伪造标签
封装一个通用的 getType
项目里最常见的需求是写一个工具函数,传进任何值都能拿到准确的类型名,用来做参数校验或者深拷贝时的分支判断。
typeof 对比着看JavaScriptconst toStr = Object.prototype.toString
function getType(value) {
return toStr.call(value).slice(8, -1) // 去掉 "[object " 和 "]"
}
console.log(typeof [], typeof null, typeof /a/) // typeof 分不清
console.log(getType([1, 2]), getType(null), getType(/a/))
console.log(getType(undefined), getType(new Date(0)), getType(() => {}))
console.log(getType(new Map()), getType(Promise.resolve()), getType(1n))
console.log([1, 2].toString()) // 数组自己的 toString
console.log(toStr.call([1, 2])) // 基类方法才返回标签查看输出与解释
object object object
Array Null RegExp
Undefined Date Function
Map Promise BigInt
1,2
[object Array]'[object ' 正好 8 个字符,所以从下标 8 开始截,-1 去掉最后的 ]。
最后两行说明了为什么一定要 call。数组原型上有自己的 toString,作用是把元素用逗号拼起来,[1, 2].toString() 走的是它,得到 '1,2'。只有直接调用 Object.prototype 上的那个版本,才会返回类型标签。日期也一样,它自己的 toString 返回的是可读的时间字符串。
深拷贝就是典型用法:先用 getType 拿到类型,Array 走数组分支,Date 用 new Date(value) 复制,RegExp 用 new RegExp(value.source, value.flags) 复制,Map 和 Set 逐项复制,其余普通对象递归处理。
标签从哪来,什么时候会不准
标签有两个来源。数组、null、undefined、函数、错误对象、日期、正则这些,是引擎按对象的内部结构直接判断出来的。ES2015 之后新增的 Map、Set、Promise,以及 JSON、Math 这类对象,标签来自它们原型或自身上的 Symbol.toStringTag 属性。而且只要对象有这个属性,就会优先用它,这就带来了两个问题。
const toStr = Object.prototype.toString
class User {}
console.log(toStr.call(new User())) // 自定义类只是 Object
class Money {
get [Symbol.toStringTag]() { return 'Money' }
}
console.log(toStr.call(new Money())) // 自己定义标签
const fake = { [Symbol.toStringTag]: 'Array' }
console.log(toStr.call(fake), Array.isArray(fake)) // 标签能伪造
console.log(toStr.call(JSON), toStr.call(Math)) // 内置对象自带标签
console.log(toStr.call(function* () {}), toStr.call(async () => {}))查看输出与解释
[object Object]
[object Money]
[object Array] false
[object JSON] [object Math]
[object GeneratorFunction] [object AsyncFunction]第一个问题:自定义类的实例默认都是 [object Object],标签法认不出它是 User。判断自定义类型要用 instanceof,或者给类加上 Symbol.toStringTag。
第二个问题:任何对象都能把自己的标签设成 'Array',标签法就被骗了,而 Array.isArray 看的是对象的内部结构,不受影响。所以判断数组一律用 Array.isArray,标签法留给那些没有专用判断方法的类型。
最后一行是个小坑:生成器函数和 async 函数的标签是 GeneratorFunction 和 AsyncFunction,不是 Function。如果 isFunction 写成 getType(v) === 'Function',会漏掉它们,用 typeof v === 'function' 更稳。
- 跨 iframe 的对象
- 从 iframe 里拿到的数组,原型是 iframe 自己的
Array.prototype,instanceof Array会返回false。标签法和Array.isArray不依赖原型链,都能正确识别。这是标签法相比instanceof的一个实际优势。
容易答错的地方
- 直接调用
value.toString() [1, 2].toString()返回'1,2',new Date(0).toString()返回时间字符串,这些都是子类重写的版本。必须写Object.prototype.toString.call(value),才能调到返回标签的那个方法。- 以为标签是不可篡改的
Symbol.toStringTag是一个普通的属性,谁都能设置。第三方库给自己的对象加标签是正常用法,但同时意味着标签可以伪造。安全相关的判断要用Array.isArray这类基于内部结构的方法。- 以为它能认出自定义类
new User()得到的是[object Object],和普通字面量对象一样。标签法只对内置类型精确,业务类要用instanceof。
面试官还会怎么问?
它和 Array.isArray 判断数组,哪个更可靠?
Array.isArray 更可靠。它直接检查对象是不是数组的内部结构,不读 Symbol.toStringTag,也能识别跨 iframe 的数组。标签法的优势在于通用,一个函数能覆盖日期、正则、Map 等多种类型。
为什么 typeof null 是 'object',而这里能得到 Null?
typeof null 是 JavaScript 第一版实现留下的 bug:当时用低位标记类型,对象的标记是 0,而 null 是空指针,全 0,被误判为对象。为了不破坏已有代码一直没改。Object.prototype.toString 在规范里对 null 和 undefined 做了单独处理,所以能分别返回 [object Null] 和 [object Undefined]。
各种判断类型的方法怎么选?
原始类型和函数用 typeof,但要单独处理 null。数组用 Array.isArray。自定义类用 instanceof。日期、正则、Map 这类内置对象,或者需要一个通用的 getType 时,用 Object.prototype.toString。NaN 用 Number.isNaN。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。