有赞前端笔试复盘:手写代码、JS基础与综合题全解析

📅 发布时间:2026/8/28 19:27:37
有赞前端笔试复盘:手写代码、JS基础与综合题全解析 2019年秋招那会儿我印象最深的笔试之一就是有赞的前端卷。有赞当时已经是做零售SaaS的明星公司技术氛围在杭州校招圈里口碑不错笔试题目流传度也高。我参加的是第二批整体感觉是题目不算偏但很考验基础功底的厚度尤其是手写代码的部分直接朝你平时写代码的“舒适区”开刀。这篇博文就基于我回忆整理的题目和答题思路展开重点讲清楚每类题到底在考什么、怎么答才能稳拿分以及我在真实笔试中踩过和复盘出的坑给接下来准备前端校招的同学一份能直接照着复习的参考。不管你是在准备2026年的春招秋招还是刚入坑前端不久想检验自己的水平这篇内容都可以帮你在知识梳理和应试策略上获得一些思路。前端校招笔试的底层逻辑这些年变化其实不大——JS基础、手写代码、网络与浏览器、CSS布局、算法与场景设计万变不离其宗有赞只是其中一道比较有代表性的考题。1. 笔试的整体设计与考察逻辑1.1 从题目分布反推前端团队的用人标准先把印象里的题目分布拉一张表出来大家心里有个底。有赞笔试第二批满分100分时间90分钟总共大概七八道题有一些是空栏填补有一些是要完整实现一个函数。它的题型分布大概是这样的考察模块题量占比代表题型深层目标JavaScript 基础约30%类型判断、闭包、this、事件循环检测语言底层理解是否扎实手写代码约30%防抖节流、Promise、数组去重、深拷贝检测日常编码习惯和边界意识网络与浏览器约15%缓存策略、URL输入过程、跨域检测前端工程中的基础素养CSS 与布局约10%三栏布局、垂直居中、移动端适配检测页面还原和布局功底算法与数据结构约15%数组/字符串/链表题检测逻辑推导和编码严谨性这个分布透露出的用人标准远比“你会不会写页面”要高级得多。有赞的业务是零售SaaS前端要处理的是高复杂度管理后台、多端商城、数据看板这类场景所以特别看重异步编程能力、状态管理意识和性能敏感度。笔试里反复出现的手写Promise、防抖节流本质就是在检验你平时写业务代码时有没有思考过“为什么这样封装”而不只是会用lodash。再说直白一点有赞的笔试不考死记硬背的API考的是“在真实业务里遇到问题你能不能给出可靠的实现”。这跟很多大厂笔试强调“边界条件、异常处理、代码可读性”是一个思路。你写的代码不是给机器看的是给阅卷的前端工程师看的他们会从你的代码风格里判断你有没有工程素养。1.2 2019年这个时间节点的特殊考点2019年的前端技术栈有一个明显特征Vue 2.x已经非常成熟React 16.8刚刚发布Hooks面试官开始对Hooks、Fiber架构这些新东西感兴趣但笔试题仍然以ES6、浏览器原理、网络基础这些“老八股”为主。不要小看这些老考点它们恰恰是把“会用框架的人”和“理解前端的人”区分开的关键。我在那场笔试里遇到的第一道题就是“用原生JS实现数组去重要求区分1和1且保留首次出现顺序”。这道题放在今天依然高频因为它能同时考察几个点对Set的理解、对类型强弱的敏感度、对数组方法熟练度以及写代码时有没有考虑扩展性。类似的题目还有“手写一个深拷贝”“手写防抖节流”等本质上都是用一个很小的功能点撬动你对JavaScript语言特性的整体理解。所以复习2019年这批笔试题目不能只盯着“有赞的答案”背而是要把每道题背后的知识网展开。比如数组去重背后牵扯到的是数据类型、迭代器、对象引用比较、时间复杂度防抖节流背后是事件循环、闭包、参数透传和定时器边界。把这些网络打通了不管题目怎么变你都能应对。2. 手写代码题的命题套路与拆解思路2.1 数组去重一个看似简单却能拉开差距的题数组去重差不多是前端笔试里出现频率最高的手写题了但不同写法之间差距非常大。我先把几个层次的答案写出来大家自己对号入座。第一层用Set一行代码搞定const unique (arr) [...new Set(arr)];这个答案不能算错但在阅卷人眼里只能拿基础分因为它没有处理以下几个问题NaN与NaN在Set中被认为是相同的实际上NaN ! NaN但Set会去重、对象引用类型无法按内容去重、1和1这种类型不同的问题在某些要求下需要区分。如果题目明确要求“类型不同不视为重复”那Set方案直接不符合要求。第二层用reduce加Map做到类型严格区分const unique (arr) { const map new Map(); return arr.filter((item) { const key typeof item | item; if (!map.has(key)) { map.set(key, true); return true; } return false; }); };这个版本用typeof item | item构造唯一key能区分1和1也能处理NaN因为typeof NaN numberkey是number|NaN整体思路是对的。但有一个隐藏问题如果元素是对象typeof都是object拼接字符串后不管对象内容是否相同都会被认为是不同的因为对象到字符串会走toString()或valueOf()这里存在风险。第三层支持数组内对象按浅引用或深内容去重这种扩展性容易让阅卷人眼前一亮const isObject (val) val ! null typeof val object; const unique (arr, deep false) { const seen []; const isDuplicate (item) seen.some((prev) deep ? JSON.stringify(prev) JSON.stringify(item) : prev item ); return arr.filter((item) { const duplicated isDuplicate(item); if (!duplicated) { seen.push(item); } return !duplicated; }); };这里的JSON.stringify做深度比较只适用于可序列化对象局限性明显但作为笔试答题能主动考虑“如果需要深比较怎么做”已经能体现你的思维深度。我的建议是考试时先写清楚基础方案再用注释或补充代码说明扩展方案千万不要在基础实现上翻车。有一个答题技巧非常实用遇到这类手写题先明确题目要求中的边界条件再动手。常见的边界有空数组、包含null/undefined/NaN、嵌套对象循环引用一般笔试不会要求。你可以在代码顶部用注释写出你假设的边界这样即使代码有瑕疵阅卷人也能看到你考虑问题的方式。2.2 手写Promise链式调用的背后是状态机有赞第二批笔试里有一道“手写一个简易版Promise要求支持then和catch”这道题我相信很多参与者当时都写得比较挣扎。因为平时用Promise写业务代码很顺但一旦要求自己实现就需要对状态机、回调队列、微任务执行时机都有清晰认知。先给一个能跑通基本链式调用的简化版实现框架class MyPromise { constructor(executor) { this.state pending; this.value undefined; this.reason undefined; this.onFulfilledCallbacks []; this.onRejectedCallbacks []; const resolve (value) { if (this.state ! pending) return; this.state fulfilled; this.value value; this.onFulfilledCallbacks.forEach((fn) fn()); }; const reject (reason) { if (this.state ! pending) return; this.state rejected; this.reason reason; this.onRejectedCallbacks.forEach((fn) fn()); }; try { executor(resolve, reject); } catch (err) { reject(err); } } then(onFulfilled, onRejected) { // 返回一个新的Promise支持链式调用 const promise2 new MyPromise((resolve, reject) { const handleFulfilled () { try { const result onFulfilled ? onFulfilled(this.value) : this.value; resolve(result); } catch (err) { reject(err); } }; const handleRejected () { try { const result onRejected ? onRejected(this.reason) : this.reason; resolve(result); } catch (err) { reject(err); } }; if (this.state fulfilled) { setTimeout(handleFulfilled, 0); } else if (this.state rejected) { setTimeout(handleRejected, 0); } else { this.onFulfilledCallbacks.push(handleFulfilled); this.onRejectedCallbacks.push(handleRejected); } }); return promise2; } catch(onRejected) { return this.then(null, onRejected); } }这里的核心点有几个状态只能从pending变为fulfilled或rejected且一旦变化不可逆then必须返回一个新的Promise才能实现链式调用回调的收集和触发要放在数组里因为同一个Promise可能被多次then执行器executor抛出的异常要被catch到并转为reject在异步场景下回调要在状态变化后再执行这个简化版用setTimeout模拟了异步在真实实现里应该用queueMicrotask或Promise.resolve().then()来保证微任务语义。笔试考手写Promise往往不会要求完整实现Promise.resolve、Promise.all、finally这些静态方法但如果你能顺手写出MyPromise.all会是明显的加分项MyPromise.all (promises) { return new MyPromise((resolve, reject) { const results []; let count 0; for (let i 0; i promises.length; i) { MyPromise.resolve(promises[i]).then( (value) { results[i] value; count; if (count promises.length) { resolve(results); } }, reject ); } }); };我个人在实际复习时的心得是不要死记硬背代码而是先画一遍状态流转图搞清楚“为什么这样设计”然后合上书自己写。写不出来就对照源码改写三遍之后基本就能默写了。2.3 手写防抖节流闭包与时间的双重考验防抖和节流也是有赞笔试的热门选手。它们的区别用一句话说就是防抖是“最后一次说了算”节流是“固定频率执行”。业务场景也很好理解搜索输入框实时请求接口适合防抖滚动加载更多、拖拽缩放适合节流。标准防抖实现function debounce(fn, delay 300) { let timer null; return function (...args) { if (timer) clearTimeout(timer); timer setTimeout(() { fn.apply(this, args); timer null; }, delay); }; }标准节流实现function throttle(fn, interval 300) { let lastTime 0; return function (...args) { const now Date.now(); if (now - lastTime interval) { lastTime now; fn.apply(this, args); } }; }这两个基础版都是对的但如果笔试题目加了一个条件——“要求首次触发立即执行”或者“要求支持取消”你就需要扩展了。我当时在试卷上补充了一个带cancel方法的防抖因为面试官很可能希望看到你对“清除定时器”这块有掌控力function debounce(fn, delay 300, immediate false) { let timer null; let invoked false; function debounced(...args) { if (timer) clearTimeout(timer); if (immediate !invoked) { fn.apply(this, args); invoked true; } timer setTimeout(() { fn.apply(this, args); timer null; invoked false; }, delay); } debounced.cancel () { if (timer) clearTimeout(timer); timer null; invoked false; }; return debounced; }这类题目真正想考察的是你对“闭包保存状态”和“定时器生命周期”的理解。准备的时候可以多问自己几个问题为什么timer要放在闭包里而不是全局为什么args要用...args收起来再传给fn为什么要保持this指向这些问题都能答上来手写题就稳了。3. 基础理论题的答题要点与易错点3.1 JavaScript 核心机制闭包、this与事件循环有赞笔试第二部分选择题和简答题基本都在围绕JavaScript核心机制打转。我印象比较深的有这几道第一typeof null的结果是什么答案是object这是语言设计遗留的历史bug。但从这道题能延伸出很多知识点null是原始类型typeof和instanceof的区别以及如何准确判断一个值是不是nullObject.prototype.toString.call(null) [object Null]。第二给一段闭包循环的经典代码问输出什么for (var i 0; i 5; i) { setTimeout(() console.log(i), 100); }输出是5个5。原因是var没有块级作用域循环结束i已经变成5setTimeout回调执行时读取的是同一个i。修复方式有改用let声明、用IIFE包一层、或者用setTimeout的第三个参数传参。这道题看似简单但它能考察你对作用域、事件循环、异步执行时机的理解是否透彻。第三事件循环的输出顺序题。这类题你只要记住一个口诀就不容易错同步代码先执行遇到Promise.then进微任务队列遇到setTimeout进宏任务队列本轮宏任务结束后清空所有微任务然后再取下一个宏任务。我用一个具体例子说明console.log(1); setTimeout(() console.log(2), 0); Promise.resolve().then(() console.log(3)); console.log(4); // 输出顺序1 4 3 2这里很多新手会猜1 4 2 3因为直觉上setTimeout先注册。但Promise的then进入的是微任务队列会在当前宏任务结束后、下一个宏任务开始前被清空所以3一定在2之前。再补充一个高频考点async/await和Promise混在一起时的输出顺序。核心记住一句话await后面的代码相当于被放进了.then是异步执行的async function test() { console.log(1); await Promise.resolve(); console.log(2); } test(); console.log(3); // 输出顺序1 3 2答题的时候不要只写答案把事件队列的过程用注释或者简短的文字写清楚这样即便结果算错阅卷人也能看出你懂机制能拿过程分。3.2 HTTP与浏览器缓存没有死记硬背只有逻辑链条前端笔试绕不开的一道题是“在浏览器地址栏输入URL并回车到页面渲染完成中间发生了什么”有赞的版本甚至要求用图示或流程描述并对关键步骤做解释。这道题拼的不是记忆而是你能否把“网络请求、DNS解析、TCP连接、TLS握手、HTTP响应、浏览器解析渲染”这条链路讲清楚并且能针对每步补充细节。比如DNS解析要说清楚先查浏览器缓存、再查系统缓存、再查路由器缓存、最后递归到根域名服务器TCP连接要提到三次握手为什么要三次——因为要确认双方收发能力都没问题HTTPS要提到TLS握手证书验证对称加密和非对称加密的结合浏览器渲染要提到DOM树、CSSOM树、渲染树、布局、绘制以及JavaScript阻塞渲染的现象。浏览器缓存这道题也非常高频核心考点是强缓存与协商缓存缓存类型主要字段命中时状态码特点强缓存Cache-Control如max-age3600200from memory/disk cache不会请求服务器强缓存Expires绝对过期时间200from memory/disk cacheHTTP/1.0字段优先级低于Cache-Control协商缓存ETag / If-None-Match304需要请求服务器比较资源的唯一标识协商缓存Last-Modified / If-Modified-Since304比较最后修改时间精度到秒答题的时候要补充一句Cache-Control里的no-cache并不是不缓存而是每次使用缓存前都要向服务器验证也就是走协商缓存no-store才是真正不缓存。很多人在这里被绕晕记住这个区分阅卷人会觉得你理解得很细。跨域也是必考项。有赞的业务场景里前后端分离很常见所以理解跨域解决方案是基本要求。答题框架是先说明同源策略的定义协议域名端口再说明为什么需要限制跨域最后列出解决方案——CORS是标准方案JSONP能解决GET请求且兼容老浏览器但只支持GET还可以提到反向代理和postMessage。CORS里需要重点展开的是预检请求preflight当请求方法不是GET/POST或者Header中包含非简单头时浏览器会先发一个OPTIONS请求询问服务器是否允许服务器返回Access-Control-Allow-*相关字段后真正的请求才继续发送。复习这一块我的经验是不要孤立地背字段而是用“一个用户输入URL点击回车”的故事把所有知识点串起来多串几遍面试的时候就像背自己写的故事一样自然。3.3 CSS布局与移动端适配的解题套路CSS题目在有赞笔试里的占比不高但往往是拉开分差的隐藏项。因为很多前端同学的重心放在JavaScript上对CSS布局细节反而容易丢分。首先是三栏布局这几乎是所有笔试的保留节目。实现方式至少有四种浮动、绝对定位、flex、grid。我建议大家都掌握并且理解每种方式的优缺点实现方式优点缺点float margin兼容性好需要清除浮动结构略复杂position: absolute代码简单父容器需要定位左右栏脱离文档流flex代码简洁弹性强不支持太老浏览器grid语义清晰代码最少兼容性要求更高我答题时最常用的是flex版本div classcontainer div classleft左侧栏/div div classcenter主内容/div div classright右侧栏/div /div.container { display: flex; } .left { width: 200px; flex-shrink: 0; } .right { width: 200px; flex-shrink: 0; } .center { flex: 1; min-width: 0; }有个细节容易被忽略flex: 1默认是flex: 1 1 0%在弹性布局里如果中间的文本内容过长flex子项默认可以收缩但如果你在子项内设置了超长英文或者不换行文本可能会导致溢出。给.center加上min-width: 0是很多老前端踩坑换来的经验。垂直居中也是一道经典送分题。我总结一个万能的flex方案几乎适用于所有场景.parent { display: flex; align-items: center; justify-content: center; }如果要求不支持flex的老环境可以用position transform.parent { position: relative; } .child { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }移动端适配的题目也很常见。核心思路是rem方案和vw方案。rem方案需要动态设置根字体大小通常配合flexible.js这类库使用vw方案直接用视口单位不需要JavaScript介入。我当时在答题时给出了一个基于vw的适配方案代码简洁很多html { font-size: calc(100vw / 7.5); } /* 设计稿宽度750px时1rem 100px方便换算 */ .box { width: 5rem; /* 实际等于500px对应设计稿的500px */ }要注意的是calc(100vw / 7.5)假设设计稿为750px宽把屏幕等分成7.5份。如果设计稿是375px就改成calc(100vw / 3.75)。这个方案的核心逻辑是让根元素的font-size随视口宽度缩放从而让所有使用rem单位的元素等比缩放。答题时把这个推导逻辑写清楚比只给一段代码更有说服力。4. 实战过程复盘一道典型的综合题从读题到落地4.1 回忆版题目还原与需求拆解有赞第二批笔试最后一道题是一道综合题记忆里大概是这样的实现一个“商品列表页”支持本地模拟数据、搜索筛选、价格排序、分页展示语言和框架不限要求结构清晰、代码可运行。这道题限时30分钟左右分值最高也是最容易拉开差距的一道。为什么说这道题很“有赞”因为商品列表、搜索、排序、分页是电商后台的典型场景一道题把“数据管理、状态维护、交互处理、性能优化”全串起来了。我当时的策略是不着急写代码先花5分钟拆解需求。拆完之后我得到这样一张清单数据层准备一份模拟数据数组对象含id、名称、分类、价格、库存等字段模拟异步请求返回Promise。渲染层渲染商品卡片列表包括商品名、价格、操作按钮。交互层搜索框输入时防抖触发筛选点击价格排序切换升序/降序点击分页按钮切换页码。边界处理空数据展示提示搜索无结果提示分页时更新当前页数据。代码结构把数据请求、筛选排序、渲染分别拆函数保持可读性。这几分钟拆解非常重要。笔试时代码写得完不完是次要的逻辑完整才能让阅卷人从几十份试卷里记住你。我当时身边有同学拿到题就开始手写写到一半发现分页和排序的状态没想清楚代码越改越乱最后交了半成品非常可惜。4.2 从思路到编码的完整演进基于上面拆解的需求我用原生JavaScript实现了一个可运行版本放在这里给大家参考。核心代码不复杂但每一步的取舍都有原因。首先准备模拟数据和请求函数const mockData Array.from({ length: 45 }, (_, i) ({ id: i 1, name: 商品${i 1}, price: Math.round(Math.random() * 1000) 1, category: [数码, 家居, 服饰][i % 3], })); function fetchGoods() { return new Promise((resolve) { setTimeout(() resolve(mockData), 500); }); }这里用Array.from生成长度为45的数组三个分类交替出现。包装成Promise是为了模拟真实接口的异步行为后续接真实后端时只需要替换这一段逻辑其他代码无需改动。然后是核心状态和渲染逻辑。我选择用一个state对象集中管理搜索关键词、排序方向、当前页码这样数据流的逻辑清晰const state { keyword: , sortOrder: default, // default | asc | desc page: 1, pageSize: 10, allData: [], }; let filteredData [];数据获取与处理的主流程放在一个init函数里async function init() { state.allData await fetchGoods(); applyFilterAndSort(); } function applyFilterAndSort() { let list state.allData.slice(); if (state.keyword) { list list.filter((item) item.name.toLowerCase().includes(state.keyword.toLowerCase()) ); } if (state.sortOrder asc) { list.sort((a, b) a.price - b.price); } else if (state.sortOrder desc) { list.sort((a, b) b.price - a.price); } filteredData list; render(); }需要注意slice()是为了复制一份数组避免后续sort操作直接影响原始数据。虽然sort在原数组上操作复制一份可以保证多次筛选排序时原始数据不被污染这是一个容易被忽略的细节。分页逻辑在render函数里处理function render() { const listEl document.getElementById(goods-list); const totalPages Math.max(1, Math.ceil(filteredData.length / state.pageSize)); if (state.page totalPages) { state.page totalPages; } const start (state.page - 1) * state.pageSize; const pageData filteredData.slice(start, start state.pageSize); if (pageData.length 0) { listEl.innerHTML div classempty没有符合条件的商品/div; } else { listEl.innerHTML pageData .map( (item) div classgoods-item span classgoods-name${item.name}/span span classgoods-price¥${item.price}/span span classgoods-category${item.category}/span /div ) .join(); } document.getElementById(page-info).textContent ${state.page} / ${totalPages} 页; }搜索和排序事件的绑定也需要注意防抖const searchInput document.getElementById(search-input); const sortBtn document.getElementById(sort-btn); searchInput.addEventListener(input, debounce((e) { state.keyword e.target.value.trim(); state.page 1; applyFilterAndSort(); }, 300)); sortBtn.addEventListener(click, () { const orderMap { default: asc, asc: desc, desc: default }; state.sortOrder orderMap[state.sortOrder]; state.page 1; applyFilterAndSort(); }); document.getElementById(prev-btn).addEventListener(click, () { if (state.page 1) { state.page - 1; applyFilterAndSort(); } }); document.getElementById(next-btn).addEventListener(click, () { const totalPages Math.ceil(filteredData.length / state.pageSize); if (state.page totalPages) { state.page 1; applyFilterAndSort(); } }); init();这套实现的核心思路是“状态驱动渲染”每次交互只更新state然后统一调用applyFilterAndSort重新计算列表并渲染。它很简单但已经具备了现代前端框架的核心思想——数据驱动视图。在笔试场景里用原生JS能写出这种结构会向阅卷人传递一个强烈信号即使不依赖框架你也有清晰的工程意识。4.3 交卷前的自查清单综合题的代码写完之后不要急着提交花3到5分钟做一轮自查能挽回不少分。我自己总结了一套检查清单每次笔试都会在脑子里过一遍是否有内存泄漏风险addEventListener绑定的函数是否需要移除全局变量有没有被意外污染。边界条件是否覆盖空数组查询结果是否展示“无数据”提示当前页超出总页数时是否做了兜底输入框只有空格时是否被trim处理。异步操作是否有竞态风险快速输入连续触发请求时前一个请求的响应可能晚于后一个导致界面闪烁或数据错乱。虽然模拟数据不会出现但代码里是否用“请求序号”或“取消旧请求”的方式处理了这个问题。代码可读性是否达标命名是否语义化是否有多余注释或没有注释功能模块是否拆成了小函数。我当时在第一版代码里漏掉了分页按钮在边界页时的禁用状态虽然功能没错但交互体验有明显瑕疵。如果在笔试现场建议在按钮上加上disabled逻辑哪怕只是加一行CSS类名切换也能体现你对用户体验的把控。另外如果时间允许我强烈建议你实际跑一下代码而不是只在脑子里推演。笔试平台的在线编辑器不一定支持运行和调试但你可以在本地的CodePen或本地HTML文件里快速验证再用最终代码替换上去。有些编辑器对模板字符串、箭头函数这类语法支持不一致提前验证能避免低级语法错误。5. 校招前端笔试的备考工具箱与排坑心得5.1 备考资料的筛选与使用顺序应届生准备前端校招笔试最怕的就是资料收藏了一堆但不知道从哪看起。以我当时的经验真正值得精读的资料就三类。第一类JavaScript语言基础。首选《JavaScript高级程序设计第4版》重点看变量与类型、作用域与闭包、面向对象、事件循环、Promise这几章。这本“红宝书”的覆盖面足够应对90%的笔试基础题。配合阮一峰的《ES6 入门教程》看新增特性比如let/const、箭头函数、解构赋值、Map/Set、class、async/await、模块化。这些ES6特性在2019年的笔试中已经是默认要求现在更是基础中的基础。第二类网络与浏览器原理可以用《图解HTTP》打底再结合MDN文章补充浏览器缓存、渲染机制、CORS的细节。网络这块不用背得太深重点是能用自己的话解释一遍“输入URL到页面渲染”的完整链路能把强缓存和协商缓存的字段说清楚能答出跨域解决方案。第三类手写代码专题直接看社区里整理好的“前端手写题”系列。建议先自己写一遍再看参考答案。优先级排序是防抖节流、深拷贝、Promise、数组去重、new/apply/call/bind、发布订阅、柯里化、继承。有赞这批笔试基本就覆盖了前几个。刷题平台方面LeetCode刷“热题100”中的easy题和部分medium题即可重点刷数组、字符串、链表、二叉树这几类。牛客网有专门的前端笔试题库可以模拟真实笔试环境。我当时给自己定的目标是每天1~2道LeetCode坚持一个月手写题保持一周做3次限时训练每次控制在20分钟内。我不建议零基础就扎进题海。先过一遍基础知识的框架再刷题效果会好很多。正确顺序是基础知识过一遍 → 做手写题检验 → 针对薄弱点回看知识点 → 限时模拟。这样循环两到三遍知识网络的牢固度会有一个明显的提升。5.2 笔试过程中的时间分配策略90分钟做七八道题时间其实挺紧张。以有赞第二批笔试为例我的建议分配方案是题目类型建议耗时策略选择题/填空题10-15分钟快速作答犹豫超过1分钟的题先标记跳过简答题15-20分钟分点作答画过程图关键术语写准手写代码题30分钟先确认边界再写核心逻辑最后补扩展综合题20-25分钟先拆需求再写整体结构功能可部分实现检查5-10分钟检查有无漏题、语法错误、明显边界问题这个方案的核心思想是“先易后难、保底拿分”。不要在一道手写题上死磕超过15分钟如果15分钟还理不清思路先写一个能跑的简化版本把注释标出来说明完整版思路然后去做其他题。笔试看的是总分不是单题满分学会战略性放弃非常重要。我见过一个真实的失败案例有位同学在一道手写Promise上花掉了40分钟写到最后发现逻辑还是有问题结果后面的简答题和综合题几乎没动笔最后总分没及格。备考的时候我们总想着“万一考的都会”但考场上真正决定胜负的是你在有限时间里的资源分配能力。5.3 心态与应试细节在线笔试的特殊性校招笔试大多数是线上进行环境因素其实影响很大。我自己的建议是考试前把环境准备好用Chrome浏览器并关掉多余插件提前登录笔试平台确认摄像头和麦克风权限部分平台要求开启准备好本地编辑器以备在线编辑器不好用的情况确保网络稳定虽然校招季宿舍网络经常不稳定但尽量减少风险。在线编辑器往往不带代码高亮和自动补全手感很差。我的应对技巧是先在本地编辑器里把代码调通再复制到在线编辑器里这样能避免因为编辑器卡顿导致代码丢失。如果笔试平台不支持JS运行那就只能靠人工审查了更要在代码里写清楚逻辑注释。碰到完全不会的题怎么办我的原则是“绝不交白卷”。哪怕写不出正确答案也要把相关的知识点、伪代码、思路写上去。比如让你手写一个红黑树正常情况下不会考这么偏但万一遇到了你可以写出BST插入的基本框架再注明红黑树的旋转和变色逻辑。阅卷人给分往往是分步骤给的一个思路完整的答案即使代码跑不通也可能拿到一半以上的分数。最后再说说心理状态。校招笔试现场的时间压力很大我经常看到有人在最后一分钟疯狂改代码结果越改越乱。我的经验是最后5分钟只做一件事就是提交不要再动代码。改对一个小bug的收益远远抵不上改错一个地方导致整个页面白屏的风险。写代码是理性工作保持冷静的头脑比临场多憋出一个答案重要得多。我个人在准备校招时把“限时模拟”提到很高的优先级。每一场笔试之前我都会在牛客网上找一套真题用手机定好倒计时模拟完整的考试流程。第一二次肯定做不完但到第四五次之后你会逐渐找到自己的节奏点哪类题可以加速、哪类需要多留时间。这种节奏感是刷单个题目练不出来的只能靠模拟。有赞2019校招第二批笔试只是校招路上的一个缩影。与其说这是一篇真题解析不如说它是一套“如何系统准备前端笔试”的方法论。基础知识的深度、手写代码的熟练度、时间分配的判断力这三样东西准备到位了无论你遇到的是有赞还是其他公司都能稳定发挥出真实水平。