Skip to main content
类型系统我最早靠 typeof 判断,吃过 typeof arr === 'array' 的亏,才去补隐式转换和 [] == ![] 这些规则。

Q1: typeof 能判断哪些类型?有什么坑?

1

1995年: JS 的类型标签设计

JavaScript 的值在底层以 32 位存储,前 3 位表示类型标签:000 对象、1 整数、010 浮点数、100 字符串、110 布尔值。
2

致命痛点: typeof null === 'object'

null 的机器码全为零(空指针),前三位恰好是 000,所以被误判为对象。修复会破坏现有代码,所以一直保留。
3

typeof 的八种返回值

"number"、"string"、"boolean"、"undefined"、"symbol"、"bigint"、"function"、"object"。注意:没有 "null"、"array"、"date"、"regexp"。
4

核心机制: V8 的 Typeof 实现

V8 的 Typeof 操作检查值的类型标签,对于 HeapObject 检查 Map 中的 instance_type 字段。null 的 Map 是 null_map,但它的 instance_type 被设置为 JS_OBJECT_TYPE。
5

关键洞察: 用 Object.prototype.toString.call() 替代 typeof

它读取对象的内置类型信息(ES6 后主要通过 Symbol.toStringTag 暴露),不受原型链上自定义属性影响。
类型系统解决的核心问题:
typeof 就像一个粗心的快递分拣员——大部分包裹(基本类型)都能正确分类,但遇到 null 时它会贴错标签(“object”),遇到数组和正则时只会说”都是包裹”(“object”)。而 Object.prototype.toString.call() 是一个精确的 X 光扫描仪,能看穿每个包裹的内部结构,准确告诉你这是”Array”还是”Null”还是”RegExp”。至于 ==,它就像一个太好说话的收银员——0、''、false 它都当一样的来收,导致各种反直觉的结果。
typeof 的完整结果:
typeof 只能准确判断基本类型(除了 null),判断引用类型请用 Object.prototype.toString.call()。

typeof 的坑

用 typeof 判断类型,你会踩无数坑。

Object.prototype.toString.call()

准确判断所有类型,不受原型链影响。

Q2: 怎么可靠地判断所有类型?

instanceof 的原理和局限

instanceof 检查构造函数的 prototype 是否在对象的原型链上。它不能判断基本类型,跨 iframe 也会失效。

Symbol.toStringTag 自定义类型

ES6 引入 Symbol.toStringTag,允许自定义类覆盖 Object.prototype.toString.call() 的返回值:

Array.isArray 的特殊性

Array.isArray 是 ES5 引入的专门方法,能跨 iframe 判断数组,不依赖原型链:

Q3: 类型转换有什么规则?== 的隐式转换?

+ 运算符的多义性

+ 运算符同时用于数字加法和字符串拼接,规则是:如果任一操作数是字符串,执行字符串拼接(ToString)。- 运算符只做数字运算(ToNumber)。

Symbol.toPrimitive 自定义转换

ES6 引入 Symbol.toPrimitive,允许对象自定义类型转换行为,优先级高于 valueOf 和 toString:
隐式类型转换的本质是规范中的 ToPrimitive 抽象操作。当对象需要被转换为原始值时:
  1. 先调用 [Symbol.toPrimitive](hint)
  2. 没有的话,根据 hint 调用 valueOf() 或 toString()
  3. hint === 'number' → valueOf() → toString()
  4. hint === 'string' → toString() → valueOf()
  5. hint === 'default' → valueOf() → toString()
实际开发中我只用 ===,避免隐式转换带来的 bug。ESLint 的 eqeqeq 规则可以强制使用 ===。

底层源码:V8 中值的内存布局

V8 底层将值分为寄存器级存储的 SMI(小整数) 和堆分配的 HeapObject(详见 V8 源码:src/objects/objects.h):
  • SMI:直接存储在 32/64 位的 tagged pointer 中,最低位是 0(表示 SMI)
  • HeapObject:指针指向堆上的对象,最低位是 1,对象的第一个字段是 Map
  • Oddball:特殊的 HeapObject,包括 undefined、null、true、false——它们有预分配的全局单例
typeof 的实现:V8 的 Typeof 操作检查值的类型标签,对于 HeapObject 检查 Map 中的 instance_type 字段。null 的 Map 是 null_map,但它的 instance_type 被错误地设置为 JS_OBJECT_TYPE——这就是 bug 的根源。

