最近帮团队做校招一面,技术面环节我习惯先从八股切入,十个人里面至少八个能把 typeof 和 instanceof 的定义背得一字不差:typeof 返回一个值的类型字符串,instanceof 用来判断某个对象是否是某个构造函数的实例。但等我追问几个边角场景——typeof null 为什么是 "object"、跨 iframe 之后的 instanceof 还准不准、Object.create(null) instanceof Object到底是 true 还是 false,能答完整的人就少得多了。这两个操作符是前端面试题里的常青树,尤其在 2026 年的八股文考核里依然高频出现,而且它们直接关系到你写通用工具函数、框架源码阅读、甚至 TS 类型推导的理解深度。这篇就把 typeof/instanceof 彻底拆一遍,顺带把keyof typeof、Object.prototype.toString、Symbol.hasInstance这些延伸考点一次性讲透。
无论你是准备面试的初中级前端,还是刚要系统补 JS 基础的新人,或者工作中正在为封装类型判断工具发愁,这篇都值得看完。我尽量不堆官方定义的废话,直接讲“面试官问什么、源码里怎么用、工程上怎么避坑”。
1. typeof:七个返回值之外的四个深水区
1.1 为什么 typeof 的返回值只有这些
先看一张最基础的输出清单,你要是能一眼看出哪些是坑,说明基础已经过关了:
typeof undefined; // "undefined" typeof true; // "boolean" typeof 42; // "number" typeof 42n; // "bigint" typeof "hello"; // "string" typeof Symbol(); // "symbol" typeof {}; // "object" typeof []; // "object" typeof null; // "object" ← 经典 bug typeof function () {}; // "function" typeof class Foo {}; // "function"注意 ES2020 之后 typeof 一共会返回 8 种不同的字符串:7 种是数据类型的名字,外加一个特例 "function"。为什么函数不是对象?因为 typeof 底层走的是引擎内部对值类型的“类型标签”标记,函数对象在内部有独立的 [[Call]] 行为和类型标签,所以从结果上看它成了唯一一个被单独拎出来的对象子类型。
很多同学不理解的一点是:typeof 并不是把值转换成字符串,而是引擎直接读取值的内部类型位信息。对于原始类型,这个判断又快又准;但对于对象类型,它只能告诉你“这是个 object”,至于这个 object 是数组、日期、Map 还是普通对象,它一概不管。所以 typeof 的正确使用边界应该是:判断原始类型用 typeof,判断对象内部构造类型,得交给下一章说的 instanceof 或者更底层的 Object.prototype.toString。
1.2 面试里最容易翻车的四个 typeof 场景
第一个必须说透的是typeof null === "object"。这是 JS 从诞生起就带着的历史包袱,早期实现里引擎用低位若干 bit 表示值的类型,对象类型标签是 0,null 的空指针地址正好也是 0,于是被误判成了 object。这个 bug 早就被发现了,但为了兼容线上存量代码一直没改。面试加分回答是补一句:如果真要精确判断 null,得用value === null或者Object.prototype.toString.call(value) === "[object Null]",不能用!value去代替,因为0、""、false也会让!value成立。
第二个场景是 typeof 能“安全”访问未声明的变量。比如一个全局 API 只在某个浏览器环境里有,直接写window.someAPI.xxx会抛 ReferenceError,但typeof someAPI !== "undefined"是安全的,不会报错。这背后的原因是 typeof 操作符不会强制求值它的操作数,只需要读取类型的元信息就够了。很多老代码里判断某个全局库是否存在,用的就是这个技巧。
第三个场景是 typeof 遇到暂时性死区(TDZ)会立刻翻车。举个例子:
typeof x; // ReferenceError: Cannot access 'x' before initialization let x = 1;这是最反直觉的地方,因为很多人以为 typeof 是“绝对安全”的。实际上 let/const 声明的变量在初始化之前有一个不可访问的区间,typeof 试图读取这个区间里变量的类型时会直接抛错,而不是返回 undefined。只有“未声明”和“已声明未赋值”两种情况才会返回 "undefined"。
第四个场景是包装对象。typeof new Number(1)返回 "object" 而不是 "number",因为 new 出来的必然是个对象。面试官如果问“为什么new Number(1) === 1是 false”,本质原因是两者类型都不同,一个 object 一个 number。这里最容易让人混淆的是字符串方法明明可以直接在字面量上调用,比如"abc".length,这其实是引擎在运行时临时把原始字符串包装成了 String 对象,方法调用完就销毁了,原始值的类型仍然没变。
2. instanceof:表面是类型判断,底层是原型链搜索
2.1 判定规则与几个反直觉例子
instanceof 的完整语义是:检查右侧构造函数的 prototype 对象,是否出现在左侧对象实例的原型链上。很多人只记“判断是不是某个类的实例”,却忽略了它的运行机制是沿着__proto__一路向上找。因为机制是“找原型链”,所以它天然能判断父子继承关系:
class Animal {} class Dog extends Animal {} const dog = new Dog(); dog instanceof Dog; // true dog instanceof Animal; // true,因为 Dog.prototype 的原型链上有 Animal.prototype dog instanceof Object; // true,所有普通对象链路的尽头都有 Object.prototype几个反直觉的例子你得提前熟悉。[] instanceof Array是 true,[] instanceof Object也是 true,因为数组的原型链是Array.prototype -> Object.prototype -> null,所以它同时满足两个判断。Object.create(null)创建的对象的原型是 null,不在任何原型链上,所以Object.create(null) instanceof Object是 false,这个细节很多面经里都会考。
instanceof 还有两个硬性规则。左侧如果根本不是对象,比如字符串原语,规范里直接返回 false,所以"hello" instanceof String是 false,除非你写new String("hello") instanceof String。右侧必须是个可调用的函数,并且要有 prototype 属性,否则会抛 TypeError。箭头函数没有自己的 prototype,所以({}) instanceof (() => {})会直接报错。
更进阶的玩法是通过Symbol.hasInstance改写 instanceof 的判断逻辑。这个静态符号方法允许你完全重定义右侧对象的 instanceof 行为:
class FancyString { static [Symbol.hasInstance](value) { return typeof value === "string" && value.length > 5; } } "hello world".length > 5; // 此时 "hello world" instanceof FancyString → true "hi".length > 5; // "hi" instanceof FancyString → false这种写法在业务代码里不常见,但很能体现你对 ES6 符号和运算符重载的理解深度。面试时如果能主动提到 Symbol.hasInstance,通常能把普通的八股问答抬到一个加分的位置。
2.2 跨 realm 失效与手写 instanceof 完整实现
instanceof 最常见的坑是跨 realm(跨全局环境)判断失效。典型场景是 iframe。每个 iframe 有独立的 window 和独立的构造函数,你在主页面写[] instanceof Array没问题,但拿到的 iframe 内部创建的数组,用主页面 Array 去判断就会返回 false,因为两个构造函数的 prototype 不是同一个对象。
const iframe = document.createElement("iframe"); document.body.appendChild(iframe); const iframeArray = new iframe.contentWindow.Array(); iframeArray instanceof Array; // false Array.isArray(iframeArray); // true所以工程上判断数组不要用 instanceof,直接用Array.isArray,它是为跨 realm 场景专门设计的。同理,判断一个值是不是某个内置类型,更稳的方式是Object.prototype.toString.call(value),它读的是对象内部的 Symbol.toStringTag 标签,基本不依赖当前执行环境。
手写 instanceof 是前端机试题里点名率很高的题。完整版应该考虑三个边界:左侧不是对象直接返回 false、右侧不是函数抛 TypeError、原型链走完后没找到返回 false。代码可以这样写:
function myInstanceof(left, right) { if (typeof right !== "function" || !right.prototype) { throw new TypeError("Right-hand side of instanceof is not callable"); } if (left === null || (typeof left !== "object" && typeof left !== "function")) { return false; } let proto = Object.getPrototypeOf(left); while (proto !== null) { if (proto === right.prototype) { return true; } proto = Object.getPrototypeOf(proto); } return false; }这里有几个细节值得展开。为什么要先判断 right.prototype?因为箭头函数没有 prototype 属性,直接用它做右操作数会抛错,提前抛一个更清晰的错误更符合工程实践。为什么要判左侧是 null?因为Object.getPrototypeOf(null)本身就是非法操作,会抛 TypeError。为什么循环条件用proto !== null而不是proto?因为对象原型链的终点一定是 null,用严格不等于可以避免误把某个 falsy 对象当成链的终点。如果你留心,会发现手写 instanceof 的代码本身就在考原型链、异常处理、TypeError 语义三件事,比单纯背一句“沿着原型链找”要有价值得多。
3. 类型判断全家桶:面试官想要的对比都在这里
3.1 五种判断方式横向对比
面试官问到 typeof/instanceof 时,你最好主动把整套类型判断方案摆出来对比,而不是被动一问一答。常规的对比维度有五个:typeof、instanceof、constructor、Object.prototype.toString、Array.isArray。它们的核心差异可以压成一张表:
| 判断方式 | 示例结果 | 优点 | 致命缺点 |
|---|---|---|---|
| typeof | "string"、"object"、"function" | 语法简单,能安全区分所有原始类型 | 对象类型只能得到 "object",分不清数组/Date/正则,且 null 误判为 object |
| instanceof | true / false | 能判断自定义类的实例,还能体现继承关系 | 跨 realm 失效;右侧必须是可调用函数;原始类型一律 false |
| constructor | Array、String、Object 等 | 写法直观,可以直接拿到构造函数名 | 属性可被改写,原型链上的 constructor 也可能被继承,安全性差 |
| Object.prototype.toString.call | "[object Array]"、"[object Null]" | 识别范围广,内置类型全覆盖,跨 realm 基本稳定 | 只能得到笼统的内置类型名,自定义类的实例统一返回 "[object Object]" |
| Array.isArray | true / false | 官方专门给数组设计的,跨 realm 稳定 | 只解决数组这一个场景 |
我自己实际封装通用类型判断函数时,长期用的方案是“typeof 优先 + Object.prototype.toString 兜底”的组合,兼顾性能和准确度。一个可落地的版本长这样:
function getType(value) { if (value === null) { return "null"; } const rawType = typeof value; if (rawType !== "object" && rawType !== "function") { return rawType; } return Object.prototype.toString.call(value).slice(8, -1).toLowerCase(); } getType(null); // "null" getType(undefined); // "undefined" getType([]); // "array" getType(new Date()); // "date" getType(/abc/); // "regexp" getType(new Map()); // "map"注意 Object.prototype.toString 返回的格式固定是[object 类型名],所以 slice(8, -1) 能干净地截掉前缀和后括号。拿到的类型名首字母是大写,再 toLowerCase 一下就和 typeof 的返回值风格统一了。这个函数在 lodash 等工具库内部有类似实现,本质上是把“标签位读取”这件事标准化了。
3.2 那些大厂源码里的实际用法
总有人说八股文脱离实际,但 typeof/instanceof 在框架源码里的出镜率极高。拿 React 举例,React 元素对象上有一个特殊的$$typeof字段,它是Symbol.for("react.element")。React 源码在判断一个对象是不是合法 ReactElement 时,会同时检查对象类型和该字段:
if (typeof element === "object" && element !== null && element.$$typeof === REACT_ELEMENT_TYPE) { // 这是一个 React Element }这里面的 typeof 被用来快速排除非对象类型,避免对字符串、数字做后续的属性读取。这个设计还顺带解决了反序列化 XSS 的问题:被篡改过的对象没有正确的 Symbol 标记,就能被拒之门外。面试时聊到 React 安全机制,你能把这个例子抛出来,比单纯复述 typeof 返回值生动得多。
Vue 3 源码里同样大量使用 typeof。比如判断一个值是不是响应式对象,第一步就是把原始值先过滤掉,常见的 isObject 函数长这样:
export const isObject = (value) => value !== null && typeof value === "object";先用 typeof 排除原始类型,再用value !== null排除 null 这个 typeof 判断不了的特殊值,之后才进入 Proxy 或者访问器属性的处理逻辑。这种组合判断思路,就是你在日常业务代码里最该复制的套路:先快筛类型,再针对性处理。
还有一个高频场景是判断“类数组对象”和 arguments。lodash 这类库不会傻乎乎用 instanceof,而是先用 typeof 和 Object.prototype.toString 把参数对象、NodeList 一类东西识别出来,再检测 length 属性是否是数字。这类工具函数写久了你会形成肌肉记忆:能判断原语优先用 typeof,能判断内置对象优先用 toString 标签,只有自定义类实例才考虑 instanceof。
4. 从 JS 到 TS:keyof typeof 与工程化避坑
4.1 JS 的 typeof 和 TS 的 typeof 是两个东西
很多同学在面试里被问到keyof typeof会卡住,根源是把 JS 运行时操作符和 TS 类型查询操作符混在一起。JS 的 typeof 是程序运行到某一行时读取值的类型,返回的是字符串;TS 的 typeof 出现在类型位置,是在编译期“取一个值/变量的静态类型”,两者连返回值的形态都完全不同,只是恰好多义词。
一个很典型的 TS 用法是先定义常量对象,再用typeof拿到它的类型:
const borderRadiusMap = { sm: 4, md: 8, lg: 16, }; type BorderRadiusMap = typeof borderRadiusMap; // 等价于 type BorderRadiusMap = { sm: number; md: number; lg: number; }keyof typeof是进一步把对象的键提取成联合类型。TS 的 keyof 操作符本来就是取对象类型的所有键,前面加上 typeof,意思是“先把常量对象映射成类型,再取这个类型的键”:
type Size = keyof typeof borderRadiusMap; // 等价于 type Size = "sm" | "md" | "lg"; function getRadius(size: Size) { return borderRadiusMap[size]; }这样写的价值是让函数入参不再是一个宽泛的 string,而是精确的枚举联合,写错键名编译器直接报错。在工程里我经常用这个模式维护路由表、图标映射、状态枚举之类的一组常量,只要维护常量对象,类型就能自动联动,不用写重复的字符串字面量类型。面试题里出现“ts keyof typeof”指的就是这个场景。
还有一个高频的衍生用法是ReturnType<typeof fn>,它先用 typeof 拿到函数的类型,再用 ReturnType 工具类型提取函数返回值的类型。这类组合工具类型在封装库函数、透传类型时会频繁用到。
4.2 写通用类型判断工具时我踩过的 4 个坑
第一坑:不要用 String(obj) 来代替 Object.prototype.toString。String(null)返回 "null",String({})返回 "[object Object]",看起来差不多,但对 Symbol 对象和 undefined 的表现都不一样,而且 String 方法对自定义 toString 敏感,一个对象改了 toString 方法后结果就不可控。类型判断要的是稳定标签,不是字符串转换。
第二坑:Symbol.toStringTag 可以伪造 Object.prototype.toString 的结果。原生内置对象 Math、JSON 之所以能返回 "[object Math]" 和 "[object JSON]",一部分靠的就是内部标签。但任意对象都能通过[Symbol.toStringTag]伪装成这些标签:
const fakeArray = { [Symbol.toStringTag]: "Array", }; Object.prototype.toString.call(fakeArray); // "[object Array]"所以如果你的程序要基于类型标签决定一个比较敏感的逻辑,不能只看 toString 结果,还得结合其他特征判断,比如数组要额外验证 length 和数值索引。这个坑在“校验不可信输入”的场景里尤其关键。
第三坑:constructor 属性不可信。对象原型链上确实有 constructor 指回构造函数,看起来判断类型很方便,但它是普通属性,能被任意改写,而且原型对象上的 constructor 会被子类继承,导致判断结果偏移。举例来说:
const arr = []; arr.constructor === Array; // true arr.constructor = Object; arr.constructor === Array; // false,但 arr 仍然是数组如果你用 constructor 去判断数据来源,一个不小心的赋值操作就会让类型检测全盘失效。日常开发里我尽量不用它做关键判断,只在调试日志里拿 constructor.name 辅助打印。
第四坑:跨 realm 场景不要依赖 instanceof。前面 iframe 的例子已经说明问题。现实中还有 electron 窗口间消息传递、iframe 表单提交、postMessage 传输数据等场景,拿到的数组或 Date 实例经常来自另一个全局环境,这时 instanceof 十有八九返回 false。我的原则是:判断内置类型一律优先 Array.isArray、Number.isNaN、Date 对象用 toString 标签兜底;instanceof 只用来判断业务自定义类的实例,因为自定义类本来就没有跨 realm 的需求。
5. 高频追问速查与复习建议
5.1 一套速查表收下常见追问
把面试中围绕 typeof/instanceof 最容易出现的追问整理成速查表,方便你在面试前最后一小时扫一遍:
| 追问 | 标准答案 | 面试官想听的加分点 |
|---|---|---|
| typeof null 的结果? | "object" | 说明是历史 bug,早期用低位类型标签实现,null 地址为 0,误判为 object |
| typeof 未声明变量会怎样? | 返回 "undefined",不抛错 | 说明 typeof 不对操作数做强制求值 |
| typeof 一个块级 let 变量? | 如果在声明前访问,抛 ReferenceError,因为处于暂时性死区 | 能指出 typeof 不是绝对安全 |
| [] instanceof Object 是 true 吗? | 是,因为数组原型链上有 Object.prototype | 能画出原型链路径 |
| Object.create(null) instanceof Object? | false,因为它的原型是 null | 理解原型链终点不是 Object.prototype |
| "hello" instanceof String? | false,"hello" 是字符串原语,不是对象 | 能区分原语和包装对象 |
| 如何判断一个跨 iframe 的数组? | 用 Array.isArray | 能说清跨 realm 下 instanceof 失效 |
| 手写 instanceof 注意什么? | 左侧非对象返回 false、右侧必须可调用、循环找原型链 | 能写出异常处理和原型链终点判断 |
| constructor 能替代 instanceof 吗? | 不能,constructor 可被改写且可被继承 | 能举出改写例子 |
| Symbol.toStringTag 影响什么? | 影响 Object.prototype.toString 的结果 | 能演示伪造自定义标签的场景 |
| keyof typeof 是做什么的? | 用 typeof 把对象值映射成类型,再用 keyof 取键联合类型 | 能写一个常量对象配联合类型的例子 |
这张表不能死记硬背。每一条背后都对应着 JavaScript 引擎对类型、原型链、符号的处理机制,理解和记忆需要结合起来。
5.2 一次线上事故让我放弃用 instanceof 做通用判断
说个真实踩坑经历。之前我在做一个可嵌入的业务组件,需要接收父页面上传的一组配置数据,配置里包含回调函数和业务对象。当时为了区分传入的是一个普通对象还是数组,我图省事直接写了value instanceof Array来判断。在本地开发环境跑得好好的,一上生产,组件被嵌到了页面里的同域 iframe 中,配置数据从主文档通过 postMessage 传进来,我这边收到的数组就全都来自 iframe 的 Array 构造函数。程序里的分支判断直接翻车,本该走数组处理逻辑的请求全走了对象逻辑,线上报错排查了半个下午。
那次之后我把项目里所有“判断内置类型”的地方全部替换成了 Array.isArray 和 Object.prototype.toString 组合方案,并且专门写了一个 getType 工具函数统一收敛判断逻辑。排查问题的过程让我把 instanceof 的适用边界记得很牢:不是它没用,而是它只适用于“同一个全局环境下的自定义类实例判断”,跨环境判断必须换成更底层的方案。
复习这两个操作符时,我建议你别只背结论,亲手把 typeof 的八种返回值、手写 instanceof 的代码、Object.prototype.toString 的标签截取脚本敲一遍,再想想 React、Vue 源码里那些 typeof 判断到底在防什么。把这些场景串起来之后,再遇到任何围绕类型判断的追问,你都不会只是在背八股,而是真正在讲一门自己每天都在用的语言机制。