
最近帮团队做校招一面技术面环节我习惯先从八股切入十个人里面至少八个能把 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 表示值的类型对象类型标签是 0null 的空指针地址正好也是 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。它们的核心差异可以压成一张表判断方式示例结果优点致命缺点typeofstring、object、function语法简单能安全区分所有原始类型对象类型只能得到 object分不清数组/Date/正则且 null 误判为 objectinstanceoftrue / false能判断自定义类的实例还能体现继承关系跨 realm 失效右侧必须是可调用函数原始类型一律 falseconstructorArray、String、Object 等写法直观可以直接拿到构造函数名属性可被改写原型链上的 constructor 也可能被继承安全性差Object.prototype.toString.call[object Array]、[object Null]识别范围广内置类型全覆盖跨 realm 基本稳定只能得到笼统的内置类型名自定义类的实例统一返回 [object Object]Array.isArraytrue / 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 到 TSkeyof 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”指的就是这个场景。还有一个高频的衍生用法是ReturnTypetypeof fn它先用 typeof 拿到函数的类型再用 ReturnType 工具类型提取函数返回值的类型。这类组合工具类型在封装库函数、透传类型时会频繁用到。4.2 写通用类型判断工具时我踩过的 4 个坑第一坑不要用 String(obj) 来代替 Object.prototype.toString。String(null)返回 nullString({})返回 [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误判为 objecttypeof 未声明变量会怎样返回 undefined不抛错说明 typeof 不对操作数做强制求值typeof 一个块级 let 变量如果在声明前访问抛 ReferenceError因为处于暂时性死区能指出 typeof 不是绝对安全[] instanceof Object 是 true 吗是因为数组原型链上有 Object.prototype能画出原型链路径Object.create(null) instanceof Objectfalse因为它的原型是 null理解原型链终点不是 Object.prototypehello instanceof Stringfalsehello 是字符串原语不是对象能区分原语和包装对象如何判断一个跨 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 判断到底在防什么。把这些场景串起来之后再遇到任何围绕类型判断的追问你都不会只是在背八股而是真正在讲一门自己每天都在用的语言机制。