十、及格回答 vs 满分回答

问题:“说说 JavaScript 的类型系统”

及格回答(60-70分):
“JS 有 7 种基本类型和 Object。typeof 有坑,null 返回 object。用 Object.prototype.toString.call() 可以准确判断类型。== 会隐式转换,建议用 ===。”
问题出在:知道规则但不理解底层。 满分回答(90-100分):
“V8 底层将值分为寄存器级存储的 SMI(小整数) 和堆分配的 HeapObject(首字段为描述形状的 Map)。typeof null === 'object' 源于早期 JS 机器码前三位为类型标签的设计(对象为 000,null 全零恰好吻合)。因修复成本过高被作为规范永久保留。[1]”
“隐式类型转换的本质是规范中的 ToPrimitive 抽象操作。它会根据 hint 调用对象的 valueOf 或 toString。+ 运算符对字符串有特殊处理——如果任一操作数是字符串,执行字符串拼接(ToString)。- 运算符只做数字运算,先把字符串转为数字(ToNumber)。所以 1 + '1' 是 '11',1 - '1' 是 0。[2]”
“实战中可通过实现对象的 Symbol.toPrimitive 来实现最高优先级的自定义类型转换。Object.is() 比 === 更严格:Object.is(NaN, NaN) 返回 true,Object.is(+0, -0) 返回 false。[3]“

常见追问

答题要点: 从适用范围和跨环境两个维度分析。高分回答: “typeof 的局限:不能区分 null 和 object(历史 bug)、不能区分数组和对象和正则和日期(都返回 ‘object’)、NaN 返回 ‘number’。instanceof 的局限:不能判断基本类型(1 instanceof Number 返回 false)、跨 iframe 失效(不同 iframe 的 Array 构造函数不同)。最可靠的方案是 Object.prototype.toString.call()。”
答题要点: 说出内部方法和 Symbol.toStringTag 的关系。高分回答: “它早期依赖 ES5 的 [[Class]] 内部槽;ES6 起规范改用内置类型槽配合 Symbol.toStringTag 生成 [object Xxx]。内置对象都有确定的类型标签,不受原型链上自定义属性影响。自定义类可以通过 Symbol.toStringTag 自定义返回值。例如 class MyArray { get [Symbol.toStringTag]() { return 'MyArray'; } }。”
答题要点: 逐步推导完整的类型转换链。高分回答: “完整推导链路:![] 先执行,[] 是 truthy,所以 ![] 是 false。然后 [] == false,触发布尔转数字 [] == 0。再触发对象转原始值 '' == 0。最后字符串转数字 0 == 0,结果为 true。这类考题暴露了 == 的危险本质——它是一个隐式的多层类型转换规则引擎,极其脆弱且反直觉。”
答题要点: 说出 + 运算符的多义性。高分回答: “+ 运算符对字符串有特殊处理——如果任一操作数是字符串,执行字符串拼接(ToString)。所以 1 + '1' 是 '11'。- 运算符只做数字运算,先把字符串转为数字(ToNumber)。所以 1 - '1' 是 0。这不是类型转换规则的 bug,而是 + 运算符的多义性——它同时用于数字加法和字符串拼接。”
答题要点: 栈内存 vs 堆内存的区别。高分回答: “基本类型(number、string、boolean、null、undefined、symbol、bigint)的值直接保存在变量槽里(引擎内部可能是栈或寄存器,字符串内容仍在堆上)。引用类型(object、array、function)的对象本体在堆内存中,变量保存的是指向堆的引用。
这也是为什么函数参数传递对象时是’按引用传递的效果’——本质是按值传递引用地址。”
答题要点: 从语义和规范两个层面解释。高分回答: “语义上:undefined 表示变量已声明但未赋值,是引擎自动赋的值;null 表示变量被显式赋值为空,是开发者主动设置的。规范上:null == undefined 是规范明确规定为 true 的,这是唯一一条不需要类型转换的 == 规则。
答题要点: 结合 toString 和 typeof 实现完整的类型判断。高分回答:
核心思路:先用 typeof 处理基本类型(快),再用 toString 处理引用类型(准)。”
答题要点: 错误处理和类型还原。高分回答:
注意:JSON.parse 不支持 undefined、Function、Symbol 等值。需要日期等特殊类型时,应配合自定义的 reviver 函数处理。“

延伸阅读与引用参考