JavaScript 面试题在圈子里有个刻板印象背八股。但真正当过面试官的人都知道同样是说说闭包这一道题有人能聊到内存管理、模块化设计、工具链原理有人只能背出函数套函数、返回函数六个字。差在哪差在对语言本质的理解差在有没有亲手写过、调过、踩过坑。这篇文章是我这些年面试和被面试的实战总结把类型判断、闭包、this、事件循环、异常处理、跨端互调这些绕不开的考点拆开讲清楚每个考点都配上答题思路和手写代码。不准备面试的人看了能补短板准备面试的人看完可以直接拿去自测。所有内容都基于真实面试场景不是从官方文档抄出来的概念堆叠。1. 面试官真正在意的底层能力JS 面试为什么越来越原理化1.1 从背八股文到验证工程思维的转变头一个问题先讲明白现在的技术面试到底在考什么我早期面试候选人的时候喜欢直接问 API数组的 flat 方法怎么用Object.assign 深不深。后来发现这种问法在筛掉真正的牛人因为答案在网上到处都有背下来只需要一个通宵。现在我的面试提纲上每个知识点都会准备三级追问先问是什么再问为什么这么设计最后要求给我讲一个你实际用它的场景。比如问闭包完整链路是这样的第一层闭包是什么第二层闭包为什么能记住外部变量内存上怎么实现的第三层闭包一定造成内存泄漏吗什么情况下会泄漏如何用 WeakMap 优化一个以对象为 key 的缓存第四层现场手写一个只执行一次的函数或者实现一个带有记忆化的斐波那契。能走完四层的候选人哪怕其中一两层答得磕绊我都会给高分。因为这说明他有debug思维遇到问题能顺着调用链往下追而不是停在报错信息表面。这就是原理化的本质面试官不是在为难你而是在模拟真实工作中的排错场景。线上出了内存暴涨你不知道闭包引用了什么就只能去翻代码猜理解了执行上下文和垃圾回收的引用关系看一眼 Heap Snapshot 就能定位。1.2 一条旋转视频的收藏夹代码能挖出多少知识点有个网络热词很有意思内容是收藏夹里的一个小脚本大意是把网页视频旋转90度的一段 bookmarklet 代码。别看它短它能挖出一串面试题。第一层document.querySelector(video)返回什么如果没有 video 标签会怎样——返回null这时候再访问null.style会直接抛 TypeError。资深候选人会先说要判空这就是防御性编程的本能反应。第二层style.rotate -90deg这个写法是什么新一点的浏览器支持独立的 CSSrotate属性老浏览器只认transform: rotate(-90deg)。这里考的是对 CSSOM 和浏览器兼容性的了解能说到独立属性性能更好、但要注意兼容矩阵就加分。第三层代码里的v document.querySelector(...)没有声明关键字在非严格模式下会把 v 挂到全局对象上污染全局命名空间。面试官如果追问用let和不用有什么区别就自然过渡到了作用域和全局污染话题。第四层如果想旋转页面上所有视频用querySelectorAll遍历要注意什么NodeList不是数组要不要转成数组forEach能不能直接用——又是一串 DOM 和迭代协议的问题。所以你看一个外人看来是小技巧的收藏夹脚本在面试官眼里就是一套完整的 DOM、CSS、作用域、错误处理考察素材。准备面试最重要的能力不是把题库背完而是学会像这样把一个知识点往深了想三层。2. 数据类型判断与隐式转换第一道必考题的完整拆解怎么判断一个变量的数据类型基本是 JavaScript 面试的开场题因为它能快速看出候选人有没有真正踩过跨端、序列化、兼容性的坑。2.1 typeof 的局限与 Object.prototype.toString 的正确打开方式先说基础。typeof的返回值有这些undefined、boolean、number、bigint、string、symbol、function、object。这个表背下来不难但面试喜欢考的是几个反直觉点typeof null返回object这是语言设计早期的一个经典 bug因为当时的类型标签位设计有误现在为了兼容生态不能改。typeof一个未声明的变量不会报错返回undefined这个特性在一些防御性检测代码里很有用。typeof function返回function但函数本质上是对象的一种之所以单列是因为词法上需要一个可调用对象的类型返回。真正复杂的是判断数组、日期、正则、Map、Set、Promise 这些对象子类型。typeof []只能告诉你它是object无法区分数组和普通对象。万金油方案是用Object.prototype.toString.call(value)Object.prototype.toString.call([]); // [object Array] Object.prototype.toString.call(new Date()); // [object Date] Object.prototype.toString.call(/ab/); // [object RegExp] Object.prototype.toString.call(new Map()); // [object Map] Object.prototype.toString.call(null); // [object Null] Object.prototype.toString.call(undefined); // [object Undefined]原理很简单对象内部有一个[[Symbol.toStringTag]]属性内置类型都有Object.prototype.toString会读取它拼出[object Type]格式而且它对跨 iframe 的对象也成立这是instanceof做不到的。2.2 与 的隐式转换规则别只背全等更安全第二道经典题是说说 和 的区别。如果你只回答会类型转换不会那你只是背了结论。面试官真正想听的是你能不能推导出下面这些结果null undefined; // true规范里明确规定 0 false; // true布尔转数字 0; // true字符串转数字 [] false; // true复杂推导 [1, 2] 1,2; // true数组先转字符串 NaN NaN; // falseNaN 不等于任何值包括自己[] false的推导过程是对象转原始值走ToPrimitive数组先调用valueOf()返回自身因为是对象不返回原始值再调用toString()得到空字符串布尔值false转数字得到0然后空字符串转数字也是0所以两边都是0结果为true。面试答题的思路建议分三步先明确定义再给推导案例最后给工程建议——业务代码里无脑用判空时可以显式用value ! null value ! undefined不要依赖隐式规则。如果面试官追问Object.is可以补充Object.is(NaN, NaN)是trueObject.is(0, -0)是false它解决的是严格相等对NaN和-0的特殊处理。2.3 手写一个可靠的 type 判断工具函数这道手写题几乎是必考面试官想看你代码的健壮性和对边界条件的敏感度。function getType(value) { // 基本类型和 null 直接处理 if (value null) return null; if (typeof value ! object typeof value ! function) { return typeof value; } // 对象类型走 toString 标签 return Object.prototype.toString.call(value).slice(8, -1).toLowerCase(); }测试几个典型值getType(42); // number getType(a); // string getType(null); // null getType(undefined); // undefined getType([]); // array getType(new Date()); // date getType(/x/); // regexp getType(new Map()); // map getType(Promise.resolve()); // promise getType(class A {}); // function这里有个加分点slice(8, -1)是把[object Array]里的Array截出来。如果有人问为什么不用正则可以答正则也行但手写 slice 更直观。再深入一步Symbol.toStringTag是可以被自定义对象伪造的比如给一个普通对象设置[Symbol.toStringTag] Array上面的方法也会返回array。真想严谨还得配合Array.isArray等专用 API 交叉验证。能聊到这一层的候选人说明研究过类型标签的机制。3. 函数、闭包与 this 指向绕不开的三连问函数是 JavaScript 的一等公民闭包和 this 是函数题里的重头戏。这一部分我按本质、规则、实战三层讲。3.1 闭包的本质是词法作用域与应用场景先给一个纠正很多教程说闭包是函数套函数这是误读。闭包的本质是——函数创建时会记录自己所在词法环境的引用之后无论函数在哪里被调用都能访问那个环境里的变量。function createCounter() { let count 0; return { increment() { count; return count; }, get() { return count; } }; } const counter createCounter(); console.log(counter.increment()); // 1 console.log(counter.get()); // 1count对外部不可见但被返回的两个方法持有了createCounter执行上下文的引用所以它不会被垃圾回收。作用有三个数据私有化、状态持久化、工厂模式。面试里更常问的是反面闭包会导致内存泄漏吗答案是可能会。如果闭包引用的环境里挂着一个巨大的 DOM 节点或数组而这个闭包被全局对象持有那这块内存永远无法释放。典型例子是事件监听器回调里捕获了大对象组件销毁后监听器没移除。解法是用WeakMap保存这类引用让键值对被回收时引用能自动清空。3.2 this 指向的四条调用规则与绑定优先级this 是 JavaScript 面试的分手题一半候选人折在这。其实规则只有四条按优先级排列场景规则this 指向独立调用默认绑定严格模式下undefined非严格模式全局对象对象方法调用隐式绑定调用它的对象call / apply / bind显式绑定指定的对象new 调用构造器绑定新创建的对象优先级从高到低newbindcall/apply 隐式绑定 默认绑定。常见坑有两个。第一个是丢失 thisconst obj { name: js, getName() { return this.name; } }; const fn obj.getName; fn(); // undefined此时是独立调用第二个是箭头函数。箭头函数没有自己的 this它捕获的是定义时的外层 this。所以事件回调里用箭头函数就能让this指向组件实例而普通函数需要先const self this或bind。面试答题时把四条规则背熟再补一句箭头函数不适用任何一条规则它是词法 this就完整了。3.3 高频手写题为什么防抖节流要用闭包防抖和节流我面试必考因为它是闭包、this、定时器、参数透传的综合体。先看防抖function debounce(fn, delay 300) { let timer null; return function(...args) { if (timer) clearTimeout(timer); timer setTimeout(() { fn.apply(this, args); }, delay); }; }为什么要用闭包因为timer需要在上一次调用和下一次调用之间共享状态而这个状态又不能暴露到全局。每次调用返回的函数都在访问同一个词法环境里的timer这就是闭包最典型的工程用途。节流的常见版本是时间戳 定时器:function throttle(fn, interval 300) { let last 0; let timer null; return function(...args) { const now Date.now(); const remaining interval - (now - last); if (remaining 0) { if (timer) { clearTimeout(timer); timer null; } last now; fn.apply(this, args); } else if (!timer) { timer setTimeout(() { last Date.now(); timer null; fn.apply(this, args); }, remaining); } }; }这里两个细节会被追问为什么用fn.apply(this, args)——为了保留调用方的 this。为什么last初始为 0——让第一次调用立即执行。能解释清楚这两点的候选人说明不是从博客抄的模板。4. 事件机制与异步编程事件循环这道关卡怎么答异步是 JavaScript 的灵魂事件循环是面试里最容易聊崩的一题因为它涉及浏览器规范、执行栈、任务队列多个层面。4.1 冒泡、捕获与事件委托的实战价值先说事件传播三阶段捕获从 window 到目标元素、目标、冒泡从目标元素到 window。addEventListener的第三个参数可以是布尔值true捕获阶段或false冒泡阶段默认也可以是对象{ capture, once, passive }。为什么要掌握这个因为事件委托是基于冒泡实现的。比如一个列表有上千个 li给每个 li 单独绑定监听器会创建上千个函数对象改成委托给父容器事件处理器的数量降到 1而且对后来动态添加的 li 同样生效document.querySelector(#list).addEventListener(click, (e) { const item e.target.closest(li); if (!item) return; console.log(item.dataset.id); });e.target.closest(li)是为了避免点击 li 内部的子元素时拿不到 li 本身。closest从当前元素往上找命中则返回找不到返回 null所以要先判空。这里stopPropagation和preventDefault的区别也要拎清前者阻止继续传播后者阻止默认行为两个不是一回事。4.2 宏任务与微任务的执行顺序一道题的完整推理过程面试官最爱出这类题——直接给一段代码让你说输出顺序console.log(script start); setTimeout(() console.log(timeout), 0); Promise.resolve() .then(() console.log(promise1)) .then(() console.log(promise2)); queueMicrotask(() console.log(microtask)); console.log(script end);输出顺序是script start、script end、promise1、microtask、promise2、timeout。推理过程整段代码先同步执行遇到setTimeout把回调放进宏任务队列遇到 Promise 的then把回调放进微任务队列queueMicrotask同样放微任务队列。当前同步代码执行完调用栈清空事件循环开始工作先清空整个微任务队列再取一个宏任务执行。微任务队列里promise1先执行它又追加了promise2但此时queueMicrotask的回调排在更早的位置所以先打promise1再打microtask再打promise2。最后清空微任务后才轮到宏任务timeout。在这个基础上再往前推一步Node.js 环境里process.nextTick的优先级高于微任务队列浏览器渲染一般发生在宏任务执行完、微任务队列清空后的间隙。面试答到这一层基本就不会被挑毛病了。4.3 从回调地狱到 async/await 的演进逻辑异步的考察除了背输出顺序还会问为什么要有 Promise这种设计题。我的答题框架是三个阶段每个阶段解决什么问题回调函数阶段异步结果通过回调传递问题是嵌套地狱、错误难以集中处理、控制流颠倒。Promise 阶段把异步操作抽象成对象用then/catch链式表达错误沿链传播配合Promise.all、Promise.race、Promise.allSettled组合多个异步任务。async/await 阶段语法糖让异步代码长得像同步代码try/catch一次接住整个流程的错误。手写题方面至少要把Promise.all手写出来我见过很多次了function promiseAll(promises) { return new Promise((resolve, reject) { const results []; let completed 0; if (promises.length 0) { resolve(results); return; } promises.forEach((p, i) { Promise.resolve(p).then(value { results[i] value; completed; if (completed promises.length) resolve(results); }, reject); }); }); }注意两点结果按下标存放而不是 push避免异步完成顺序不一致时错位数组为空时直接 resolve否则永远 pending 会卡死。这两点都是真实场景会踩的坑。5. 运行时报错与异常处理面试官爱追问的边界场景JavaScript 运行时报错是搜索热词说明这是新手最头疼的地方。面试里它常以这段代码哪里会崩的形式出现。5.1 常见运行时错误的类型与触发原因先给一张速查表把对象实例和触发时机对应上错误类型典型触发原因是否可被 try-catch 捕获ReferenceError访问未声明的变量、let/const 暂时性死区同步场景可以TypeError调用非函数、访问 null/undefined 的属性同步场景可以SyntaxError语法错误代码无法解析不能因为根本没运行RangeError栈溢出无限递归、无效数组长度可以但一般建议别捕URIErrorencodeURIComponent 等收到非法参数可以AggregateErrorPromise.any 全部被拒绝可以最经典的手写题是用 try-catch 能捕获 setTimeout 回调里的错误吗try { setTimeout(() { throw new Error(boom); }, 0); } catch (e) { console.log(catch); } // 实际不会打印 catch原因setTimeout回调是在当前调用栈之外、由事件循环另行推入的try的执行上下文早已结束。想捕获这种错误得在回调内部再包一层 try-catch或者用全局错误事件。这题几乎是我必问的因为它直接检验候选人是否理解调用栈和异步任务是两个执行空间。5.2 错误捕获的正确姿势同步、异步与漏网之鱼工程上的错误处理有三个层面的套路同步错误try-catch但不要在每行都包只在外部输入不可靠的边界比如解析 JSON、读取用户配置使用。异步错误Promise 场景用.catchasync/await 场景用try-catch包住await。注意如果await后面跟的不是 Promise它会被自动包装成立即 resolve 的 Promise错误照样能捕。漏网之鱼浏览器里监听window.addEventListener(error)和unhandledrejectionNode 里监听process.on(uncaughtException)和unhandledRejection。这是我做监控系统后的心得——线上 90% 的问题都发生在全局监听没覆盖的角落。还有一个关键认知Error 对象上name和message是给人看的stack是给 debugger 看的。生产环境上报错误时至少要带message和stack最好再加当前路由、用户操作步骤、页面状态快照。面试问你们怎么采集前端错误能答出这一层的候选人我会直接标记可以进团队。5.3 内存泄漏与性能排查的面试回答框架内存泄漏题在面试里越来越常见因为前端应用复杂度上来了。我的标准回答框架是三来源、两工具、一原则。三个常见来源全局变量污染挂到window上的对象永远不会被回收。定时器和监听器未清理组件销毁时setInterval还在跑事件监听器还挂着。闭包持有大对象缓存收集了大量数据或者引用链里挂着已卸载的 DOM。两个排查工具Chrome DevTools 的 Performance 录一段操作看内存曲线是否阶梯式上升不回落Memory 的 Heap Snapshot 拍两张快照比对找Detached DOM节点和(closure)项。回答里能说出这两个工具的名字比背一堆理论有用得多。一个原则能释放就释放null赋值、clearInterval、removeEventListener、WeakMap做缓存日常养成习惯。6. 工程化、跨端互调与框架题加分项怎么答才不像背答案最后一块是加分题区域。面试到这个环节基础题答得差不多剩下的就是看候选人有没有真正做过东西。6.1 WebView 里 OC 与 JavaScript 互调的底层原理OC 和 JavaScript 互相调用是个高频搜索词也是移动端混合开发面试的经典题。我见过太多人只答得出用 evaluateJavaScript但问JS 怎么主动调原生就卡住了。先厘清两条方向原生调 JSiOS 里 WKWebView 用evaluateJavaScript(_:completionHandler:)Android 里用evaluateJavascript()。原理都是把 JavaScript 字符串交给 WebView 的 JS 引擎执行所以传 String 拼接参数时要小心转义实际项目中建议用 JSON 序列化防止注入。JS 调原生iOS 用WKScriptMessageHandler页面里通过window.webkit.messageHandlers.名称.postMessage(数据)发消息原生侧注册对应 handler 接收。Android 常见的是addJavascriptInterface注入一个对象页面直接调对象的方法。更古老的方案还有拦截 iframe 跳转和 prompt 弹窗现在基本被 MessageHandler 取代了。能拉开差距的是下面几点通信时机页面刚加载时 JS 可能还没准备好原生就调 JS 会失败。成熟方案是消息队列——JS 先把要发给原生的消息 push 到数组原生注入一个 ready 回调等到 ready 再统一消费。数据序列化WKWebView 的postMessage走的是结构化克隆对象能直接传老式 URL scheme 只能传字符串复杂数据得 JSON.stringify。安全边界注入给 JS 的桥暴露面越小越好不要直接把文件系统、通讯录等能力挂上去每个 bridge 方法都要做入参校验。这题能答出方向-时机-序列化-安全四层的候选人说明他真的调过混合应用的接口不是背概念。6.2 框架对比与组件库选型别背对比表说说 Vue 和 React 的区别这种题每年都有。最差的回答是背一张模板/JSX、双向绑定/单向数据流的对比表我听都不想听。好的回答是讲原理和取舍都是从状态到视图核心是状态变化如何驱动界面更新。React 靠不可变数据配合 diff 算法由用户触发重新渲染Vue 靠 Proxy 做依赖追踪精确更新。各自的成本React 的自由度高但需要自己管理渲染时机和性能Vue 的响应式系统用起来简单但理解深层代理、ref和reactive的区别需要时间。生态博弈React 的 hooks 生态成熟Vue 在工程化默认配置上更省心。组件库选型题比如被搜到的 FullCalendar 这类日历组件也是同一个思路。面试官不是真要你评价某个库而是看你选型的方法论业务场景需要什么能力包的体积和定制成本维护活跃度和文档质量团队能不能在它基础上二次开发。能把需求分析 → 技术指标 → 风险评估 → PoC 验证这套流程讲出来比你说这个库很流行有说服力得多。6.3 给准备面试的读者几条实操建议最后说几条我自己带人时的经验不一定适合所有人但至少能帮你少走弯路所有手写题必须自己敲一遍眼睛会了手不会是最常见的悲剧。防抖节流、深拷贝、Promise.all、事件订阅器这几样建议从理解到默写都来一遍。答题时养成先结论、再推导、最后举例子的习惯。比如问事件循环先说微任务先于宏任务再演示代码推导最后补一句浏览器渲染时机。面试官要的是可复现的思维过程不是你背下来的段落。准备两三个真实项目里的问题复盘线上报了什么错你怎么定位的最终怎么修的。这类故事比任何八股都值钱因为它证明你有闭环解决问题能力。有条件就做模拟面试并且开着录音回放。你会发现自己在回答时有多少口头禅和逻辑断裂这是自测性价比最高的方式。别追求把所有题都答对。面试官在意的是你在不会的题面前如何思考、如何四处试探、如何在提示下推进。把一次面试当成一次结对调试心态会稳很多。我自己带过的候选人里最终拿到 offer 的都不是背题最狠的人而是那种能把自己的真实经验讲透、遇到不会的题还能保持分析姿态的家伙。JavaScript 这个语言坑多、生态乱但反过来想正因为水够深扎实的人才会被看见。把这些基础题练到懂原理、能手写、会讲场景你离 offer 就不远了。