ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

吃透JavaScript核心12题,前端面试稳过原型链与闭包

吃透JavaScript核心12题,前端面试稳过原型链与闭包 这两年我帮团队做过不少前端面试官自己也面过上百人。说个扎心的现象大多数候选人简历上写着“精通JavaScript”结果一问原型链就支支吾吾让手写防抖节流写出来的是一团乱码。而那些最终拿到 offer 的反而没有背一堆“最新前端面试题”就是把十二个 JS 核心问题吃透了。所谓核心不是网上那种“20道题背完进大厂”的填空题而是 JavaScript 这门语言最底层、最容易被工作场景反复触发的机制。原型、this、闭包、事件循环、异步、深浅拷贝、防抖节流、类型判断、数组方法、模块化、垃圾回收再加上作用域这十二个点几乎覆盖了前端面试 90% 的 JS 问答环节。搞懂它们不止是为了面试更是为了写代码时能预判风险、解释现象、快速调试。这篇文章不按面试题清单走而是按“面试官到底想听到什么”来拆。每个点我都说一下考察意图、必背结论、常见追问以及我实际面试候选人时最常遇到的说法。你看完可以直接拿去背也可以拿去自查知识树哪里是空的。1. 为什么面试官死磕这 12 个 JS 核心面试官问 JS 核心从来不是想看你背了多少 API而是想确认三件事你有没有真正理解这门语言的运行模型你能不能解释线上出现的诡异 Bug以及你在写业务代码时会不会主动规避坑。先说运行模型。JavaScript 是单线程语言但又有异步能力它靠事件循环来调度任务。你写一个setTimeout里面的回调什么时候执行这取决于宏任务与微任务的顺序。业务里最常见的场景是页面数据请求返回了但界面没更新你怀疑是异步时序问题或者一个forEach里发了好几个请求结果回调顺序乱七八糟。如果不懂事件循环这些问题就只能靠瞎试。再说诡异 Bug。同事说“这个变量怎么变没了”你一查发现是this指向错了或者“明明深拷贝了为什么改了新对象老对象也变了”那就是深浅拷贝没分清。这类问题几乎每天都在发生面试官拿真实场景一包装就成了“为什么”类考题。所以别怪面试官“尽问些用不上的”恰恰是用得上才会问。第三点是工程素养。防抖、节流、手写Promise、数组扁平化、去重这些是框架和组件的日常实现基础。组件库里的resize监听为什么用节流上传组件为什么用并发控制背后全是这些基础能力。面试官手写题其实是在考察你有没有写过、想没想过原理解析而不只是看过。我把这十二个点列了一张表方便你对照自查序号核心知识点面试考察意图高频问法1原型与原型链是否理解对象继承的机制“用原型链实现一个继承”2this 指向面对真实代码能否判断指向“说说这段代码输出什么”3闭包是否理解作用域与变量生命周期“闭包是什么有什么应用”4作用域与提升对 var/let/const 差异的敏感度“说说变量提升和暂时性死区”5事件循环对异步执行时序的掌握“宏任务微任务输出顺序”6Promise 与 async/await异步编程是否达到工程级别“手写一个 Promise 或实现并发控制”7深浅拷贝是否踩过引用共享的坑“实现一版深拷贝”8防抖与节流前端性能优化的基本功“手写带前置执行的防抖”9类型判断与转换对隐式转换的理解“为什么 typeof null 是 object”10数组与字符串高阶操作日常开发的熟练度“用 reduce 实现分组统计”11模块化是否理解构建工具原理“CommonJS 与 ES Module 有什么区别”12垃圾回收与内存管理能否避免常见内存泄漏“WeakMap 为什么能防止内存泄漏”下面我按“三座大山”“异步核心”“数据工程”“模块与内存”四组来展开说每组都是面试里的一个密集区。2. 三座必翻的大山原型链、this、闭包这三样是所有 JS 机制中最“劝退”的部分因为它们不像普通 API 可以靠记忆而是需要脑内建立一套模型。一旦建立起来后面看框架源码都轻松很多。2.1 原型链把那张经典的三角图画清楚考察原型链时面试官大概率会问“new发生了什么”或“instanceof是怎么实现的”。别急着答先把底层关系说清楚。每个函数都有一个prototype属性指向一个对象每个对象都有一个隐藏属性[[Prototype]]在浏览器里可以通过__proto__访问现在已经标准化但仍建议用Object.getPrototypeOf。当你访问一个对象的属性时引擎会先找对象自身找不到就沿着__proto__向上找直到Object.prototype再往上就是null这条链路就是原型链。经典的三角关系是函数的prototype对象的构造函数回指函数本身Fn.prototype.constructor Fn而函数的实例的__proto__指向它的构造函数的prototype。比如const arr [1,2]arr.__proto__ Array.prototypeArray.prototype.__proto__ Object.prototype。能一口气画出来说明你真懂。手写new是高频变体。它本质上是四步创建一个新对象、把这个对象的原型指向构造函数的prototype、把构造函数体内的this绑定到新对象上并执行、如果构造函数返回了对象则返回那个对象否则返回新对象。代码长这样function myNew(Fn, ...args) { const obj Object.create(Fn.prototype); const result Fn.apply(obj, args); return (result ! null typeof result object) || typeof result function ? result : obj; }这里有个容易被追问的细节为什么构造函数返回基本类型比如return 1时new出来的对象还是新对象因为规范里规定只有返回引用类型才覆盖this。理解了这一步才算没有死记硬背。instanceof的原理本质上就是沿着原型链找构造函数的prototype。手写版也不难function myInstanceof(instance, constructor) { let proto Object.getPrototypeOf(instance); while (proto) { if (proto constructor.prototype) return true; proto Object.getPrototypeOf(proto); } return false; }不过要注意一个边界Object.create(null)创建的对象没有原型instanceof会返回false这种极端情况能答出来会很加分。2.2 this 指向四种规则一眼看穿this是面试中的“送命题”很多候选人背了一堆规则却不会做题。我面试时最喜欢出的一句代码是const obj { name: obj, say() { setTimeout(function () { console.log(this.name); }, 0); }, }; obj.say();正确答案是输出undefined或取决于运行环境的全局this因为setTimeout里的普通函数最终由全局环境调用。有候选人答“obj”说明他把“在哪里定义的”和“在哪里调用的”搞混了。记住一句话this永远看调用方式不看定义位置。四条规则按优先级从低到高是默认绑定直接调用函数非严格模式指向globalThis或window严格模式为undefined、隐式绑定通过对象属性调用指向这个对象、显式绑定call、apply、bind强制指定、new绑定new创建的新对象。如果同时出现new的优先级最高。额外提一个坑bind返回的新函数如果再用new去构造this会指向新对象而不是bind绑定的那个值。这在规范里叫“new绑定优先于显式绑定”能答出来的人很少属于加分项。箭头函数是另一个重点。它没有自己的this捕获的是定义时外层的this且不能用call改变。面试里常见追问是“如果你把普通函数改成箭头函数输出会变吗”。答案当然会变因为它沿用的是外层obj的this。多练几道混着普通函数和箭头函数的题比背定义管用。2.3 闭包能说出作用和坑才算真懂闭包的定义一句话能说完函数在定义时记住了它所在的词法作用域即使外层函数执行结束内层函数依然能访问外层变量。但面试官很少满足于定义他会继续问“闭包能用来做什么”。应用场景我得先说三个最贴近工作的。第一函数工厂比如创建一个生成前缀字符串的函数固定前缀是通过闭包保留的。第二私有变量模块里用闭包隐藏状态只暴露接口这在写自定义组件时很常见。第三防抖节流本身也用到了闭包来保存定时器 ID这就能把两个知识点串起来。闭包最大的坑是内存泄漏。我印象特别深的一次是线上图表组件越用越卡排查后发现内部函数被绑定到全局事件上却一直引用着一个不断更新的实例对象这个对象里又挂着一个巨大的配置数据。每次切换页面都新增一个引用旧对象根本回收不掉。所以回答时要主动说怎么治理及时removeEventListener、把大对象置空、尽量用WeakMap保存引用这样显得你有实战意识。只要把“定义、形成原理、应用场景、内存治理”这四层讲完闭包这道题基本就稳了。3. 异步与数据副本事件循环、Promise、深浅拷贝这一组是前端工作的“事故高发区”也是面试题里的重头戏。顺序输出、请求并发、状态同步全是围绕这几个点展开。3.1 事件循环宏任务微任务的输出顺序先建立模型JavaScript 引擎执行代码时维护一个调用栈遇到异步函数不会立刻执行回调而是把它放进对应的任务队列。当前同步代码执行完后引擎会先清空所有微任务再取出一个宏任务执行这个宏任务执行过程中又可能产生新的微任务于是下一轮又先清空微任务。整个循环反复进行。所以常考的结论是同步代码 → 所有微任务 → 一个宏任务 → 当前宏任务产生的微任务 → 下一个宏任务 ...宏任务有哪些setTimeout、setInterval、I/O、UI 渲染、MessageChannel。微任务有哪些Promise.then、queueMicrotask、MutationObserver、process.nextTickNode 环境特有且优先级比普通微任务还高。经典面试题是判断这段代码的输出console.log(1); setTimeout(() console.log(2), 0); Promise.resolve().then(() console.log(3)); console.log(4);顺序是1、4、3、2。很多人错在以为setTimeout的回调会先于微任务执行其实“回调在 0 毫秒后进宏任务队列”和“微任务立刻执行”不是一回事。当前同步代码结束后微任务队列是空的集合它会先执行3然后才会轮到宏任务里的2。候选人如果能顺手提一嘴 Vue 的nextTick为什么会用微任务因为 DOM 更新后需要尽快执行回调微任务比宏任务时机更早面试官会高看你一眼。这个细节说明你有把基础知识和框架原理连起来的能力而不只是背了事件循环的八股。3.2 Promise 与 async/await从状态机到手写实现Promise是一套状态机pending可以变为fulfilled或rejected一旦状态改变就不可逆。面试官最爱问“Promise怎么处理异常”“async函数里的throw能不能被try/catch捕获”本质上都是考状态流转。async/await本质是 Generator 加自动执行的语法糖。每次await相当于暂停当前函数把后续代码包装成微任务扔进队列。这里有个常见坑await后面跟的是一个已经resolve的Promise时后续代码也不是同步执行而是要等微任务轮转。所以async函数里的时序不能想当然。手写Promise是很多中高级岗位的必考环节。你不用写完整的Promise/A规范但至少抓住三个关键点resolve和reject只能改变一次状态then必须返回一个新的Promise才能支持链式调用传入then的回调需要放进微任务简化时用queueMicrotask或Promise.resolve().then模拟。简单骨架可以写成这样class MyPromise { constructor(executor) { this.state pending; this.value undefined; this.reason undefined; this.onFulfilled []; this.onRejected []; const resolve (value) { if (this.state ! pending) return; this.state fulfilled; this.value value; this.onFulfilled.forEach((fn) queueMicrotask(() fn(value))); }; const reject (reason) { if (this.state ! pending) return; this.state rejected; this.reason reason; this.onRejected.forEach((fn) queueMicrotask(() fn(reason))); }; executor(resolve, reject); } then(onFulfilled, onRejected) { return new MyPromise((resolve, reject) { // 这里需要处理回调返回值如果返回 Promise 则需要等待它 }); } }写到这里一定要提一句“透传”细节也就是then里返回值如果是Promise新的Promise要等它完成再resolve。这能体现你对链式调用的理解比完整默写规范更容易拿分。再补一个实际工作中很常用的并发技巧用Promise.allSettled而不是Promise.all当一个请求失败时不拖垮整个批量任务。这个点面试官可能不问但你要是主动说出来会显得有实战经验。3.3 深浅拷贝从 JSON 方法的坑到 WeakMap 防循环深浅拷贝是前端写业务代码时最容易踩的雷。浅拷贝只复制第一层属性嵌套对象仍然是引用。典型实现是展开运算符和Object.assignconst obj { a: 1, b: { c: 2 } }; const copy { ...obj }; copy.b.c 99; console.log(obj.b.c); // 99因为嵌套对象还是同一个引用解决方案是深拷贝。最偷懒的写法是JSON.parse(JSON.stringify(obj))但它的坑非常固定undefined、函数、Symbol会直接被丢弃Date会被转成字符串RegExp会变成空对象循环引用会直接抛错。所以面试官才会追问“如果数组里有undefined怎么办”“遇到循环引用怎么处理”。一版能拿分的深拷贝需要做到判断Map、Set、Date、RegExp这些特殊类型用WeakMap记录已经拷贝过的对象遇到循环引用时直接返回缓存的副本递归复制数组和普通对象。核心片段如下function deepClone(target, map new WeakMap()) { if (target null || typeof target ! object) return target; if (map.has(target)) return map.get(target); if (target instanceof Date) return new Date(target); if (target instanceof RegExp) return new RegExp(target.source, target.flags); const cloneTarget Array.isArray(target) ? [] : {}; map.set(target, cloneTarget); Reflect.ownKeys(target).forEach((key) { cloneTarget[key] deepClone(target[key], map); }); return cloneTarget; }注意这里用Reflect.ownKeys才能把Symbol类型的键也拷贝进去这是很多答案会漏掉的一点。如果候选人能说出“为什么不直接for...in因为它会遍历原型链上的可枚举属性”那这道题就答得很扎实了。顺便说一下浏览器现在有原生structuredClone能处理多数场景但碰到函数和Symbol还是会抛错。所以手写深拷贝在面试里短期内不会过时它是考察递归、类型判断、引用处理能力的综合体。4. 高频手写与类型方法防抖、节流、类型判断、数组操作这类题被面试官称为“性价比最高”的题出题简单但很容易看出候选人有没有真的写过代码。背答案和会推导写出来的代码完全不一样。4.1 防抖与节流背熟的代码为什么一改就错防抖和节流的概念一句话就能区分防抖是“你一直触发我就一直重置定时器直到你停了我才执行”节流是“固定时间间隔内只执行一次”。前者适合搜索输入后者适合滚动监听和resize。防抖的标准实现长这样function debounce(fn, delay 300) { let timer null; return function (...args) { clearTimeout(timer); timer setTimeout(() { fn.apply(this, args); }, delay); }; }注意我用的是function ... args而不是箭头函数因为需要让内部的this指向调用者同时把事件对象event透传进去。很多候选人喜欢写箭头函数结果问“如果事件处理函数里需要拿event怎么办”就卡住了。这里的this和arguments保存是手写题最容易失分的点。节流有两种常见版本。时间戳版记录上次执行时间第一次触发会立即执行定时器版则在delay后执行能保证最后一次触发后还会执行一次。简单实现function throttle(fn, interval 300) { let last 0; return function (...args) { const now Date.now(); if (now - last interval) { last now; fn.apply(this, args); } }; }但只答这一版容易被追问“最后一次触发没执行怎么办”。结合定时器版才能覆盖首尾两个场景。面试官要的不是极其复杂的underscore实现而是你能说清每种版本的取舍并能在页面滚动场景里解释“为什么底部要多执行一次”。4.2 类型判断typeof、instanceof、toString 三件套typeof的坑大家都知道typeof null返回object这是历史遗留typeof []返回objecttypeof function(){}返回function。所以单纯靠typeof做类型检查在真实项目里根本不够用。instanceof能判断[]是不是Array但它只能用于引用类型且如果跨iframe或者多窗口原型链会断裂导致判断失败。Array.isArray为什么更稳因为它不依赖原型链而是直接检测内部属性标识。完整方案是Object.prototype.toString.call()这也是各类类型判断库的核心原理Object.prototype.toString.call([]); // [object Array] Object.prototype.toString.call(null); // [object Null] Object.prototype.toString.call(/x/); // [object RegExp] Object.prototype.toString.call(new Date()); // [object Date]写一个通用函数就是function getType(value) { const typeStr Object.prototype.toString.call(value); return typeStr.slice(8, -1).toLowerCase(); }另一个面试常考点是字符串与数组包含判断includes、indexOf、search、match的区别。判断字符串是否包含子串现代写法用includes就可以了需要忽略大小写可以把双方都toLowerCase()或给正则加i标志。这些点虽然简单但面试时会和类型判断一起包装成“写一个方法判断输入值是否为合法 URL”之类的题。合法 URL 校验最好用new URL(input)加try/catch还能判断协议是否http:或https:比手写正则靠谱得多。4.3 数组高阶方法reduce 是手写题的万能主角数组方法是前端开发每天都要用的技能。map是映射、filter是筛选、forEach是遍历、reduce是聚合。面试官特别喜欢让候选人用reduce实现其他方法因为能看出对不同方法语义的理解。比如用reduce实现分组const data [ { city: 上海, value: 3 }, { city: 北京, value: 5 }, { city: 上海, value: 8 }, ]; const result data.reduce((acc, item) { (acc[item.city] acc[item.city] || []).push(item.value); return acc; }, {});再比如数组去重的多种写法Set一行版、filter indexOf版、reduce版。候选人如果能说清indexOf的时间复杂度以及为什么Set去重对大数组性能更好那就不只是会背 API 了。扁平化flat和手写递归扁平化也是高频题关键是能控制深度参数。这里给一个最容易丢分的细节map和forEach的区别。forEach的解引用是忽略执行结果的map会把每次回调的返回值收集成新数组。所以“用forEach实现map”这种反问题几乎能拦住一半候选人其实只用加一个空数组push就行。字符串相关的split、join、trim、substr/substring区别也可以顺手复习前端传参时经常做格式化。把数组和字符串那套方法串成一条线面试时无论怎么变体都能接到。5. 模块化与内存管理从工程视角理解 JS 核心走到这里基础知识已经差不多了但中高级面试还会往工程方向再挖一层。模块化和垃圾回收就是常见的“工程化分水岭”。5.1 模块化CommonJS 与 ES Module 的本质差别前端面试题里必有一道“说说require和import的区别”。很多人答“一个是 CommonJS一个是 ES Module”这是正确的废话得说透。CommonJS里的require是运行时加载它本质是一个函数调用可以在if里动态使用module.exports导出的是一个对象值是拷贝引用。而ES Module的import是静态加载写在哪都无所谓因为会提升到模块顶部export的变量是实时绑定的导入方看到的是同一份活的引用。这里有个经典追问循环依赖。CommonJS遇到循环依赖时只输出已经执行完的部分ES Module则会通过动态绑定在后续正确取到值。理解区别以后能解释为什么像Tree-shaking这种功能只能依赖ES Module实现因为静态分析靠的就是import/export这种确定性的声明语法。在实际开发里typemodule的脚本天然具有defer行为并且在严格模式下运行这两点也可以补充进去。答出“浏览器原生模块默认defer”能让面试官觉得你在真正用ES Module写项目而不是只背概念。5.2 垃圾回收从闭包聊到 WeakMap 的使用场景JavaScript 的垃圾回收不是靠delete就能完成的V8 引擎主要使用可达性分析加标记清除。对象没有被任何根节点引用时才会被回收。现在常见的分代回收模型中新生代常驻在内存较小区域对象经过一轮又一轮Scavenge复制后若还存活就晋升到老生代。面试官问到垃圾回收重点通常不在理论而在“你怎么防止内存泄漏”。常见的泄漏来源有五个意外创建的全局变量、未解绑的定时器和事件监听、被闭包长期持有的 DOM 对象、脱离DOM引用后依然被JS变量保存的节点、以及Map里存放的目标对象。第四和第五个在真实项目里非常隐蔽我见过一个数据大屏项目每 10 秒刷新一个Mapmap 的 key 存的是组件实例对象页面切换后老实例永远不会被回收内存曲线一路涨到崩溃。这时候WeakMap的价值就体现了它的 key 必须是对象且是弱引用不阻止垃圾回收。用WeakMap做缓存和关联数据即使你忘了手动清理也不会造成泄漏。同样的道理适用于WeakSet。能主动说出这个场景的候选人说明他真的踩过坑。如果还能补一句“所以前面深拷贝用WeakMap缓存不只是因为需要Map特性更是为了避免缓存对象本身导致内存泄漏”那前后两个知识点就完美闭环了。6. 面试回答的组织技巧与临场心态最后一章不聊具体知识点聊一个经常被忽略的事很多人知识都会但面试时回答得一团糟问题就出在表达结构上。面试官不是机器他需要从你的叙述里判断“你究竟理解到哪一层”所以回答方式本身就是一种能力。6.1 黄金回答结构结论先行再展开三层面试官问“讲讲闭包”时最好的回答不是一上来背定义而是先说“闭包就是函数带着外部变量的引用最常见的用途是封装状态”然后给出一个 5 行代码的最小例子。比如计数器function createCounter() { let count 0; return () count; }这个例子 3 秒钟就能说完但信息量很大它展示了函数返回函数、外层变量被保留、每次调用共享同一个count这三个闭包核心特征。说完例子再讲一个边界或坑比如“如果这里返回的箭头函数被保存到全局并且外层还挂着大量配置数据就可能造成内存无法释放”最后落回应用场景“我在封装防抖函数时就用闭包保存定时器 ID”。这一整套下来大约 1 分钟面试官听到的是概念准确、有例证、有坑点、有应用四个维度全中。比你背 200 字定义要生动得多。6.2 被追问“还有吗”时的兜底姿势很多候选人被追问以后就慌了其实“还有吗”通常不是否定你而是想看看你的知识边界在哪里。这种时候不要硬憋新概念把已有的回答从四个方向扩展原理层面、边界情况、性能表现、工程化落地。比如你刚讲了this四种规则可以补充“在严格模式下默认绑定会是undefined所以类方法里直接用setTimeout很危险”。这既是边界情况也带出了“那该怎么解决”这个话题自然能引导到箭头函数或bind。如果讲Promise可以补一句“并发请求时我会优先用Promise.allSettled避免单个失败导致整体崩溃”这就从原理跳到了工程实战。别小看这句补的时机它能把你和只会念八股的候选人区分开。6.3 遇到不会的题先复述题意再拆解没有任何人所有题目都会。关键是怎么处理“不会”这也是一项面试技能。我的建议是先用不超过一句话复述题意比如“你让我手写Promise.all我理解它的作用是并发执行多个 Promise并且等待全部成功对吗”。这既能确认自己没有理解偏也能给自己争取一点思考时间。然后拆解思路Promise.all无非是把数组里的每一个Promise都then起来记一个计数器或者记录结果数组全部完成再resolve有任何一个reject就整体失败。即便你写不完全部代码把骨架和处理边界说清楚面试官也会给你基础分。最怕的是愣在那里不出声或者直接说“没准备过”。面试官也是人他实际想看的是你遇到未知问题的处理路径而不是你是个移动题库。最后一件事回答时要有停顿和节奏。说完一个关键点停两秒观察对方是否问细节比一口气输出十分钟强得多。我见过有些候选人把图书馆目录倒背如流但面试官想追问时他已经切到下一个问题了这种沟通方式是面试里最可惜的失分项。我自己总结下来前端面试近几年包装花样变了很多但内核一直没变原型链、异步、数据操作、工程意识。把这 12 个点练成自己的东西不只是为了那几道题而是你在日常写代码时能更清楚地知道自己在做什么、为什么这么做。希望这份拆解能帮你把知识线串起来面试时候的底气自然也就出来了。
返回列表