十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

JavaScript面试八股文:高频考点与手写题全攻略

JavaScript面试八股文:高频考点与手写题全攻略 九、十月份是秋招和跳槽的黄金期圈子里叫“铜九铁十”。这段时间我身边不少同学都在准备前端面试问来问去绕不开的永远是JavaScript。面试官不管考框架多深最后都会落到语言本身闭包、this、事件循环、手写题、运行时报错排查这些是前端面试的“八股文”也是最容易拉开差距的地方。很多候选人项目经历写得漂亮一到JavaScript基础就露馅非常可惜。这篇内容我按自己面试别人和被别人面试的经验把JavaScript这部分的高频考点和答题思路整理出来覆盖基础、手写、异步、实战几个维度适合正在备战面试的朋友直接拿去背、拿去练。1. 核心基础考点数据类型、作用域与闭包1.1 数据类型判断从typeof到Object.prototype.toString面试官很喜欢从“JavaScript有哪些数据类型”开始切入。这个问题看似简单但回答的深度直接决定后面问题的难度。标准答案分成两种原始类型Primitive Types和引用类型Reference Types。原始类型包括string、number、boolean、null、undefined、symbol、bigint引用类型主要是object数组、函数、日期都是object的细分。紧接着会被追问“如何判断数据类型”。这里有几个层次typeof适合判断原始类型但有两个著名坑点typeof null返回objecttypeof function(){}返回function而不是object。前者是历史遗留bug后者是特意设计的。instanceof用于判断引用类型但它的原理是沿着原型链查找所以[] instanceof Object也是true无法精确判断具体类型。Object.prototype.toString.call()这个才是终极方案。Object.prototype.toString.call([])返回[object Array]call(/a/)返回[object RegExp]可以精确区分所有内置类型。我在面试别人时最喜欢让候选人手写一个getType函数。大部分人能写出来但会忽略null这个边界情况。这里把我的标准版本贴出来function getType(value) { if (value null) return null; if (typeof value ! object) return typeof value; return Object.prototype.toString.call(value).slice(8, -1).toLowerCase(); }这个版本的逻辑是先处理null和原始类型再对引用类型做精细判断实际项目中完全够用。注意不要在typeof value ! object这里把函数丢掉函数应该由typeof直接返回。1.2 作用域与闭包面试官真正想听什么闭包是JavaScript面试的“必考题”但很多人的理解停留在“函数嵌套函数内层函数可以访问外层函数的变量”。这个说法没错但太浅了。面试官真正想听的是闭包的产生机制、解决了什么问题、带来了什么副作用。要理解闭包先要理解作用域链。JavaScript在创建函数时会保存一个[[Environment]]引用指向函数定义时的词法环境。函数执行时会把这个词法环境作为作用域链的最前端。当内层函数引用了外层函数的变量外层函数执行完毕后本应销毁的词法环境因为被内层函数引用而保留在内存中这就是闭包。我一般建议候选人用一个计数器来串讲function createCounter() { let count 0; return function () { count; return count; }; } const counter createCounter(); console.log(counter()); // 1 console.log(counter()); // 2这里count变量没有被回收就是闭包在起作用。回答时要主动提到闭包的三个特点变量私有化、状态保持、内存驻留。后面两点是双刃剑尤其要主动提内存问题展示你有性能意识。我在面试中只要听到候选人主动讲出“闭包可能导致内存泄漏因为词法环境不会释放”第一印象就会很好。1.3 this指向四种绑定规则与箭头函数例外this指向问题是JavaScript八股文里翻车率最高的点之一。很多候选人刷了题但换道题又错。核心原因是没有理解this的绑定规则。默认绑定非严格模式下独立函数调用fn()this指向window/global严格模式下this是undefined。隐式绑定obj.fn()这种形式this指向对象obj。显式绑定call、apply、bind可以强制指定this。new绑定构造调用时this指向新创建的对象。箭头函数没有自己的thisthis继承自外层词法作用域。面试里最难的是“隐式绑定丢失”问题。比如const fn obj.fn; fn();这里this指向window而不是obj因为函数被赋值后调用位置已经没有obj前缀了。同理setTimeout(obj.fn, 100)也会丢失绑定因为setTimeout里的回调是独立调用。我推荐一个答题模板见到this题目先看函数是不是箭头函数如果是直接找外层作用域如果不是看调用方式是函数名()还是对象.方法()还是new调用再套对应规则。这一套流程走完正确率能到90%以上。1.4 原型链与继承别只背“寄生组合式继承”原型链考的是对JavaScript对象系统的理解。面试官问“new的时候发生了什么”不只是让你背四步流程。四步是创建空对象、将空对象的__proto__指向构造函数的prototype、将this指向空对象并执行构造函数、如果函数没返回对象则返回这个空对象。然后会考“手写寄生组合式继承”。这里不要直接甩代码先讲思路子类的prototype要指向父类prototype的副本同时修复constructor指向。标准写法function inherit(child, parent) { child.prototype Object.create(parent.prototype); child.prototype.constructor child; } function Parent(name) { this.name name; } Parent.prototype.say function () { console.log(this.name); }; function Child(name, age) { Parent.call(this, name); this.age age; } inherit(Child, Parent); const child new Child(张三, 18); child.say(); // 张三注意Object.create(parent.prototype)不能直接写成child.prototype parent.prototype否则修改子类原型会污染父类。也不能用new parent()会多执行一次父类构造函数。这个细节面试官一定会追问提前准备好。2. 高频手写题防抖、节流、深拷贝与数组方法2.1 防抖与节流场景决定方案防抖和节流是手写题里出现频率最高的题目。我统计过自己参与过的几十场前端面试几乎每两场就有一场考这个。两者的核心区别要一句话说清防抖是“最后一次执行”节流是“固定频率执行”。防抖的经典场景是搜索框输入。用户连续输入时只在停止输入后一段时间内发起请求。节流的经典场景是滚动监听、按钮防连点保证一定时间内只执行一次。防抖手写要分成“立即执行版”和“非立即执行版”我面试时如果候选人只写出一种会追问另一种能写出来的不多。这里给出完整版function debounce(fn, delay, immediate false) { let timer null; return function (...args) { const callNow immediate !timer; if (timer) clearTimeout(timer); timer setTimeout(() { timer null; if (!immediate) fn.apply(this, args); }, delay); if (callNow) fn.apply(this, args); }; }注意保存this因为事件监听里this指向当前DOM元素直接用fn(args)会让this丢失。节流的实现有两种时间戳版和定时器版。时间戳版先执行定时器版后执行。回答时主动说出两者的区别会显得你有思考深度。2.2 数组去重从双层循环到Set的演进数组去重也是经典手写题但我更建议大家把它当作“一题多解”的展示机会。从最简单的Set去重到filter去重再到考虑对象属性、NaN、Symbol等边界情况的版本层层递进展示知识广度和边界意识。const arr [1, 2, 2, 3, NaN, NaN]; const unique [...new Set(arr)]; console.log(unique); // [1, 2, 3, NaN]上面这个版本能处理NaN因为Set使用SameValueZero比较NaN等于NaN。但如果是数组里的对象Set去重是无效的因为每个对象引用不同。这时候可以用Map按唯一标识去重。回答到这一层面试官通常会点头进入下一题。除了去重数组遍历也是高频考法。forEach、map、filter、reduce、some、every都要能说出区别forEach不返回新数组、不可中断map返回等长新数组filter返回筛选后的新数组reduce可以做累积操作。这里有个细节很多人不知道forEach和map都不能用break中断只能用some返回true或者用for...of。能主动提到这个很加分。2.3 深拷贝从乞丐版到面试版深拷贝是手写题里的“分水岭”。简单的递归拷贝谁都会写但完整处理各种边界情况的版本能过滤掉大部分候选人。基础版function deepClone(obj) { if (obj null || typeof obj ! object) return obj; const result Array.isArray(obj) ? [] : {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { result[key] deepClone(obj[key]); } } return result; }这个版本有哪些问题一是循环引用会栈溢出二是Date、RegExp、Map、Set等特殊对象会被拷贝成普通对象三是函数的处理。面试版至少要解决循环引用用WeakMap缓存已经拷贝过的对象。function deepClone(obj, cache new WeakMap()) { if (obj null || typeof obj ! object) return obj; if (obj instanceof Date) return new Date(obj); if (obj instanceof RegExp) return new RegExp(obj); if (cache.has(obj)) return cache.get(obj); const result Array.isArray(obj) ? [] : {}; cache.set(obj, result); for (const key of Object.keys(obj)) { result[key] deepClone(obj[key], cache); } return result; }这里用Object.keys而不是for...in是为了不遍历原型链上的属性。这个版本题型覆盖了“为什么用WeakMap”“为什么v8引擎的structuredClone能处理更多类型”这些追问题值得好好研究。2.4 手写Promise.all与reduce掌握异步与数组的交叉题手写Promise.all是近几年面试的新宠它综合考察了Promise原理、异步并发控制、错误处理。回答时要先说清楚Promise.all的行为接收一个可迭代对象全部resolve后返回结果数组任何一个reject就立即reject。function promiseAll(promises) { return new Promise((resolve, reject) { const results []; let count 0; if (promises.length 0) { resolve(results); return; } promises.forEach((promise, index) { Promise.resolve(promise).then( (value) { results[index] value; count; if (count promises.length) resolve(results); }, (reason) reject(reason) ); }); }); }注意这里不能用results.length promises.length判断完成因为异步任务完成顺序不一定和索引顺序一致如果最后一个先完成results.length已经等于数组长度但前面的还没填充会导致提前resolve。这是一个非常隐蔽的bug我在面试中专门用这道题“钓鱼”能发现这个坑的候选人凤毛麟角。3. 事件循环与异步编程输出顺序题的正确打开方式3.1 宏任务、微任务与async/await的执行顺序JavaScript的事件循环是面试必考且几乎每场都会出“打印顺序”的题目。很多候选人死记硬背“先微任务后宏任务”这句话一遇到async/await就出错。实际上要理清楚的是每次执行宏任务之前都要把当前微任务队列清空。Promise.then里的回调是微任务async函数里await后面的代码也是微任务。setTimeout、setInterval、I/O、UI渲染这些是宏任务。微任务优先于宏任务执行但不是说所有微任务执行完才执行宏任务而是“每一个宏任务之后都要把微任务清空”。经典打印题console.log(script start); setTimeout(() console.log(timeout), 0); Promise.resolve().then(() console.log(promise)); async function foo() { console.log(async start); await bar(); console.log(async end); } async function bar() { console.log(bar); } foo(); console.log(script end);正确答案是script start - async start - bar - script end - promise - async end - timeout。这里最容易被忽略的是async end。await bar()这行会先执行bar()同步代码输出bar然后await后面的代码相当于放进了微任务队列。所以虽然Promise.resolve().then先注册但async end要等bar()执行完后才注册。理解这个执行顺序的关键是await不是全部立即让出而是先执行右侧表达式再把后续代码塞进微任务队列。3.2 手写一个简易Promise理解then链的机制如果面试官看了你的输出顺序题觉得基础不错很可能会进一步问“能不能手写一个Promise的核心逻辑”。这道题很大但核心其实只有两点状态机和then链。Promise有三个状态pending、fulfilled、rejected状态只能从pending流转一次。then方法要支持链式调用因此需要返回一个新的Promise并且在回调里处理返回值的resolve或reject。核心骨架class MyPromise { constructor(executor) { this.state pending; this.value undefined; this.reason undefined; this.onFulfilledCallbacks []; this.onRejectedCallbacks []; const resolve (value) { if (this.state pending) { this.state fulfilled; this.value value; this.onFulfilledCallbacks.forEach((fn) fn()); } }; const reject (reason) { if (this.state pending) { this.state rejected; this.reason reason; this.onRejectedCallbacks.forEach((fn) fn()); } }; executor(resolve, reject); } then(onFulfilled, onRejected) { // 处理链式调用返回新的promise } }写到这里已经能说明对Promise的理解了但要注意回调数组的设计不是为了支持同一个promise多次then而是为了处理resolve在executor内部是异步调用的情况即先注册回调后状态才改变。很多候选人漏掉这个点我认为这是最值得背下来的“底层细节”。3.3 定时器延迟与事件循环的“排队”问题setTimeout(fn, 0)真的会立即执行吗不会。它只是把回调放进宏任务队列真正执行时间取决于当前调用栈和之前的微任务队列是否为空。这也是面试中常考察的“setTimeout不准时”问题。回调会等当前代码执行完毕、微任务清空后才执行如果前面有大量同步计算或微任务实际延迟远大于设置的0毫秒。更隐蔽的坑是嵌套setTimeout的最小延迟限制。HTML规范要求嵌套层级超过5层后每次延迟最小为4ms。这就是为什么有些高精度任务用setTimeout会“变慢”的原因。面试中如果被问到“如何实现一个高精度的定时器”可以用requestAnimationFrame或Web Worker的方案回答多数候选人不会能讲到这层基本就锁定胜局了。3.4 内存泄漏闭包、定时器与监听器的回收前面提到闭包可能导致内存泄漏面试官通常会顺着追问“还有哪些常见的内存泄漏场景”。这个问题考察的是代码实践经验不是背诵能力。典型场景有全局变量意外泄漏未声明的变量直接赋值会在非严格模式下挂到window上。定时器未清理setInterval创建后没有clearInterval回调引用的对象无法释放。事件监听器未移除DOM元素被移除时监听器还挂在它身上或者监听器引用了外部变量。闭包持有大量数据闭包内部引用了大对象即使外层函数执行完毕该对象仍被闭包引用。我在实际项目中排查内存泄漏的经验是先用Chrome DevTools的Performance面板录制一段页面操作观察内存曲线是否持续上升不回落然后利用Memory面板的Heap Snapshot对比两次快照过滤出增长较大的对象再通过Retainers面板分析这个对象被谁引用着。这一步只能挨个看引用链定位到具体代码。这个排查思路如果面试时能讲出来会比单纯背概念有效得多。4. 运行时报错与工程化实战从void(0)到微前端4.1 javascript:void(0)和javascript:void(o)报错一个古老的坑近期很多人搜“javascript:void(0)”和“javascript:void(o)报错”这两个问题其实是同一个根源。javascript:是一个URL协议浏览器会把它后面的内容当作JavaScript代码执行。void(0)表示执行0然后返回undefined。在a标签的href里写javascript:void(0)是为了让点击链接时不跳转、不刷新页面而交由onclick事件处理。为什么有人写成javascript:void(o)会报错因为o不是已定义的变量引擎会抛出ReferenceError: o is not defined。这个报错通常发生在有人手误把数字0打成了字母o或者复制代码时被混淆过。解决办法很简单要么改回void(0)要么干脆不用javascript协议用href#配合event.preventDefault()。实际面试中这个问题常被包装成“a标签hrefjavascript:void(0)和href#有什么区别”来问。答案是void(0)不会改变URL和滚动位置而href#会跳到页面顶部需要在点击事件里return false才能阻止。这个细节理解到位说明候选人真的处理过实际页面问题。4.2 运行时报错的类型与排查思路前端JavaScript运行时报错是每个开发者都绕不开的问题。面试中会被问到“你遇到过哪些运行时报错是怎么排查的”这里要能说出具体错误类型和排查路径。ReferenceError变量未定义。排查思路是用浏览器的Sources面板在报错位置打断点看作用域链里到底缺了哪个变量。TypeError读取了undefined/null的属性。这是最频繁的报错排查时先看报错信息里“Cannot read property xxx of undefined”然后去数据源排查字段是否为空。RangeError递归太深或数组越界。常见于无限递归排查时看调用栈。SyntaxError语法错误通常在构建阶段就会暴露浏览器里出现的概率低。URIError、EvalError比较少遇到知道存在即可。我自己的排查习惯是先看错误信息的堆栈Stack Trace定位到具体文件和行号然后再看作用域Scope面板里变量的当前值如果数据是从接口来的还要Network面板确认返回内容是否符合预期。最忌讳的是不看堆栈直接猜原因。七成以上的js报错堆栈已经告诉你了答案。4.3 严格模式与模块化ESM和CommonJS的区别严格模式use strict在面试中出现的频率也不低。要记住几个关键差异严格模式下this默认指向undefined而不是全局对象禁止给未声明的变量赋值禁止删除不可删除的属性函数的参数名不能重复八进制字面量被禁止。其中“this指向undefined”是最高频的考点因为它直接对应了前面讲的this默认绑定规则。模块化方面ESM和CommonJS的区别是必考ESM的import/export是静态分析可以在编译期确定依赖关系支持tree-shakingCommonJS的require/module.exports是运行时加载。ESM在浏览器端原生支持CommonJS是Node.js的规范需要构建工具转换才能在浏览器用。ESM的this是undefinedCommonJS模块顶层this指向module.exports。ESM是引用拷贝CommonJS是值拷贝。这句话展开讲CommonJS输出的是值的拷贝模块内部变化不影响外部已拿到的值ESM输出的是值的引用模块内部变化外部能感知到。能把这四点讲清楚面试官基本上就会跳过工程化切换下一题了。4.4 Worker与微前端JavaScript在复杂场景下的表现热词里出现了“前端使用worker上传大文件”和“微前端”这两个点其实是JavaScript进阶面试的方向。Web Worker可以让JavaScript在后台线程执行避免阻塞主线程。上传大文件时可以在Worker里做文件分片、计算哈希比如用SparkMD5计算文件指纹主线程只负责UI交互。这样即使文件有好几个GB页面也不会卡死。这个思路在面试中属于“加分项”能主动讲出来说明你有大规模文件处理的实战经验。微前端考察的是JavaScript的隔离能力。多个子应用同时运行在页面上怎么避免变量冲突、样式冲突方案有JS沙箱用with语句配合Proxy拦截全局变量的读写、样式隔离CSS Modules、Shadow DOM、通信机制自定义事件、全局状态管理。面试官会重点问“如何实现一个简易的JS沙箱”核心是用Proxy监听window上的属性访问只允许子应用访问属于自己的命名空间。这个问题很综合但答好了会非常出彩。5. 面试答题经验从背答案到讲思路5.1 答题节奏先给结论再展开原理我面试过不少候选人发现最大的通病是回答问题时想到哪儿说到哪儿没有结构。比如被问到“什么是闭包”直接开始背定义背完就结束了完全没有展开。我建议的回答模板是先一句话给结论比如“闭包是函数与其词法环境的组合它让函数可以访问定义时的作用域变量”然后给一个最简单的例子最后讲它的应用场景私有变量、函数柯里化和副作用内存驻留。这个节奏控制在1分半到2分钟之间最合适。如果面试官没有喊停就继续往下延伸可以主动讲“闭包在react函数组件中的表现”因为函数组件每次render都会生成新的闭包这就联系到了useCallback、useRef的作用。从八股文延伸到框架原理是区分“背题型选手”和“实战型选手”的关键一步。5.2 面试官追问的深水区如何展示知识纵深八股文背熟了只能到及格线想拿高分必须能接住追问。面试官最喜欢追着某个点往下深挖。比如你提到“深拷贝用WeakMap解决循环引用”他会问“为什么是WeakMap而不是Map”。答案是WeakMap的键是弱引用不会阻止垃圾回收。如果用Map缓存的对象永远不会被回收本身就是内存泄漏。这个追问如果你能答上来直接证明了你是“理解了”而不是“记住了”。另一个常见追问是“Set的NaN去重是怎么实现的”。这里要提到SameValueZero算法它和严格等于的区别在于SameValueZero认为NaN等于自身同时认为0和-0相等。这套比较算法是ES规范里的核心很多候选人完全没听过能答出来就是亮点。5.3 面试前最后一周手写题和概念题的复习清单根据自己的面试经验我整理了一份JavaScript八股文的复习优先级清单必背手写题防抖节流、深拷贝、Promise.all、数组去重、函数柯里化、new的实现、call/apply/bind的实现。必背概念题事件循环与打印顺序、原型链、闭包、this绑定、模块化差异、垃圾回收机制。高频场景题大文件上传、前端并发请求控制、大量数据渲染优化、前端错误监控。这里我特别想强调“并发请求控制”。它通常会包装成“有100个请求但浏览器最多同时发10个怎么控制并发”解法用递归或队列本质是生产者和消费者模型。这道题既能考察异步编程又能考察设计能力出现频率不低值得提前练一练。最后再分享一个小技巧。面试前一天不要继续刷新题把已经写好的手写题代码重新默写一遍。你会发现很多“我以为会了”的代码真的动笔写的时候会卡壳。八股文这种东西临时记忆不会管用肌肉记忆才是关键。把每一道手写题练到不需要思考就能写出来的程度面试时才能留出脑力应对追问。希望这份整理能帮到正在准备铜九铁十的你JavaScript这块稳住面试就稳了一半。
返回列表