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