ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

前端校招笔试高频考点全解析:从异步原理到工程化实战

前端校招笔试高频考点全解析:从异步原理到工程化实战 “前端笔试”这四个字对于2020年那会儿的应届生来说基本等同于“八股文面试”的同义词。但其实回过头看像乐信这种体量、业务又偏金融科技的公司前端笔试题反而不是纯粹考背书的它更看重你能不能把手上的JS和CSS用明白能不能用工程化的思路去解决一个看似简单的页面问题。我花了点时间把这类校招笔试题的常见套路、出题逻辑以及背后真正想考察的能力点重新梳理了一遍结合我自己当年刷题和后来面人的经验写成这篇详细点的拆解。无论是正在准备校招的应届生还是想查漏补缺的初级前端希望这份复盘能帮到你。1. 笔试想考什么从乐信的招聘画像倒推考点很多同学准备笔试有个误区拿到题就刷刷完就忘完全没去想过“出题人到底想考察什么”。校招笔试题不像社招那样直接考察项目经验和解决复杂问题的能力它更像一个漏斗先筛掉基础不扎实、代码习惯差、思维模式有硬伤的人。乐信2020年前端笔试的题型分布其实很能反映当时主流互联网公司对校招生的期望画像。从整体结构来看这套笔试题可以拆成四大块计算机基础与网络、JavaScript语言特性与手写代码、浏览器与前端工程化、框架应用与场景设计。其中JavaScript和网络是占分最大的两块。为什么这两块占大头因为前端这个岗位往上走的瓶颈从来不是CSS写得多漂亮而是你对语言本身的理解深度以及对浏览器这个运行环境的掌控能力。尤其是JavaScript校招笔试题特别喜欢在“原型链”“异步”“闭包”“作用域”这几个经典考点上做文章。这些东西不是背下来就行而是要通过写代码的方式考察你能否跳出API层面看到语言设计背后的逻辑。再往深一层说乐信的业务是金融科技前端要处理大量表单、交易状态、数据可视化图表。所以笔试里一定会出现跟“异步流程控制”“数据不可变”“性能优化”“边界情况处理”相关的题目。这些考点不是单纯为了难为你而是为了模拟真实业务里可能出现的状态管理混乱、接口竞态、大数据量渲染卡顿等问题。还有个容易被忽略的点就是笔试题的“时间密度”。2020年这套题我记得总时长大概在90到120分钟之间题量却不小。如果你对某个知识点不熟在那儿卡了10分钟后面的题基本就写不完了。这就考察了一个很重要的能力在有限时间内如何合理分配精力先拿稳拿分、再攻难题。很多同学栽在笔试题上不是不会做而是时间没分配好前面一道手写题写了40分钟后面两道框架题直接放弃。所以准备这类笔试题我的建议是不要只刷题要“带题复习”。看到一道题先想它对应哪个知识模块再想这个模块在真实业务里解决什么问题最后才是写代码。这样才能把知识串成体系而不是碎片。2. JavaScript异步与手写代码最容易被拉开差距的板块JavaScript的异步和手写代码是前端笔试里拉开差距的关键。有的同学能拿满分有的同学看起来什么都懂一动手写就漏洞百出。这一章我挑几个乐信笔试里出现频率最高、也最容易出错的类型详细讲一下解题思路和踩坑点。2.1 事件循环与宏任务/微任务的执行顺序这几乎是前端笔试必考的一类题乐信2020年也不例外。题目通常给出一段包含setTimeout、Promise、async/await的代码让你写出输出顺序。这类题的核心考点就是浏览器的事件循环机制。你得彻底理解一个事实JavaScript是单线程的但浏览器不是。JS主线程执行代码时遇到异步任务不会傻等而是会把它们交给浏览器的其他线程去处理等结果准备好之后再把回调放进任务队列。主线程空闲了再从任务队列里取任务执行。任务队列又分宏任务macrotask和微任务microtask。宏任务setTimeout、setInterval、I/O、UI渲染、事件回调微任务Promise.then/catch/finally、MutationObserver、async/await后面的代码执行顺序的规则是先执行一个宏任务整个script其实也算一个宏任务执行完这个宏任务之后会清空当前所有的微任务。等微任务清空了如果需要渲染就渲染然后再从宏任务队列里取下一个宏任务执行。只看这句话可能还是会乱我拆个例子console.log(1); setTimeout(() { console.log(2); Promise.resolve().then(() { console.log(3); }); }, 0); Promise.resolve() .then(() { console.log(4); setTimeout(() { console.log(5); }, 0); }) .then(() { console.log(6); }); console.log(7); // 输出顺序是什么解这类题有个固定的思维路径第一步执行同步代码输出1和7。 第二步分析异步任务setTimeout回调进宏任务队列记为macro1Promise.resolve().then()进微任务队列记为micro1第三步同步代码执行完清空微任务队列先执行micro1输出4然后micro1的then链把6也加入微任务队列继续执行输出6。同时micro1内部又注册了一个setTimeout进宏任务队列记为macro2第四步微任务清空后取第一个宏任务macro1执行输出2它内部的Promise.resolve().then()创建了微任务micro2执行输出3第五步宏任务macro1执行完再清空微任务micro2已执行完然后取下一个宏任务macro2输出5最终输出顺序就是1, 7, 4, 6, 2, 3, 5。这里最容易错的地方是把micro1内部注册的setTimeout当成宏任务macro1之后的处理。实际上微任务执行是“全部清空”的你在微任务里往宏任务队列塞任务得等当前所有微任务清空之后才轮到它。笔试题里还有一个常见变体就是async/awaitasync function async1() { console.log(async1 start); await async2(); console.log(async1 end); } async function async2() { console.log(async2); } console.log(script start); async1(); console.log(script end);这里只要记住await的语义await后面的代码相当于被放到了.then()里也就是微任务。所以上面代码输出是script start async1 start async2 script end async1 endasync1 end放在最后因为await等待async2()执行完但async2()本身是同步执行完的执行完async2之后async1函数体剩余部分就被推迟到微任务里了。实战小贴士遇到async/await的题先把await翻译成Promise.then再执行就不容易错。2.2 手写Promise.all / 手写防抖节流手写题是笔试题里的重头戏。乐信2020年笔试里出现过手写Promise.all还有手写防抖函数。这类题看似简单但要在纸上写得又快又对还是需要平时练过。先看手写Promise.all。这道题的核心考点是你对Promise构造器、resolve/reject的调用时机、以及异步任务收集的理解。function myPromiseAll(promises) { return new Promise((resolve, reject) { if (!Array.isArray(promises)) { throw new TypeError(promises must be an array); } const results new Array(promises.length); let count 0; if (promises.length 0) { resolve(results); return; } promises.forEach((promise, index) { Promise.resolve(promise) .then((value) { results[index] value; count 1; if (count promises.length) { resolve(results); } }) .catch((err) { reject(err); }); }); }); }这里有三个容易踩的坑第一空数组的情况。Promise.all([])返回的应该是一个已经resolve的 Promise结果是空数组。很多人写的时候没做这个判断直接进去循环count 永远是0永远不会触发 resolve整个 Promise 就挂起了。第二Promise.resolve(promise)这一步很关键。因为传入的数组里可能混有普通值而不是 Promise 实例。Promise.all的特性是会把数组里的非 Promise 值直接当作已决议的值处理所以包一层Promise.resolve是最稳妥的做法。第三结果的顺序必须和输入顺序一致而不是完成的先后顺序。所以不能用results.push(value)而是要用results[index] value这种方式按索引赋值。如果你用 push一旦前面某个 Promise 卡住后面先完成的就会占错位置。再看手写防抖函数。防抖的核心思路是在事件被触发后延迟wait毫秒再执行回调如果在这段时间内又被触发就重新计时。典型的应用场景是搜索框输入、窗口 resize。function debounce(fn, wait 300, immediate false) { let timer null; let isInvoked false; return function (...args) { const context this; if (timer) { clearTimeout(timer); } if (immediate !isInvoked) { fn.apply(context, args); isInvoked true; return; } timer setTimeout(() { fn.apply(context, args); isInvoked false; timer null; }, wait); }; }这里有个容易被忽略的点this的绑定。如果你在debounce内部直接调用fn(...args)那么this就会指向undefined严格模式下或全局对象非严格模式下。所以必须用fn.apply(context, args)把原函数的this传进去。这是闭包加高阶函数题目里最常见的考点。至于节流throttle核心思路是控制执行频率保证在一段时间内至少执行一次。跟防抖的区别在于防抖是“最后一击”节流是“匀速释放”。笔试里这两者常被拿来对比考概念手写的话用时间戳版本比较不容易出错function throttle(fn, interval 300) { let lastTime 0; return function (...args) { const context this; const now Date.now(); if (now - lastTime interval) { fn.apply(context, args); lastTime now; } }; }时间戳版的缺点是第一次会立即执行最后一次触发不会执行因为间隔不够就丢掉了。面试时如果能答出这个缺点并补充一个定时器版本的实现会是个很好的加分点。2.3 深拷贝与类型判断手写深拷贝也是笔试常客。乐信2020年笔试里出现过一道拷对象数据、要求不改变源数据的题本质上就是在考察深拷贝。深拷贝的考点其实分三层第一层基础版本能处理对象和数组用递归实现第二层考虑循环引用用WeakMap或Map保存已拷贝的对象第三层考虑Date、RegExp、Function、Map、Set等特殊类型笔试一般不会要求非常完整的版本但至少要做到安全处理循环引用否则一遇到循环引用就爆栈。function deepClone(target, map new WeakMap()) { if (target null || typeof target ! object) { return target; } if (target instanceof Date) { return new Date(target); } if (target instanceof RegExp) { return new RegExp(target.source, target.flags); } if (map.has(target)) { return map.get(target); } const cloneTarget Array.isArray(target) ? [] : {}; map.set(target, cloneTarget); Reflect.ownKeys(target).forEach((key) { cloneTarget[key] deepClone(target[key], map); }); return cloneTarget; }这里用Reflect.ownKeys而不是Object.keys是为了连不可枚举属性和 Symbol 键也能拷贝到。但笔试如果用这个方法需要对Reflect比较熟悉所以平时得练过。关于类型判断有一个经久不衰的面试题typeof和Object.prototype.toString的区别。笔试里可能会给你一堆变量让你判断类型。typeof只有几个返回值undefined、boolean、string、number、bigint、symbol、object、function。它的局限是null返回object数组、对象、正则都返回object没法区分具体类型。最稳妥的方式是Object.prototype.toString.call(target)它返回类似[object Array]、[object Date]这样的字符串。这个方法基本能区分所有内置类型。笔试里如果需要精确判断类型建议封装一个getType函数function getType(target) { return Object.prototype.toString.call(target).slice(8, -1).toLowerCase(); }slice(8, -1)会去掉[object 前缀和最后的]得到Array、Date这样的字符串。这个函数在手写深拷贝的时候非常实用可以作为类型分支判断的依据。3. HTTP与浏览器缓存看似基础实则细节密集如果说 JavaScript 手写题是拉开分数差距的“主观题”那 HTTP 和浏览器相关的题就是决定你能不能过线的“客观题”。这部分分数拿起来相对容易因为它的考点非常固定。但失分点也很集中主要是概念混淆比如把缓存策略的Cache-Control和Expires搞混把ETag和Last-Modified的验证逻辑搞反。3.1 强缓存与协商缓存的完整链路关于浏览器缓存只要记住一句话强缓存是“不问你直接用”协商缓存是“问一下再用”。这个“问一下”就是发一次 HTTP 请求带上验证字段让服务器判断资源有没有变。强缓存相关的头部主要是Expires和Cache-Control。Expires是 HTTP/1.0 时代的产物是一个绝对时间。但因为服务器时间和客户端时间可能不一致所以现在基本不用了。Cache-Control是 HTTP/1.1 引入的用相对时间max-age来控制比如Cache-Control: max-age3600表示资源在3600秒内可以直接用缓存不用发请求。如果Cache-Control和Expires同时存在Cache-Control的优先级更高。协商缓存的核心机制是当强缓存过期后浏览器带着验证字段去问服务器资源有没有更新服务器根据字段判断Last-Modified/If-Modified-Since服务器返回资源最后修改的时间下次请求时浏览器把这个时间放到If-Modified-Since头里服务器比对。如果资源没变返回304 Not Modified浏览器继续用缓存。ETag/If-None-MatchETag是资源内容生成的唯一标识通常是对文件内容做哈希资源变了标识就变。浏览器下次请求时带上If-None-Match服务器比对。如果一致返回304。这两个字段有个优先级问题ETag的优先级高于Last-Modified。因为Last-Modified只能精确到秒如果同一秒内文件被修改了但没有内容变化或者修改后又改回去了Last-Modified就没法精确感知而ETag是内容哈希更准确。笔试题里常见的坑是让你设计一套缓存策略适合一个版本化资源比如app.js?v1.0.0和一个非版本化资源比如index.html。正确的做法是版本化资源使用强缓存设置超长的max-age比如一年。因为文件名带v1.0.0只要版本变了URL 就变了永远不会命中旧缓存。index.html使用协商缓存或者设置成no-cache。因为 HTML 是入口文件如果它被强缓存了用户可能永远拿不到最新的 JS/CSS 引用。所以 HTML 必须每次“问一下”服务器拿到最新的资源引用。这个思路在真实业务里非常重要。当年踩过的坑是上线后用户打开页面还是旧版本排查半天发现index.html被设置了Cache-Control: max-age86400浏览器走强缓存压根没有问服务器要新页面。3.2 从URL输入到页面展示中间发生了什么几乎是前端笔试必考的开放题。乐信2020年笔试也有一道类似的不过问得更具体从输入URL到页面展示中间经过了哪些步骤哪些步骤可能成为性能瓶颈这道题的标准链路是URL 解析判断是搜索词还是合法 URL补全协议头DNS 解析把域名解析成 IP。这里涉及浏览器 DNS 缓存、操作系统 DNS 缓存、本地 hosts 文件、LDNS 递归查询建立 TCP 连接三次握手确认双方收发能力正常发送 HTTP 请求浏览器发起请求可能带缓存验证走协商缓存服务器处理请求并返回涉及负载均衡、后端处理、资源组装浏览器接收响应处理状态码、响应头触发缓存策略解析 HTML 构建 DOM 树解析 CSS 构建 CSSOM 树渲染合并 DOM 和 CSSOM 成渲染树进行布局Layout和绘制Paint加载子资源遇到script、link、img等标签继续发起请求笔试答这道题尽量别只写骨架最好在几个关键节点加上一句“为什么”。比如在 DNS 解析那里说明为什么用 CDN 可以加速——因为 CDN 厂商会在 DNS 层面做智能解析让你就近访问节点在 TCP 连接那里说明为什么 HTTP/2 有优势——因为它可以在一个 TCP 连接上多路复用多个请求减少了多次连接的开销。乐信这道题还有个追问哪些步骤可能造成性能瓶颈这个可以从多个层面回答DNS 解析慢DNS 服务器响应慢、没开 DNS 预解析TCP 连接慢网络环境差、TCP 握手延迟HTTP/2 多路复用可以缓解服务器处理慢后端接口响应慢、没有做缓存资源下载慢资源体积大、没有开启 CDN、没有做压缩渲染阻塞render-blocking的 CSS/JS 太多、脚本过大导致主线程长时间占用这个问题答得好不好其实能看出你平时是真的关心过页面性能还是只在面试前背过一道题。3.3 跨域同源策略下的常见解决思路跨域是前端笔试里躲不开的考点。因为前后端分离架构下几乎每个项目都会遇到。先理解为什么会有跨域浏览器的同源策略协议、域名、端口三者一致才算同源限制了页面脚本不能访问不同源的资源。这个限制是浏览器实现的不是服务器实现的。所以跨域这个问题核心思路是“让服务器配合浏览器放行”或者“绕过浏览器的限制”。常见的解决方式CORS跨域资源共享最正统的方案。服务器在响应头里加Access-Control-Allow-Origin来允许指定域名访问。如果要带 cookie还得加Access-Control-Allow-Credentials: true并且不能把Allow-Origin设为*必须指定明确的域名。JSONP利用script标签不受同源策略限制这个特性动态创建script标签加载一个带回调参数的外部脚本。这是老方案只支持 GET 请求现在用得少了但笔试里经常考原理。Nginx 反向代理把前端请求代理到后端浏览器只跟同源的 Nginx 通信由 Nginx 转发请求。这是生产环境里最常用、最稳定的方案。WebSocket不受同源策略限制。postMessage用于 iframe 页面之间的跨域通信。笔试的经典题目是写一个 JSONP 的实现或者描述 JSONP 的执行流程。function jsonp(url, params, callback) { return new Promise((resolve, reject) { const script document.createElement(script); const random callback_${Date.now()}_${Math.random().toString(16).slice(2)}; window[random] function (data) { resolve(data); delete window[random]; script.remove(); }; const queryString Object.keys(params) .map((key) ${encodeURIComponent(key)}${encodeURIComponent(params[key])}) .join(); const callbackKey callback || callback; script.src ${url}?${queryString}${callbackKey}${random}; script.onerror function () { reject(new Error(JSONP request failed)); delete window[random]; script.remove(); }; document.body.appendChild(script); }); }这里有个很重要的细节回调函数名不能写死要用动态生成的随机名。因为如果两个 JSONP 请求同时进行都用了同一个回调名后面注册的会覆盖前面的导致第一个请求永远等不到回调。这是一个很经典的边界问题。4. 框架与组件设计从使用到原理的认知升级前端校招笔试的另一个重头戏是框架题。乐信2020年笔试的时候Vue 2.x 还是绝对的主流React 16.x 也有一批项目在用所以笔试题基本围绕这两个框架出。这部分题不像手写题那样需要写大量代码而是更侧重考察“你用了这个框架这么久有没有想过它底层是怎么工作的”。4.1 响应式原理Object.defineProperty 与 Proxy 的对比Vue 2.x 的响应式原理是 Object.definePropertyVue 3.x 换成了 Proxy。笔试题目通常会问Vue 2 的响应式原理是什么有哪些局限Vue 3 用 Proxy 解决了什么。Vue 2 的核心逻辑是在初始化时遍历 data 对象的每个属性用 Object.defineProperty 把它们全部转换成 getter/setter。当组件读取某个属性时触发 getter把当前 watcher 收集进依赖里依赖收集当属性被修改时触发 setter通知之前收集的 watcher 去更新派发更新。这个机制的局限在于新增属性不是响应式的。因为 Object.defineProperty 是在属性已经存在的情况下做拦截的你后来给对象新增一个属性它压根没有被 defineProperty 处理过所以修改它不会触发更新。Vue 2 的解决办法是Vue.set或this.$set来手动把新属性变成响应式。数组的某些操作无法被拦截。直接通过索引修改数组元素比如this.list[0] xxx是触发不了 setter 的。Vue 2 只好重写了数组的push、pop、shift、unshift、splice、sort、reverse这些方法让它们操作完之后再手动通知更新。需要递归遍历所有属性初始化性能有损耗。对象嵌套越深递归成本越高。Object.defineProperty 本身只能拦截属性不能拦截整个对象所以删除属性的操作也无法被感知。Vue 3 用 Proxy 重写了整个响应式系统。Proxy 可以代理整个对象不管是新增属性、删除属性、读取属性还是修改属性都能被拦截所以就不需要$set了。同时Vue 3 的响应式还做了懒收集不像 Vue 2 初始化时就把所有属性都递归遍历一遍而是等真正用到了才去收集依赖性能上也有提升。笔试答这道题建议画一个两列对比表格把“数据初始化时发生了什么”“新增属性时怎么处理”“数组变更时怎么处理”“性能表现”这几个维度列出来。这能有效展示你理解得够深。4.2 组件通信父子、兄弟、跨层级这题说白了就是考察你实际写项目时有没有遇到过组件间数据传递的场景。乐信笔试里出现过一道场景题有 A、B 两个兄弟组件用户点击 B 组件里的按钮需要让 A 组件里的数据发生变化问怎么实现。这道题没有唯一标准答案它考察的是你对多种通信方案的理解以及方案选型能力。优秀的回答应该先列出所有方案再说出你在什么场景下会用哪种方案。常见方案场景方案父传子props子传父$emit触发自定义事件兄弟组件通过共同的父组件中转子B$emit父组件监听父组件把数据通过 props 传给子A跨层级 / 任意组件Vuex状态管理库、Provide/Inject、EventBusReact 场景Context、Redux/Zustand回答这题时可以顺便讲讲 Vuex 的架构和核心概念。state是全局状态数据getters类似 Vue 实例里的 computed用于派生状态mutations是唯一能修改 state 的地方而且是同步的actions里可以写异步逻辑可以提交 mutation。为什么 mutation 必须是同步的因为 Vuex 的 devtools 要记录状态变化的快照如果 mutation 里混了异步操作时间线就没法追踪了调试起来会很痛苦。4.3 生命周期从创建到销毁的关键时刻生命周期是框架题的常客出题方式通常是给一段代码问在哪个生命周期里发请求、在哪个生命周期里销毁定时器、在哪个生命周期里访问 DOM。Vue 2 的生命周期可以归纳为四个阶段创建阶段beforeCreate实例初始化之前此时拿不到 data 和 methods、created实例创建完成data 和 methods 可用但 DOM 还没挂载挂载阶段beforeMount模板编译完成但还没有插入真实 DOM、mountedDOM 挂载完成可以访问this.$el更新阶段beforeUpdate数据变化后DOM 更新前、updatedDOM 更新完成销毁阶段beforeDestroy实例销毁前定时器、事件监听还活着适合清理、destroyed实例销毁后答生命周期题建议记住一个核心结论发起数据请求放在created或mounted都行但放在created更好因为created阶段在挂载之前数据如果提前拿到可以避免 DOM 渲染完再等数据造成的不必要闪烁。定时器和全局事件监听一定在beforeDestroy里清理。乐信笔试这道题的陷阱是问“在created里能不能访问 DOM”。答案是不能。因为created阶段实例刚创建模板还没编译DOM 还没存在访问this.$el是undefined。如果你需要在初始化阶段操作 DOM比如让某个元素获得焦点那就得放在mounted或$nextTick回调里。5. 版本控制与工程化基础考察你的职业素养前几年校招笔试里版本控制Git和工程化相关的题占比不高但偶尔会出现在附加题或简答题里。乐信2020年的笔试就有一道 Git 操作场景题。这类题你说它难它真的不难但如果你是第一次见可能连命令都写不完整。5.1 Git 协作场景冲突解决与分支管理常见笔试题目是你正在一个分支上开发功能另一个同事改了同一个文件的同一个地方你把他的分支合并到你的分支时发生了冲突怎么解决正确的操作路径先提交你当前分支的工作切换到目标分支比如 main/master拉取最新代码切回你的开发分支执行git merge main或git rebase mainGit 提示冲突CONFLICT打开冲突文件你会看到类似这样内容 HEAD 你的代码 对方的代码 feature/xxx手动编辑文件删掉、、这些标记决定保留哪部分代码或都保留保存后执行git add把文件加入暂存区执行git commit完成冲突解决。这里要补充一个概念对比merge和rebase的区别。merge会保留完整的合并历史生成一个新的 merge commit分支图会分叉rebase是把你的提交“变基”到目标分支之后重新排列提交历史是一条直线更干净。笔试如果问“你们团队用 merge 还是 rebase”比较好的回答是团队协作中尤其是公共分支尽量用 merge 来保留历史个人开发分支可以用 rebase 整理提交记录让历史更线性。5.2 Webpack 核心概念loader、plugin、性能优化乐信这类偏工程化的公司笔试里很可能会考 Webpack 的基本概念。核心考点就三个loader 是什么、plugin 是什么、两者区别在哪里。简单来说loader 是“转换器”它把 Webpack 不认识的文件转换成 JS 模块。比如 babel-loader 把 ES6 语法编译成 ES5css-loader 处理 CSS 文件里的import和url()style-loader 把 CSS 代码注入到style标签里。plugin 是“扩展器”它可以玩更高级的把控。从打包优化到文件处理到环境变量注入都能通过 plugin 实现。常见的有HtmlWebpackPlugin自动生成 HTML 并注入打包后的脚本、MiniCssExtractPlugin把 CSS 提取成独立文件、TerserPlugin压缩 JS。loader 和 plugin 最常见的混淆点很多人分不清“转换某个文件类型”到底应该用 loader 还是 plugin。记住一句话loader 只做文件级转换plugin 做构建流程级别的控制。举个直观例子处理.vue文件用的是vue-loader它把单文件组件拆成模板、脚本、样式三部分再分别处理而你想要在打包结束之后自动上传产物到 CDN那就是UploadPlugin自定义 plugin的事了。如果笔试再加问 Webpack 性能优化可以从两个维度答打包体积开启 tree-shaking去除没用到的模块代码分割splitChunks把公共依赖抽出来按需加载路由懒加载。构建速度用cache-loader或 Webpack 自带的持久化缓存用thread-loader开启多进程构建减少 loader 的include范围别让node_modules里的文件也被 Babel 去处理。5.3 一个简单的笔试场景题如何设计一个可复用的弹窗组件这类题在笔试题里被归为“设计题”不写完整代码只考设计思路。出题意图非常明确看你会不会抽象组件、会不会考虑公共逻辑、会不会预留扩展点。一个合格的弹窗组件应该至少包含以下设计点对外暴露的 API通过 props 控制弹窗标题、宽度、内容、是否显示底部按钮通过事件如visible-change通知外部弹窗关闭。控制显隐外部通过v-if或visible属性控制组件内部在点击遮罩层、点击关闭按钮时触发关闭事件。插槽slot设计支持默认插槽自定义内容区域支持具名插槽如 footer自定义按钮区域。功能细节点击遮罩层是否关闭需可配置close-on-click-overlay是否显示右上角关闭按钮需可配置是否可拖拽需可配置组件卸载时是否重置数据。层级管理弹窗的z-index应该是动态递增的防止多个弹窗互相遮挡。一般会有一个全局变量记录当前最大z-index每次打开弹窗时加1。定位与布局弹窗居中遮罩层覆盖全屏滚动锁死打开弹窗时禁止 body 滚动。回答设计题时最好口述一个“从使用到实现”的完整链路最外层是一个遮罩层position: fixed覆盖全屏半透明背景中间是弹窗主体position: fixedleft: 50%; top: 50%; transform: translate(-50%, -50%)实现居中通过 TeleportVue 3或 Portal 把弹窗挂载到body下避免被父组件的overflow: hidden裁掉。能讲到 Teleport/Portal 这层基本就比大多数候选人高出一个段位了。因为这证明你不仅知道怎么用组件还知道组件在 DOM 结构中的位置会影响样式表现。6. 从笔试到实战那些刷题之外更重要的功夫讲完这几类典型题型我知道很多读者更关心的是我到底该怎么准备才能在有限时间里最高效地通过笔试我自己的经验是笔试前一天不要把大量时间花在刷“新题”上而是做三件效率最高的事。第一把高频手写题默写一遍。防抖、节流、深拷贝、Promise.all、数组去重、数组扁平化、call/apply/bind 的实现。这些题是有标准模板的你只要平时默写过三遍以上考场上基本是肌肉记忆几分钟就能写完还能腾出时间给后面的难题。第二把框架的核心原理用自己的话说一遍。Vue 响应式、生命周期、组件通信、虚拟 DOMReact 的函数组件渲染流程、hooks 的原理。能用自己的话说清楚说明你是真理解而不是背的八股文。笔试简答题最忌讳的就是把网上的原话抄上去阅卷人一眼就能看出你是背的还容易因为表述不严谨被扣分。第三看看你们学校或目标公司往年的笔试真题搞清楚出题风格。有些公司偏爱手写题有些公司偏爱基础选择题有些公司会加一道逻辑题或场景设计题。投了不同公司策略完全不同。这里还想多说几句关于“刷题心态”的问题。我发现很多应届生准备笔试时容易陷入一种焦虑觉得题量巨大、知识无边似乎永远准备不完。实际上校招笔试是个合格性考试不是选拔性考试它不是要你考满分而是要看你在重点模块上有没有达到一个“能干活”的最低标准。所以与其在所有角落撒网不如把最高频的模块吃透。就拿乐信这套题来说JavaScript 基础、手写代码、浏览器缓存、跨域、Vue 原理、Git 常规操作这几块占了 80% 的分数。把这些搞定笔试过关基本没有问题。还有一点做题时一定要先易后难。开考后先花 5 分钟把整张卷子扫一遍标记出哪些题是送分题哪些题是模棱两可的哪些题是完全没思路的。先把送分题写完再做中等难度的最后有时间再攻克难题。很多同学喜欢按顺序做结果手写题卡了半小时后面的简答题没时间写白白丢分。最后聊一个隐藏技能笔试里的代码书写规范。手写题虽然不要求真的跑起来但阅卷人会看你的代码习惯。变量命名是不是有语义有没有处理边界情况格式是不是整洁这些都会影响印象分。我见过有些同学手写代码时连分号都不写函数名用a、b、fn代替虽然逻辑可能是对的但给阅卷人的感觉很不好。笔试不仅仅是考你会不会也是在模拟你未来在工作里写代码的样子。把一个数组去重写成两行简洁、有注释的代码和写十行逻辑混乱的代码最后得分差距会很大。我自己面过不少校招生刷掉的人不一定是技术最差的但大概率是代码习惯最差的。所以从现在开始每次在纸上或白板上写代码都把它当成一次正式的代码审查注意格式、命名、边界条件。这个习惯养成了不管什么笔试都会受益。准备笔试的过程其实是很枯燥的但也是提升最快的阶段。等你真正把原型链、事件循环、响应式原理这些硬骨头啃下来再回头看之前觉得难的项目会发现脑海里的图景完全不一样。技术这条路有时候就是这样你以为是应付考试实际上是在给自己打更深的地基。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进