ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

typeof与instanceof原理详解:从类型标签到原型链,前端面试必考

typeof与instanceof原理详解:从类型标签到原型链,前端面试必考 先把话说在前面typeof 和 instanceof 这两个操作符前端面试基本逢面必考尤其是初、中级岗位。很多人刷题的时候觉得这题简单——typeof 返回字符串嘛instanceof 检查原型链嘛——但真到面试现场面试官一旦开始连环追问比如typeof null 为什么是 object能不能手写一个 instanceofSymbol.hasInstance 是干什么的不少人就卡壳了。这篇文章不打算给你念 MDN 文档而是从原理到底层、从常规用法到变态面试题把这两个操作符彻底拆开揉碎。内容涵盖 V8 内部的类型标签机制、原型链查找的完整流程、以及我在实际面试中遇到过的各种追问方向新手能看懂老手也能查漏补缺。1. 整体设计思路为什么面试官总爱把这两个放一起问1.1 两个操作符解决的是同一个问题的不同侧面先说一个很本质的问题JavaScript 是动态类型语言变量本身没有类型只有值有类型。这就带来一个日常开发里最频繁的需求——判断一个值到底是什么类型。而 typeof 和 instanceof 恰好是语言层面提供的两种最基础的判断手段但它们的设计哲学完全不同typeof 看的是值本身的类型标签它不关心这个值是怎么来的也不关心它是否由构造函数创建instanceof 看的是对象的原型链上有没有某个构造函数的 prototype 对象它关心的是对象与构造函数之间的血缘关系。一个看身份证上的户籍一个查家族族谱。面试官把这两个放在一起问本质上是在考察你对 JavaScript 类型系统的理解深度。如果你只知道typeof 用来判断基本类型、instanceof 用来判断引用类型这种口诀那说明你还没摸到门道。1.2 面试官的连环追问逻辑从我面试别人的经验来看面试官问 typeof 和 instanceof 通常不会只问一题就收手而是会顺着你的回答层层深入。典型追问路径是这样的先问typeof 能判断哪些类型考察基础记忆再问typeof null 为什么是 object考察是否知道历史 bug 和底层原理接着问怎么准确判断一个值的类型考察 Object.prototype.toString 的用法然后转问 instanceof 的原理是什么考察原型链理解如果回答得不错就会让手写 instanceof 实现考察代码能力最后可能提升难度问 Symbol.hasInstance、跨 iframe 判断失效等考察知识广度。所以这篇文章的结构就是按照这条追问链设计的。你把这篇文章吃透等于把一条完整的面试追问链提前演练了一遍。2. 核心细节解析typeof 的返回值与底层原理2.1 typeof 的七种返回值先过一遍最基础的东西。typeof 是一个一元操作符后面跟一个操作数返回一个表示操作数类型的字符串。规范定义它可能返回以下七个值表达式返回值说明typeof undefinedundefined未定义的值typeof trueboolean布尔值typeof 42number数值typeof strstring字符串typeof Symbol()symbolES6 新增的 Symbol 类型typeof 123nbigintES2020 新增的 BigInt 类型typeof {}object对象以及 nulltypeof function(){}function函数注意其实规范层面是六种类型标签加一个 function但能返回的值是七个字符串。这里有个很微妙的点function 本质上也是 object 的一种但 typeof 会单独返回 function这是为了区分可调用对象。而数组、正则、日期这些typeof 统统返回 object所以 typeof 在判断引用类型时基本是废的。2.2 typeof null object 的真相这是前端圈最著名的历史 bug 之一面试必考。答案要说清楚两层为什么是 bug以及为什么不能修。第一层为什么 typeof null 会是 object。这要从 JavaScript 的底层存储机制说起。在 V8 等引擎的更早期实现中JavaScript 的值是用一个类型标签 实际数据的结构来存储的。类型标签占低 1 到 3 位用来标识这个值是对象、整数、浮点数、字符串还是布尔值。具体的标签编码在不同引擎里略有差异但有一个共同点null 的机器码表示是 0x00也就是全零。而在那个编码方案里对象的类型标签也是 0于是 typeof 看到低位标签是 0就直接判定为 object 了。第二层为什么这个 bug 不能修。因为 1995 年 Brendan Eich 用十天设计出 JavaScript 的时候这个行为就已经存在了。到后来 ECMAScript 规范制定时如果强行把 typeof null 改成返回 null那所有依赖这个行为的老代码——比如某些框架里用typeof obj object来判断是否需要深层遍历的逻辑——会瞬间崩溃。所以规范选择了保留这个错误并在文档里明确标注这是历史遗留问题。现在所有主流引擎都遵循规范你就别指望它改了。这个点复习到位的话你可以主动在面试时补充一句typeof null 的返回值是历史遗留实际开发中判断 null 需要用value null这会让面试官觉得你不仅知道 bug还知道怎么在生产环境中规避它。2.3 typeof 对未声明变量的特殊处理第二个高频考点typeof对未声明的变量不会报错而是返回 undefined。这个设计其实是为了容错。比如你想判断某个全局变量存不存在直接写if (window.jQuery)在旧浏览器里可能因为跨作用域访问引发问题但if (typeof jQuery ! undefined)永远是安全的。不过这里有个容易混淆的点未声明的变量和值为 undefined 的变量typeof 的结果一样但语义完全不同。let a; console.log(typeof a); // undefined a 已声明但未赋值 console.log(typeof b); // undefined b 完全不存在 // 但直接访问未声明变量会抛 ReferenceError console.log(b); // ReferenceError: b is not defined这个特性在日常开发中常被用来做全局 API 的兼容检测比如检测浏览器是否支持某个新特性。面试的时候如果能主动提这个场景会比干背结论要好得多。2.4 NaN 和包装对象的坑typeof 相关的高频坑还有两个。第一个是 NaNtypeof NaN返回 number。很多人觉得 NaN 是 Not a Number那它的类型应该也是不是数字但实际上 NaN 在 IEEE 754 浮点数标准里就是一个特殊的数值它属于 number 类型。所以判断 NaN 不能靠 typeof得用Number.isNaN()或者 ES6 之前的全局isNaN()。注意两者也有区别全局 isNaN 会先把参数强制转换为数字再判断Number.isNaN不会做类型转换只有值真的是 NaN 才返回 true。第二个坑是包装对象。JS 里有三个包装对象new String()、new Number()、new Boolean()。如果你对它们用 typeoftypeof new String(hello); // object typeof new Number(42); // object typeof new Boolean(true); // object这是因为 new 出来的东西是对象不是原始值。这个坑在面试中常以如何判断一个值是否是字符串的形式出现——如果你只写typeof value string那new String(hello)会被漏掉。实际生产环境中几乎不会有人刻意用包装对象但理解这一点能帮你把 typeof 和对象系统的关系理清楚。3. 原型链视角下的 instanceof原理与边界情况3.1 instanceof 的规范定义instanceof 是一个二元操作符左操作数是对象右操作数必须是函数更准确地说是必须有 Symbol.hasInstance 方法的可调用对象。它的核心语义是检查右操作数的 prototype 对象是否出现在左操作数的原型链上。这里有个关键概念必须澄清检查的是构造函数的 prototype 属性和对象的原型链之间的关系。用代码说function Animal() {} const dog new Animal(); dog instanceof Animal; // true // dog.__proto__ Animal.prototype所以为 true dog instanceof Object; // true // dog.__proto__.__proto__ Object.prototype原型链上能找到 Animal instanceof Function; // true // 因为 Animal 是函数函数的 __proto__ 是 Function.prototype Animal instanceof Object; // true // 函数也是对象Function.prototype.__proto__ 是 Object.prototype很多人画不清这个关系图。我建议你记住一个简单的心法实例的原型链上只要某个节点的proto指向了右侧构造函数的 prototype就返回 true。判断过程是沿着原型链一层一层往上走的一直走到 null 为止。所以顺着这条链任何对象 instanceof Object 都是 true这是边界不是 bug。3.2 手写一个 instanceof 实现面试官让你手写 instanceof 的话标准解法是沿着原型链走function myInstanceof(left, right) { // 左侧必须是对象如果 left 不是对象直接返回 false if ((typeof left ! object typeof left ! function) || left null) { return false; } let proto Object.getPrototypeOf(left); while (true) { if (proto null) return false; if (proto right.prototype) return true; proto Object.getPrototypeOf(proto); } }这段代码有四个细节要注意也是面试官可能会追问的点第一为什么要先判断 left 不是对象就返回 false。因为基本类型本身没有原型链1 instanceof Number返回 false。但这里有个反直觉的特例Object.create(null)创建出来的对象没有原型它的 instanceof 判断天然为 false因为它的原型链在起点就断了。第二为什么用Object.getPrototypeOf而不是__proto__。因为__proto__是历史遗留的非标准属性虽然所有主流浏览器都实现了但规范推荐的访问原型的方式是Object.getPrototypeOf()。面试时写这个能体现你对规范的理解。第三right 必须是函数。如果 right 不是函数标准实现会抛出 TypeError。面试写的简版可以不处理这个边界但如果你主动补上这个判断会是个加分项。第四原型链循环不会发生因为基于原型继承的链最终一定指向 null不会成环。但如果有人在运行时手动改了某个对象的proto指向自己理论上会出问题。这种奇葩场景一般不会考了解即可。3.3 跨 iframe 与跨全局对象问题instanceof 在面试中被问烂的还有一个场景跨 iframe 判断失效。比如// 在父页面里 const iframe document.createElement(iframe); document.body.appendChild(iframe); const arr new iframe.contentWindow.Array(); arr instanceof Array; // false原因很简单每个 iframe 都有自己的全局对象都有自己的 Array 构造函数。iframe.contentWindow.Array和父页面的window.Array不是同一个函数它们的 prototype 也不是同一个对象。arr 的原型链上挂的是 iframe 里的 Array.prototype自然和父页面的 Array.prototype 不相等。这个坑在现实开发中遇到得不多但一旦遇到定位起来很痛苦。比如你用第三方库解析 JSON 后得到的数组如果这个库运行在 iframe 里你拿这个数组去instanceof Array判断就会失败。标准的解决方案是用Array.isArray()它会跨全局对象正确判断。类似的判断对象类型时用Object.prototype.toString.call(value)也能跨全局对象得到正确结果因为这个方法读的是对象内部的[[Class]]标记。3.4 Symbol.hasInstance面试加分项ECMAScript 2015ES6引入了一个非常冷门但面试可以拿来装逼的机制——Symbol.hasInstance。它允许你自定义 instanceof 的判断逻辑。规范里定义了执行obj instanceof Constructor时JS 引擎会先去读取Constructor[Symbol.hasInstance]。如果这个属性是函数就调用它把 obj 作为参数传进去用它的返回值作为 instanceof 的最终结果。如果这个属性不存在才走默认的原型链查找逻辑。class MyArray { static [Symbol.hasInstance](instance) { return Array.isArray(instance); } } const arr [1, 2, 3]; console.log(arr instanceof MyArray); // true虽然 MyArray 和 arr 毫无关系这个特性让你可以欺骗 instanceof 操作符改变它的判断结果。注意用 static 定义的是类构造函数本身的静态方法也就是 MyArray 这个函数对象上的方法。如果你写成[Symbol.hasInstance](instance) {}不加 static挂的是 MyArray.prototype 上instanceof 根本不会读它。面试官如果问到这个是在考察你对元编程和 Symbol 的了解深度。能说出instanceof 不是固定查找原型链而是内部会调用右侧的 Symbol.hasInstance 方法这句话就已经超过大多数候选人了。4. 实战对比三种类型判断方案怎么选4.1 typeof、instanceof、Object.prototype.toString 的效果对比实际开发中类型判断的需求远不止 typeof 和 instanceof 两种手段。我在代码评审里经常看到有人把这两种操作符用错场景。这里整理一份对比表覆盖三种主流方案的效果目标类型typeofinstanceofObject.prototype.toString.callundefinedundefined报错/不适用[object Undefined]nullobject坑不适用[object Null]booleanboolean不适用[object Boolean]numbernumber不适用[object Number]stringstring不适用[object String]symbolsymbol不适用[object Symbol]bigintbigint不适用[object BigInt]functionfunction是 Function 的实例[object Function]数组object是 Array 的实例[object Array]Dateobject是 Date 的实例[object Date]RegExpobject是 RegExp 的实例[object RegExp]Promiseobject是 Promise 的实例[object Promise]普通对象object是 Object 的实例[object Object]从这张表可以看出三者的适用场景完全不同typeof 适合判断基本类型除了 nullinstanceof 适合判断某个对象是否由特定构造函数创建但跨全局对象会失效Object.prototype.toString.call 是判断具体内置类型的最稳妥方案。4.2 生产环境最常用的判断函数实际开发中我习惯封装一个统一的类型判断工具函数function getType(value) { if (value null) return null; if (typeof value object) { // 用 Object.prototype.toString 拿内部类型标记 const tag Object.prototype.toString.call(value); // 形如 [object Array]取中间类型名并转小写 return tag.slice(8, -1).toLowerCase(); } return typeof value; }这个函数兼容了 typeof 的简洁性和 toString 的精确性。注意必须先排除 null因为Object.prototype.toString.call(null)虽然能正确返回 [object Null]但很多业务场景希望直接拿到 null 而不是 null 这个字符串统一转成小写风格更一致。另一个常用的实践是判断一个变量是否为普通对象plain object。很多工具库喜欢写typeof value object但这会把数组、null、Date 全部误判进去。更严谨的做法是function isPlainObject(value) { if (Object.prototype.toString.call(value) ! [object Object]) return false; const proto Object.getPrototypeOf(value); return proto null || proto Object.prototype; }这个实现考虑了 Object.create(null) 创建的无原型对象它是合法的普通对象但不等于 Object.prototype。React 内部判断是否为 plain object 用的也是类似逻辑。4.3 判断数组用 Array.isArray 而不是 instanceof单独把数组判断拎出来说是因为它在生产环境太常用了。虽然[] instanceof Array能返回 true但正如前面所说跨 iframe 就会失效。Array.isArray是 ES5 引入的专门方法它的实现机制不是解析原型链而是检查对象内部的类型属性所以不受全局对象影响。ES5 时代很多人自己实现过 Array.isArray 的 polyfillif (!Array.isArray) { Array.isArray function(arg) { return Object.prototype.toString.call(arg) [object Array]; }; }用这个 polyfill 就能明白 Array.isArray 的底层原理其实不长在 Array 身上而是绕过了原型链直接读取对象的内部属性标识。这也是为什么它比 instanceof 更可靠。5. 面试追问从操作符延伸到 TS 与 Web API5.1 TypeScript 中的 typeof 与 keyof typeof如果你面试的是偏工程化的岗位面试官很可能把 typeof 和 TypeScript 的类型系统结合起来问。这是近几年新出现的面试方向。如果你不写 TS这个部分可以略过但如果你简历上写了熟悉 TS下面的内容必须掌握。TS 里也有一个 typeof 操作符但它不是运行时操作符而是类型查询操作符——在类型上下文中使用用来提取一个值的静态类型const config { url: https://api.example.com, retry: 3, timeout: 5000, }; // 提取 config 的类型 type Config typeof config; // Config { url: string; retry: number; timeout: number; }注意这里的 typeof config 并不是 JS 运行时那句 typeof它是在编译期被 TS 编译器处理的不会生成任何运行时代码。如果你面试时能主动说清楚这段代码编译后不会留下 typeof因为它是纯类型层面的操作面试官会对你刮目相看。而keyof是 TS 的索引类型查询操作符它取的是一个类型的所有键组成的联合类型。两者结合使用——keyof typeof config——就是先提取值的类型再提取这个类型的所有键type ConfigKeys keyof typeof config; // ConfigKeys url | retry | timeout这个组合在写枚举映射、表单配置、API 参数白名单时非常常用。比如你要限制一个函数的参数只能是 config 的键名function getConfig(key: keyof typeof config) { return config[key]; } getConfig(retry); // 合法 getConfig(other); // 报错5.2 worker 上传大文件与 Object 类型检测的关联看到热词里有前端使用 worker 上传大文件顺便说一个关联点。在 Web Worker 环境中postMessage 传递的数据会经过结构化克隆算法。如果你在 Worker 里拿到一个对象想判断它的类型用 instanceof 会出问题——因为 Worker 和主线程是两套全局对象Worker 里的 Array 构造函数和主线程的 Array 构造函数不是同一个arr instanceof Array在 Worker 里可能返回 false。所以无论主线程还是 Worker只要数据跨了全局环境一律用 Object.prototype.toString 或者 Array.isArray 来做类型判断。这个坑我当年做音视频上传功能时踩过在 Worker 里对文件分片数组做判断用 instanceof 死活不对排查了半天才定位到全局对象不一致的问题。另一个相关场景是 SSE 或 WebSocket 推送的数据。后端推过来的 JSON 字符串经过 JSON.parse 之后得到的对象虽然看起来是普通对象但它携带的数据结构是否和前端定义的类型一致也需要用正确的类型判断来做防御性校验。用 typeof 只能排除 undefined用 instanceof 在跨源场景下会误判最稳的还是统一走 toString 方案。5.3 水波纹进度条等场景中的性能提示热词里有水波纹进度条这跟类型判断好像不沾边但有个隐蔽关联如果你用 requestAnimationFrame 做动画在每一帧里对状态对象做类型判断频繁调用 instanceof 或者 Object.prototype.toString 都可能在低端设备上造成性能损耗。虽然单次判断的开销微乎其微但如果你在一个大列表渲染里对每个 item 都做一次深度类型检测累积消耗就上来了。实际的优化思路有两个一是把类型判断结果缓存起来不要重复计算二是能用简单比较的就不用复杂方案比如判断一个变量是否是真值就直接用if (value)非要判断是数组再走数组方法。任何类型判断代码都应该写在模块初始化阶段或者数据入口处而不是在热循环里反复执行。6. 高频面试题速查常见问题与标准答法整理一份我在面试别人时的高频问题清单每道题附上一个可以直接拿来用的回答框架。你拿去对照自测也行拿去背也行但建议理解后再用自己的话说面试官最反感背答案。面试题回答要点typeof 能返回哪些值七个字符串undefined、boolean、number、string、symbol、bigint、object、function注意 null 返回 object 是历史遗留为什么 typeof null 是 object早期引擎用类型标签存储值null 的标签是 0对应 object 的标签规范保留该行为是为了兼容老代码如何准确判断 nullvalue null注意 typeof 无法区分 null 和 objecttypeof 一个未声明变量会怎样返回 undefined不会抛 ReferenceError适合做全局 API 兼容检测instanceof 的原理检查右侧构造函数的 prototype 是否在左侧对象的原型链上沿着proto逐层向上查找手写 instanceof用 Object.getPrototypeOf 沿原型链循环直到 null为什么 arr instanceof Array 可能为 false跨 iframe 或跨全局对象时Array 构造函数不是同一个prototype 不相等如何判断一个值是不是数组首选 Array.isArray跨全局对象也能正确判断Object.prototype.toString 的作用返回 [object 类型名]能精确区分内置类型不受全局对象影响Symbol.hasInstance 是什么允许自定义 instanceof 的行为当右侧存在该静态方法时优先调用它TS 中 keyof typeof 的作用先提取值的类型再取键的联合类型常用于配置对象与枚举映射场景7. 避坑指南我在实战中踩过的类型判断的坑7.1 不要用 typeof 判断全局变量是否存在来判断 API 特性这是我早期做前端兼容性判断时踩过的坑。当时判断浏览器是否支持 fetch写了if (typeof fetch ! undefined)看似没有问题。但后来发现有些老浏览器虽然定义了 fetch 变量但实现不完整调用时报错。typeof 只能告诉你有没有这个名字不能告诉你这个功能能不能用。更可靠的方式是实际操作检测——比如调用一下然后 catch 异常或者走特性检测库比如 Modernizr 的思路。7.2 不要用 instanceof 判断类的继承关系做策略分发模块化开发里有时会写类似if (obj instanceof A) { ... } else if (obj instanceof B) { ... }的代码。这种方式在只有一个全局对象时没问题但一旦引入多个打包产物、多个 iframe、微前端架构就会出现判断失效或者误判的诡异问题。尤其微前端场景下子应用打包出来的代码可能运行在同一个全局对象下但如果某个子应用加载了双份的框架代码就会出现两个 React 构造函数、两套原型链instanceof 直接失灵。我在一个微前端项目里就遇到过主应用和子应用各自打包了一份 axios子应用里的请求实例传给主应用时用instanceof Axios判断总是返回 false。后来改成用内部属性标记——在实例上挂一个自定义的不可枚举标记或者直接检查对象的构造函数名才解决。总之在跨应用、跨全局场景下不要依赖 instanceof。7.3 小心 Object.prototype.toString 被篡改Object.prototype.toString 也不是绝对安全。ES6 引入了 Symbol.toStringTag开发者可以通过定义这个属性来改变 toString 的返回值const fakeArray { [Symbol.toStringTag]: Array, }; Object.prototype.toString.call(fakeArray); // [object Array]假的所以如果你的代码运行在不可信环境或者依赖某个第三方库篡改了内置对象的 Symbol.toStringTag基于 toString 的类型判断也可能被欺骗。好在这属于极端场景正常业务代码不用过度防御。这里提一句只是避免你到时候一脸懵。8. 最后分享一点面试经验这篇文章写到这核心内容基本都覆盖了。最后以我自己的面试经验收个尾。我面试前端候选人很少会直接让人背 typeof 的返回值表而是会给一段代码让候选人预测输出结果let a null; let b []; let c function() {}; console.log(typeof a); // object console.log(typeof b); // object console.log(typeof c); // function console.log(b instanceof Array); // true console.log(b instanceof Object); // true console.log(c instanceof Function); // true console.log(c instanceof Object); // true能不看文档写出正确答案的人很多但能解释清楚每一行为什么的人寥寥无几。你如果能做到后者面试基本就稳了。另外提醒一句面试答这类题时不要只答结论要主动补充为什么和实际开发中我会怎么做这会让面试官觉得你不只是刷了题而是真的理解这一块的知识体系。
返回列表