尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

90分钟Web前端春招笔试复盘:从事件循环到深拷贝的考点全解析

90分钟Web前端春招笔试复盘:从事件循环到深拷贝的考点全解析 春招进行到三月中旬我的web前端简历已经投出去十几份多数石沉大海。那天下午收到小满的笔试邀请邮件标题写着第二批春招Web前端岗那一刻先松了一口气——能进入第二批笔试至少说明简历筛选过了。当时我其实还不太清楚第二批意味着什么以为是补录后来才发现它只是春招的批次安排考核范围和第一批同属一个题库池只是抽取的题目不同。这场时长90分钟的笔试几乎把我自认为扎实的web前端开发能力从上到下扒了一遍。今天把完整复盘写出来既是为了给正在准备web前端开发春招的朋友提供一个具体样本也是给自己留一份技术总结。1. 考前准备我踩了一个很多人都会踩的复习顺序坑1.1 笔试邀请邮件里最容易忽略的规则笔试前一天晚上收到邮件时我其实已经连续投了两周简历每天刷面经刷到凌晨整个人处于一种好像什么都会、又好像什么都不会的混沌状态。邮件里的关键信息很明确考试时间90分钟需要开摄像头浏览器推荐Chrome不能切屏超过三次否则系统会标记作弊。我当时只盯着考试时间看了把不能切屏这条规则忽略了。开考后我才意识到这条规则有多要命。笔试的时候题目里有一道关于CSS Grid的题我记得不太清楚第一反应就是切出去查一下。但一想到切屏三次就会被标记整个人瞬间老实了只能靠自己的记忆硬写。这里提醒后来人一句凡是要求开摄像头的在线笔试基本都带切屏监测别指望考试时临时查资料。与其把希望寄托在搜答案上不如考前把基础知识点过一遍。1.2 我把80%时间浪费在看上而不是写上几乎所有准备过前端笔试的人都会犯同一个错觉得自己会了但一上手写就卡壳。我考前那晚的复习安排是80%时间在看八股文比如TCP握手几次、HTTP状态码有哪些、Vue的响应式原理是什么剩下20%时间用来默写代码。这个比例放在现在回头看完全是反的。那场笔试的实际题目告诉我选择题里确实有八股但占比不高而且考得相当偏反倒是手写代码题分值最大、最拉分。你要是能把防抖节流、深拷贝、Promise.all、数组转树这几类手写题写到闭眼都能默出来的程度笔试基本就稳了一半。而看出来的知识在考场上很容易被紧张情绪冲散。2. 试卷结构的大致画像90分钟要回答哪些东西2.1 题型分布和分值的直观感受打开试卷之后我的第一反应是题量不算大一共四大题包含若干小题总分100分。题目类型分布大概是下面这个感觉题型题量估分占比建议时间单选题8题约20分15分钟多选题4题约16分10分钟手写编程题3题约36分35分钟简答题2题约28分20分钟这里我用的词是大概因为小满的笔试页面并不会在开头就告诉你每道题的具体分值你只能从题目的难易程度猜。单选题和多选题占了大概36分剩下的64分基本压在手写题和简答题上。这个分值比例给我的直接信号是这家公司招前端不太想招背题家更想招能写代码的人。2.2 我定下的答题顺序以及后来为什么被打乱我的原定顺序是从前往后做先选择题再多选题再手写题最后简答题。为什么会定这个顺序因为选择题和判断题通常可以快速拿分先把能拿的分装进兜里心里比较有底。而且选择题里可能会有一些考点能提醒后面的手写题怎么答比如选择题考了事件循环可能后面就会考一道Promise相关的手写题。但这个秩序在执行到中途就崩了。原因很简单我在多选题上卡得比想象中久。有几道多选题设置得非常刁钻每个选项看起来都对但正确答案偏偏是ABD而不是ABCD。我在那几道题上磨了将近20分钟导致后面手写题的时间被挤掉不少。后来交卷那一刻我复盘了一下如果当时果断跳过拿不准的多选题把时间优先给手写题和简答题分数可能会高出5到8分。3. 选择题复盘四道很容易翻车的题也是最常见的考点3.1 事件循环输出顺序async/await的微任务排队规则单选题里有一道让我印象深刻的代码输出题题目大致是这样async function async1() { console.log(async1 start); await async2(); console.log(async1 end); } async function async2() { console.log(async2); } console.log(script start); setTimeout(() { console.log(setTimeout); }, 0); async1(); new Promise((resolve) { console.log(promise1); resolve(); }).then(() { console.log(promise2); }); console.log(script end);这道题的输出顺序是script start、async1 start、async2、promise1、script end、async1 end、promise2、setTimeout。关键在于await async2()后面的代码并不会立刻执行而是被包装成一个微任务排在当前宏任务之后。而且微任务队列遵循先进先出async1 end比promise2更早进入队列所以先输出。我为什么觉得这题容易翻车因为平时在浏览器控制台里跑的和在答卷上写的心理状态完全不一样。如果对微任务排队顺序的理解只停留在promise.then比setTimeout先执行这个粗糙层面看到await就会懵。建议准备笔试的朋友把await之后的那段代码理解成then回调的语法糖这样推演输出顺序时会更顺手。3.2 严格模式下this的指向一个被低估的高频坑多选题里有一道关于this指向的问题选项设计得很有迷惑性var name global; const obj { name: obj, fn: function() { console.log(this.name); }, arrow: () { console.log(this.name); } }; const f obj.fn; f(); obj.fn(); obj.arrow();题目问的是在非严格模式下以上三个调用的输出分别是什么。这个问题本身不难答案是global、obj、global。但问题设置了一个陷阱它要求你判断如果代码加了use strict结果会变成什么。在严格模式下f()里的this是undefined所以访问this.name会直接抛TypeError而不是输出global。我当时在这道题上犹豫了很久。因为很多前端学习资料在讲this时默认都在非严格模式下展开很少有人强调严格模式带来的差异。这个点提醒我面试官考this往往不是单纯考指向而是考边界条件也就是你在实际项目中可能遇到的异常情况。3.3 flex item里的min-width: auto一个非常隐蔽的布局坑另一道选择题直接给了一段CSSdiv classcontainer div classitem这是一段很长的文本内容它会不会把容器撑破/div div classitemitem2/div /div.container { display: flex; width: 300px; } .item { flex: 1 1 50%; }题目问两个.item的实际布局表现是什么答案的关键在于flex item默认的min-width是auto也就是说无论你设置flex: 1 1 50%item都不会缩小到小于它内容的最窄宽度。如果第一个item里面有一段很长的连续英文或数字它会把整个flex容器撑破第二个item被挤到几乎看不见。解决办法是给.item加min-width: 0。这个知识点如果你没在真实项目里遇到过光靠背八股很难想起来。很多组件库的grid布局出现横向溢出排查到最后都是这个原因。这道题之所以被我记下来是因为它属于看起来简单但杀伤力极大的题。3.4 协商缓存里ETag和Last-Modified同时存在听谁的单选题里有一道跟HTTP缓存相关的题当响应头里同时返回ETag和Last-Modified浏览器重新请求时请求头里会带什么正确做法是浏览器会带If-None-Match其值来自上一次响应里的ETag。服务器收到后优先比较If-None-Match这个字段匹配不上就直接返回200和新的资源只有匹配上才会去看If-Modified-Since。也就是说ETag的优先级高于Last-Modified。这题的坑在于很多人只记得浏览器会发送If-Modified-Since却忽略了ETag的优先级。我在项目里排查过两次静态资源更新不生效的问题最后都是因为CDN和Nginx的缓存策略里只配了Last-Modified导致文件内容变了但时间戳没变浏览器一直拿旧资源。如果在笔试里遇到这类题别只选带If-Modified-Since要把If-None-Match也考虑进去。4. 手写编程题逐题复盘三道题三种不同的体验4.1 第一道手写题带并发上限的异步请求调度器题目描述大概是有一个数组里面每一项都是一个返回Promise的函数现在要求写一个run(list, limit)方法让这些异步任务最多同时执行limit个全部执行完后返回每个任务的结果数组。我当时的思路是维护一个正在执行的任务池数量不超过limit。每当一个任务完成就立刻从任务列表里取下一个任务补上。实现如下async function run(tasks, limit) { const results new Array(tasks.length); let index 0; const workers []; const execute async () { while (index tasks.length) { const current index; try { results[current] await tasks[current](); } catch (e) { results[current] undefined; } } }; const count Math.min(limit, tasks.length); for (let i 0; i count; i) { workers.push(execute()); } await Promise.all(workers); return results; }这里有个细节很容易写错如果用for循环去主动管理任务池容易在任务完成回调和补充新任务之间出现混乱。换成每个worker一直从共享的index里取任务这种写法逻辑会清晰很多也不容易出现并发下的竞态问题。我交完卷后回忆觉得还有更好的写法比如用Promise.race动态监听任务池中最早结束的任务。但那需要额外维护一个数组在笔试的紧张环境下反而不如上面这个Promise.all加worker的方案稳妥。如果你的目标是拿分而不是炫技这种写法足够。4.2 第二道手写题扁平数组转树结构第二道题给了一个数组每个元素长这样{ id, parentId, name }要求转换成嵌套的树结构。这个题型在前端笔试里出现频率极高我见到的版本至少有三种一种是纯递归时间复杂度偏高一种是利用Map先存节点再二次遍历第三种就是拆成先建映射再拼父子关系。我采用的是Map加两次遍历的做法function arrayToTree(items) { const map new Map(); const roots []; items.forEach((item) { map.set(item.id, { ...item, children: [] }); }); items.forEach((item) { const node map.get(item.id); if (item.parentId ! null map.has(item.parentId)) { map.get(item.parentId).children.push(node); } else { roots.push(node); } }); return roots; }之所以用两次遍历是因为题目里没有保证父节点一定在子节点前面出现。如果只做一次遍历遇到一个子节点时它的父节点可能还没放进Map那就只能挂在临时数组里逻辑会更复杂。用Map先把每个节点的引用存下来再统一挂靠就不会有顺序依赖时间复杂度是O(n)比递归的O(n^2)要稳。这道题真正容易丢分的地方是边界条件如果parentId指向一个不存在的节点怎么办如果数组为空怎么办我在答题时把roots数组兜住了空数组的情况但parentId指向不存在的节点时我只是简单地把这个节点当成根节点处理了。严格来说可能跟出题人预期不一致但这种边界处理至少证明你考虑过异常场景比什么都不写强。4.3 第三道手写题深拷贝循环引用是最隐蔽的坑第三道题是手写深拷贝。这道题我本以为最简单结果反而让我最紧张原因是出题人在描述里加了一句要处理循环引用。常规深拷贝写法无非就是递归遍历对象typeof判断类型但一旦涉及循环引用比如obj.self obj不加缓存的话就会无限递归把调用栈撑爆。我当时写的是function deepClone(value, cache new Map()) { if (value null || typeof value ! object) { return value; } if (cache.has(value)) { return cache.get(value); } if (value instanceof Date) { return new Date(value); } if (value instanceof RegExp) { return new RegExp(value.source, value.flags); } if (Array.isArray(value)) { const arrCopy []; cache.set(value, arrCopy); value.forEach((item, index) { arrCopy[index] deepClone(item, cache); }); return arrCopy; } const objCopy {}; cache.set(value, objCopy); Object.keys(value).forEach((key) { objCopy[key] deepClone(value[key], cache); }); return objCopy; }这里最核心的cache参数就是用Map把原始对象和拷贝对象的对应关系存下来。当递归遇到同一个对象时直接从Map里取出之前创建的拷贝对象从而打破循环。我当时在Array.isArray的分支里忘了先cache.set后来检查时发现的赶紧补上。如果你在考场上也遇到这题一定要记住只要走了typeof value object分支就要在创建拷贝对象后立刻cache.set否则后面递归到循环引用时会出事。深拷贝还有个扩展点是函数和Symbol。函数通常直接复用引用不拷贝Symbol作为key时Object.keys拿不到需要换成Reflect.ownKeys。我答题时没有写完这个扩展但简答题里我提到了一句完整实现需要考虑Reflect.ownKeys和Symbol不知道能不能多拿一点印象分。5. 简答题性能优化题怎么答才能让面试官觉得你真有实战经验5.1 我给出的分层答案从指标、工具到手段简答题有两道一道是如果线上页面加载很慢你会怎么排查和优化另一道是如何理解前端工程化。第二道我答得比较中规中矩说一下模块化、组件化、构建工具、CI/CD这些不再展开。第一道才是真正拉分的题我当时的答案分了三层。第一层先定位问题指标。我会说打开DevTools的Performance面板录制一次页面加载过程用Lighthouse跑一个总分重点看FCP、LCP、INP、CLS这几项指标。如果LCP超过2.5秒基本能判断瓶颈在资源加载如果CLS比较大说明页面视觉稳定性有问题。第二层按资源链路排查。先看HTML是否被缓存、是否有多余的重定向再看JS和CSS体积有没有做代码分割再看图片是不是WebP或者AVIF格式有没有用懒加载最后看服务器有没有开Gzip或Brotli压缩。如果项目用的是Webpack就检查有没有开启SplitChunksPlugin把第三方库和业务代码拆开。第三层给出优化措施和验证结果。比如将首屏非必需的组件改为动态importLCP从2.8秒降到1.9秒这种表达。这一层特别重要因为它让面试官看到你有量化的概念而不是只会背概念。5.2 为什么先量化再做优化在笔试里更吃香很多人在答性能优化题时会直接写一堆手段比如CDN、缓存、懒加载、SSR听起来都对但缺乏抓重点的思路。我后来复盘时觉得笔试里的性能优化题考官真正想看的不是你能罗列多少方法而是你拿到一个模糊问题时会不会先缩小范围。先量化这个思路放在答案的开头会给阅卷人一种这个人真的有线上排查经验的印象。我答那题时用的句式是先跑Lighthouse看指标再进Performance面板定位长任务其次按网络加载阶段逐个排查最后给出措施和前后对比。这样的结构天然就是一篇小项目复盘比单纯堆概念要立体得多。如果你准备面试强烈建议自己找几个真实项目跑一遍性能分析哪怕只是把一张大图的体积降下来都能成为很好的回答素材。6. 交卷后的24小时复盘优先级比焦虑更重要6.1 我预估最可能丢分的三个位置交卷那一刻我第一感觉是完了多选题肯定错了不少但冷静下来后我用十分钟快速做了一个自我评估。最可能丢分的三个位置我按风险从高到低排了一下。第一是简答题的第二道关于工程化的理解我写得太泛没有结合实际项目经验。第二是深拷贝那道题虽然核心逻辑对了但在Symbol和非枚举属性上处理得不够完整。第三是多选题里的CSS Grid题考了一个比较少见的grid-template-areas命名方式我填的答案比较犹豫。我当时还列了一个简单的预估得分表题型预估得分区间主要失分点选择题24~32分多选选项纠结有漏选风险手写题26~32分深拷贝边界处理不完整简答题22~26分工程化题太泛不够落地把得分预期写下来之后我反而没那么焦虑了。因为我能清楚看到自己扣分的大方向这也直接指导了接下来两周的复习重点。6.2 接下来两周的复习方向调整笔试后的第三天我调整了复习计划把我之前看的部分全部砍掉改成默写模式。每天早上起来先花半小时默写一份手写题包括防抖节流、深拷贝、Promise.all、数组去重、数组转树、发布订阅、防重复请求等。默完再对答案错了就重写一遍。另一个明显的调整是多选题的练习。以前我刷题总喜欢只选一个最有把握的选项但笔试里多选题是漏选给部分分还是零分不同公司规则不一样小满这套题是漏选也给一部分分所以多选策略反而应该先选绝对有把握的再决定要不要冒险。我后来练题时专门做了几套多选题组的模拟专门练判断哪些选项我真的能确定。这两周里我还干了一件事就是把之前项目里的一个组件库的滚动性能问题重新排查了一遍用Performance面板记录了优化前后的对比数据。这个案例后来在面试环节帮了我大忙因为面试官问性能优化时我直接拿这个真实案例说了十分钟。最后再分享一个小经验如果让我回到那场笔试前我最想告诉自己的一句话是别把所有希望寄托在临场发挥上。在线笔试的题目难度其实往往低于你的心理预期但就是在基础题上最容易翻车。尤其要注意那些平时看代码觉得没什么难的的题比如事件循环、this指向、flex的最小宽度它们才是真正筛人的题。那次笔试最后没有给我offer但我在复盘笔记里写了一段话一次笔试能够逼你重新梳理前端知识体系本身就是一笔很划算的买卖。后来我靠那两周整理的手写模板和性能优化案例拿到了另一家公司的offer。现在把这些内容写出来也是希望准备web前端开发春招的朋友少走一点弯路。祝你们都能拿到想要的offer。
返回列表