有赞前端笔试题解析:JavaScript到工程化核心考点

发布时间:2026/8/29 22:04:35
有赞前端笔试题解析:JavaScript到工程化核心考点 很多同学在准备校招笔试的时候习惯性按“大厂题库”刷题结果一到真正笔试就蒙了。原因很简单头部互联网公司尤其是做SaaS、电商中台这类业务的和纯流量产品公司的前端考察侧重点其实差得挺远。拿有赞2019校招前端笔试第一批来说这份卷子最大的特点就是不装、不炫技但每一道题都在考验你能不能真正干活。整张试卷覆盖了JavaScript语言基础、异步机制、浏览器原理、前端工程化、数据结构和算法、以及实际业务场景设计。难度上属于“基础题占六成进阶题占三成剩下的一成用来区分尖子生”的典型校招笔试题型。本文我不会只给你贴答案而是把每一类考点背后的真实意图、容易踩的坑、以及我自己的做题思路完整拆开讲清楚这篇文章的价值在于帮你建立一套应对前端笔试的分析框架而不是死记硬背几十道题。1. 有赞前端笔试的考点分布与出题逻辑1.1 先搞懂有赞是一家什么样的公司有赞本质上是商家服务公司核心产品是帮助商户搭建商城、做私域流量运营、管理订单和客户。这种业务形态决定了它对前端工程师的诉求非常明确电商交易链路复杂、页面状态多、支付流程不能出错、中后台系统极高频、营销活动页面需要快速迭代。所以你去看有赞的前端笔试题会发现它不太会像某些公司那样问“请实现一个红黑树”或者“讲讲编译器工作原理”。有赞的题目更务实数组去重、数据深拷贝、Promise执行顺序、事件循环、浏览器缓存、如何设计一个组件、如何处理首屏性能。这些题目背后每一道都在对应真实业务中的具体问题。笔试中高频出现的考点及其对应的业务动机数组去重和深拷贝对应的是“处理后端返回的复杂JSON数据保证状态更新不可变”的日常需求事件循环和Promise对应的是“支付回调、异步请求竞态”等电商场景的代码编写基础浏览器缓存和页面加载流程对应的是“商城首页和活动页的秒开体验优化”算法题对应的是“海量商品列表的排序、筛选和索引”的通用能力。理解了这层对应关系你再看题目就不会觉得散它们其实是一条线串下来的。1.2 题目结构难度分层和淘汰逻辑整张试卷可以分成三个层次第一层语言基础与代码输出题约35分 - 考察this指向、闭包、原型链、类型转换、作用域 - 这类题刷过就能答对但考察细心程度 第二层进阶应用与代码实现题约40分 - 考察数组去重、深拷贝、防抖节流、Promise封装 - 需要平时真正写过代码光背概念很难通过 第三层工程化与算法约25分 - 考察webpack构建优化、性能优化、以及一道中等难度的算法题 - 决定面试官要不要深聊你这种结构的淘汰逻辑非常清晰第一层筛掉基础不扎实的人第二层筛掉只会背书没写过代码的人第三层筛掉没有大局观的人。所以你在做题时要做的是“确保前两层满分第三层拿部分分”而不是死磕某道题导致时间不够。1.3 与同类型公司笔试题的横向对比跟同期其他公司的前端笔试题相比有赞的这套卷子有很明显的差异化特征对比维度有赞纯流量型互联网公司业务题占比高偏向电商交易场景低偏向通用场景框架考察方式侧重原理与实现侧重API使用工程化深度深入构建、打包优化通常问概念算法难度中等偏应用中等偏难偏竞赛横向比较下来结论很清晰如果你目标方向是SaaS、电商、产业互联网这类公司有赞这批笔试题的参考价值非常大如果你目标是做纯To C产品的公司这套题也值得做但需要额外补充算法题训练。2. JavaScript核心原理题this指向、闭包与作用域2.1 输出题背后的this指向规则有赞笔试第一道大题通常会安排几道代码输出题其中this指向是最经典、最容易被扣分的地方。比如下面这类的经典变体var name global; var obj { name: obj, getName: function() { return function() { console.log(this.name); }; } }; obj.getName()();这道题的关键在于理解“函数调用时this的指向是由调用方式决定的而不是定义位置”。obj.getName()返回的是一个普通匿名函数当直接执行这个匿名函数时它是作为全局函数被调用的所以this指向window。在浏览器环境下输出global特殊情况下undefined。如果你能画出执行栈的调用关系这类题基本不会错。另一个高频变体是箭头函数的this绑定var name global; var obj { name: obj, getName: function() { return () { console.log(this.name); }; } }; obj.getName()();这里的输出反而变成obj因为箭头函数本身不绑定this它的this继承自外层的执行上下文即getName函数被调用时的this。这种题一旦理解透不管怎么变都能秒解普通函数看调用点谁调用就指谁箭头函数看定义点外层上下文是什么它就是什么。2.2 闭包的典型考法与内存泄漏陷阱闭包是有赞笔试必考点通常结合循环和定时器来考。最常见的那个题目如下for (var i 0; i 5; i) { setTimeout(function() { console.log(i); }, 1000); }输出结果是五个5这个很多人知道。但笔试很少只考这一种更多是换一个外壳来“钓鱼”比如让定时器在0秒、100毫秒依次输出或者把var改写成let。改写成let之后输出是0、1、2、3、4因为let为每次循环创建了独立的块级作用域定时器回调里引用的i实际上是每一次迭代的独立副本。除了能写出答案有赞的题目还会让你解释“闭包对内存性能的影响”本质上就是在考察你是否清楚当闭包引用了外层函数的变量而这些变量又始终被内层函数持有引用时外层函数的作用域链无法被垃圾回收机制释放。实际操作中如果创建了大量闭包且不需要继续使用时应该把引用置为null帮助GC回收。2.3 原型链的经典继承实现原型链相关题目在有赞笔试中也是常客。最容易考的是两种一种是让你写出new关键字做了什么另一种是手动实现一个寄生组合式继承。以实现new为例我建议按四步来理解function myNew(fn, ...args) { // 第一步创建一个新对象 const obj {}; // 第二步让新对象的原型指向fn.prototype Object.setPrototypeOf(obj, fn.prototype); // 第三步将fn的this绑定到新对象上并执行 const result fn.apply(obj, args); // 第四步如果构造函数返回的是对象则返回该对象否则返回新对象 return typeof result object result ! null ? result : obj; }这个实现对应了JS引擎处理new表达式的完整流程。重点在第四步很多构造函数内部有显式返回值如果返回的是引用类型那么new的结果会变成这个引用类型对象。这个细节非常容易被忽略却是笔试判卷时用来区分“背模板”和“理解原理”的关键点。3. 异步编程事件循环、Promise 与 async/await3.1 从一道Event Loop题目说起有赞笔试题中异步编程相关的输出题基本不会缺席。常见的形式是给出一串混有setTimeout、Promise、async/await的代码让你写出输出顺序。这类题目表面上是在考察事件循环实际上是在看你有没有建立完整的“执行栈、宏任务、微任务”心智模型。来看一道我当时印象很深的变体async function async1() { console.log(A); await async2(); console.log(B); } async function async2() { console.log(C); } console.log(D); setTimeout(function() { console.log(E); }, 0); async1(); new Promise(function(resolve) { console.log(F); resolve(); }).then(function() { console.log(G); });正确答案是D、A、C、F、B、G、E。这道题的魔鬼细节在await async2()这一行。async2()是同步执行后立即返回一个Promise但await部分之后的代码console.log(B)是被放到微任务队列中的而Promise.then中的console.log(G)也是被放到微任务队列中的。它们的入队顺序决定了最终的输出顺序先入队的B在G之前。我自己的做题习惯是直接在草稿纸上画三个队列同步执行栈、微任务队列、宏任务队列然后按时间推进顺序打勾。画出队列图后这类题基本不会错。你可以把这个方法变成非常机械的操作遇到异步输出题就画队列。3.2 实现一个带并发限制的异步调度器有赞笔试里单纯考事件循环输出的题还不够后面通常会接一道让你手动封装异步控制的题“实现一个带并发限制的调度器”就属于高频考点。这类题的背景其实是电商大促时前端需要同时上传多张图片、批量请求商品详情接口但浏览器和服务器都有并发限制所以需要控制同时执行的异步任务数量。一个可以直接写在卷子上的实现思路如下class Scheduler { constructor(limit) { this.limit limit; this.activeCount 0; this.queue []; } add(promiseCreator) { return new Promise((resolve) { const task () promiseCreator().then(() { resolve(); }).finally(() { this.activeCount--; this.next(); }); if (this.activeCount this.limit) { this.activeCount; task(); } else { this.queue.push(task); } }); } next() { if (this.queue.length 0 this.activeCount this.limit) { this.activeCount; const task this.queue.shift(); task(); } } }核心思路就是“一个执行池加一个等待队列”。当正在执行的任务数小于limit时直接执行否则把任务放进等待队列。每个任务执行完从队列头部取出一个任务补位。这套设计思路不仅笔试适用日常开发中自己封装并发控制组件时也是同样的套路。3.3 async/await的错误处理异步部分的最后一道题往往是“如何优雅处理async/await的异常”。很多人只会用try/catch包住每一段异步代码但笔试更期待你用.catch加统一错误出口的方式。下面是我推荐写在卷子上的写法async function loadData() { const res await fetch(/api/data) .then(response response.json()) .catch(error ({ error })); if (res.error) { // 统一错误处理逻辑 return; } return res; }这种处理方式把错误和正常数据统一到同一条代码路径上调用方只需要做一次判断。在实际业务中多个接口请求往往并发进行你可以用Promise.allSettled替代Promise.all避免某个接口失败导致整个页面崩溃。这一点在业务代码里非常重要所以有赞笔试偶尔也会顺势考到Promise.allSettled和Promise.all的区别。4. 数据与对象处理数组去重、深拷贝的工业级实现4.1 数组去重不能只会Set数组去重是有赞笔试题中出现频率最高的题目之一。表面上看很简单但出题人会故意叠加条件来增加难度。比如“对一个包含对象、包含NaN、包含undefined和null的数组去重”或者“要求不能改变原数组顺序”。如果你一上来就写[...new Set(arr)]基础分拿到但真正的进阶条件你还没有覆盖到。比如NaN的情况在Set中NaN和NaN被视为相等所以new Set([NaN, NaN])可以正确去重成[NaN]。如果你用indexOf去判断NaN永远不等于自身去重逻辑会失效。如果你使用includes虽然能正确处理NaN但遇到对象时includes用的是严格相等比较两个内容相同但引用不同的对象不会被去重。所以如果你想让对象按“内容相同”来去重需要设计特殊的比较逻辑。笔试时我建议分两步回答第一步写Set版本第二步针对“对象数组去重”给一个通用方案function uniqueArray(arr) { const seen new Map(); return arr.filter(item { const key typeof item object item ! null ? JSON.stringify(item) : item; if (!seen.has(key)) { seen.set(key, true); return true; } return false; }); }用Map而不是Object来作为存储结构本质上是为了覆盖所有类型的key。用Object有个知名陷阱obj[null]会被转成字符串null导致不同类型的原始值被错误合并。这类细节一旦在代码里体现出来会让阅卷人一眼看出你踩过这方面的坑。4.2 深拷贝JSON.parse(JSON.stringify())错在哪另一道高频笔试题是手写深拷贝。我见过很多同学直接写JSON.parse(JSON.stringify(obj))然后就被判错了因为这种写法有一堆限制。笔试中这些限制通常被包装成选择题或者判断题出现值为undefined、function、Symbol的属性会被丢弃Date对象会被转成字符串而不是Date类型RegExp和Error对象会被转成空对象{}嵌套循环引用会直接报错NaN和Infinity会被转成null。如果你在笔试中需要手写深拷贝至少要覆盖普通对象、数组、循环引用以及保留基本类型的正确性。下面这个版本是一个相对标准且有赞阅卷人认可度比较高的实现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); } const existing map.get(target); if (existing) { return existing; } const clone Array.isArray(target) ? [] : {}; map.set(target, clone); Reflect.ownKeys(target).forEach(key { clone[key] deepClone(target[key], map); }); return clone; }这里用了WeakMap来记录“原对象到克隆对象的映射”当遇到循环引用时直接返回已经创建的克隆对象避免无限递归。用Reflect.ownKeys替代Object.keys可以让拷贝结果连Symbol类型的键也不遗漏。这个版本虽然还有Map、Set等类型没有处理但作为笔试答案已经足够体现能力了。4.3 深拷贝在业务中的真实用武之地我一直强调笔试题目不是死题它们背后有真实业务场景。深拷贝在有赞这类SaaS公司里的典型应用场景是购物车数据快照、表单编辑器撤销重做、以及运营后台组件的配置状态回滚。比如用户在商城后台拖拽配置一个商品活动页每一步操作都会改变组件的配置对象。为了支持“撤销”功能我们需要在每次操作前保存一份配置快照如果用户点撤销就把当前配置替换成上一次的深拷贝结果。如果在这里只是做浅拷贝那么快照和当前状态会共享底层对象改当前配置会直接污染历史快照整个撤销功能就废了。所以深拷贝不是“造轮子学概念”而是实打实的功能基础。5. 浏览器与网络缓存策略、页面加载过程5.1 浏览器的三层缓存机制有赞的商城页面中对静态资源的加载速度要求很高所以笔试针对浏览器缓存会考得很细。选择题出现频率最高的有三个强缓存与协商缓存的区别、Cache-Control与Expires的优先级、ETag与Last-Modified的优先级。理解浏览器缓存我建议用“过期时间”和“资源变更验证”这两条线索串起来第一次请求资源时服务器可以通过响应头告诉浏览器“你能缓存多久”。在缓存有效期内浏览器直接从本地读资源不再发请求这就是强缓存它靠Cache-Control的max-age和Expires来标识。Cache-Control优先级高于Expires因为Expires是绝对时间容易受客户端本地时间修正影响max-age是相对时间更可靠当强缓存过期后浏览器不是直接放弃缓存而是带着缓存的标识If-Modified-Since或If-None-Match去问服务器“这个资源有没有变化”。服务器返回304 Not Modified就会让浏览器继续用本地缓存这就是协商缓存。ETag优先级高于Last-Modified因为Last-Modified只能精确到秒同一秒内多次修改文件时可能误判为未修改。实际操作中静态资源的文件名带上hash值配合Cache-Control: max-age31536000来缓存文件内容变化后文件名变化导致URL变化浏览器自然请求新文件。这套方案在几乎所有中大型前端项目里都是标准做法笔试时你可以直接写出来作为加分项。5.2 从输入URL到页面渲染讲清楚“浏览器做了什么”这个题目太经典了但很多人回答得很乱。有赞阅卷时最看重的是你能不能按时间顺序把事情讲得有层次、不遗漏关键步骤。我自己的回答框架分为五个阶段DNS解析把域名解析成IP地址优先查浏览器缓存、操作系统缓存再到本地DNS服务器和根域名服务器逐级查询建立TCP连接经过三次握手建立可靠的传输通道如果是HTTPS还需要进行TLS握手协商密钥发送HTTP请求并获取响应浏览器构造请求报文服务器处理后返回HTML文档和其他资源解析和渲染HTML经过解析构建DOM树CSS构建CSSOM树两者合并生成渲染树然后进行布局和绘制页面资源加载HTML解析过程中遇到script标签会暂停解析先下载并执行脚本所以现在普遍在脚本标签上加defer或async。关于第四、第五阶段有赞笔试可能还会追问“重排和重绘的区别”。重排是指布局变化触发重新计算几何位置重绘是指像素级更新但布局不变。实际优化时可以通过transform替代top/left来触发合成器只做合成避免整棵渲染树重排。如果你把这条写进去阅卷人会判断你是有实际性能调优经验的人。5.3 前端性能优化首屏时间过长的排查思路性能优化类的题目在有赞笔试中通常以开放性问答题出现。比如“首屏加载速度慢你会怎么排查和优化”。这类题没有标准答案但有标准的答题结构。我习惯从“前端视角”和“后端视角”分开列前端视角看资源的体积和数量压缩代码、开启gzip、做tree-shaking、把首屏不需要的组件做按需加载、把体积较大的库用CDN加载、图片用WebP并做懒加载后端视角检查接口响应时间如果接口太慢导致页面白屏需要对接口做缓存或把首屏数据通过HTML直出、SSR等方式内联到页面里加载策略层面把关键CSS内联、把非关键脚本延迟加载、使用preconnect提前建立连接。把这些维度答全基本能覆盖这道题的采分点。注意不要只停留在“图片懒加载”这个层面有赞这类公司更看重你对完整加载链路有全局认识多答链路、少说单个技巧。6. 前端框架与组件化类组件与函数组件的深层理解6.1 为什么有赞要考察框架底层原理有赞是React和Vue双栈都用的公司早期很多中后台系统用React小程序和H5商城则相当比例用Vue。所以笔试不会指定单一框架而是以“框架通用原理”为主来出题。比如“虚拟DOM是什么它一定比真实DOM操作更快吗”这个问题实质上是在考察你对“声明式UI”和“命令式UI”的理解。虚拟DOM并不是魔法它的优势在于让开发者用声明式的方式描述UI由框架在更新时算出最小变更集合。如果只是一个按钮的文本变化手动改DOM一定比虚拟DOM diff更快虚拟DOM胜在“维护成本低 在复杂交互下能批量更新”。面试官看到你能辩证回答“虚拟DOM不总是更快”反而会给你加分因为这说明你真的思考过而不是只会背概念。6.2 React中setState到底是同步还是异步有赞React题里经典陷阱之一就是“setState到底是同步还是异步”。这个问题的标准答案在React 18之前是“在React事件处理器中是异步批量执行的在setTimeout、原生事件监听器、Promise回调中是同步的”。容易出错的地方是这里handleClick() { this.setState({ count: this.state.count 1 }); console.log(this.state.count); // 这里拿到的还是原来的值 this.setState({ count: this.state.count 1 }); console.log(this.state.count); // 拿到的还是原来的值 }连续调用两次setState如果直接用this.state.count来计算两次其实是基于同一个旧值计算的所以最终count只会加一次。正确做法是传函数给setStatethis.setState(prevState ({ count: prevState.count 1 })); this.setState(prevState ({ count: prevState.count 1 }));这个解法背后的原理是React会把传入的函数排入更新队列按顺序执行前一个函数的结果会传给下一个函数。这不仅是一个API用法更体现了对React更新调度机制的理解。面试官出这道题其实是想看你是否真的调试过这类问题而不只是查过文档。6.3 Vue的响应式原理和组件通信选型如果你用的是Vue有赞笔试会考到Vue 2的Object.defineProperty和Vue 3的Proxy的区别。核心点有两个一是defineProperty只能拦截对象已有属性的读写新增或删除属性需要额外调用Vue.set或Vue.delete二是Proxy可以拦截整个对象的13种操作包括属性新增、属性删除、in操作符、Object.keys等所以Vue 3不再需要特殊处理新增属性。组件通信方面有赞业务里最常见的场景是“父子组件传值”和“跨层级共享状态”。笔试中会让你手写组件间通信方式。统一的答案框架如下父子通信父传子用props子传父用$emit触发事件跨多级通信先考虑provide/inject适合祖先向后代注入依赖复杂状态共享使用Vuex或Pinia做集中管理避免$emit层层传递造成维护灾难。同样的问题如果你用的是React对应答案就是props回调、Context、以及Redux或Zustand。把这些核心方案答全框架题目基本不会失分。7. 工程化与构建Webpack原理和优化策略7.1 手写loader和plugin的底层逻辑Webpack相关的题目有赞笔试很少让你背配置项因为它更关心你是否理解构建工具的工作原理。考得比较深入的一道题是“让你手写一个loader去掉文件中的console.log”这非常符合业务实际因为上线前去掉调试日志几乎是所有团队的刚性需求。// strip-console-loader.js module.exports function(source) { return source.replace(/console\.log\([^)]*\);/g, ;); };你的loader代码本身不用太复杂但你需要解释清楚loader接收的是源码字符串经过处理后返回新代码loader支持链式调用执行顺序是从右到左、从下到上。如果你能进一步指出“loader不要做异步文件操作尽量保持纯函数”效果更佳。在plugin方面最简单的理解是“plugin基于事件钩子可以侵入webpack整个构建生命周期的不同阶段”。比如在emit阶段所有资源已经生成到compilation.assets中你可以遍历这些资源分析体积、做自定义压缩、生成额外的清单文件。plugin的思路是理解webpack的“事件流机制”而不是具体API。7.2 构建体积优化从分析到落地的完整流程有赞笔试的工程化问答题很多时候是基于一个假设场景某次构建后单个chunk体积超过了3MB页面加载非常慢请给出优化方案。这种题最怕空谈大道理好一点的答法是给出可执行的排查链路先用webpack-bundle-analyzer插件生成依赖树分析图观察到底是哪个包占用了大头如果是第三方库比如moment、lodash、echarts体积过大考虑按需引入、替换成更轻量的库、或者用splitChunks拆出单独chunk如果是业务代码中引入了多余的模块检查路由懒加载是否真正生效防止把整棵component tree都打进首屏chunk开启Gzip压缩服务端配合返回Content-Encoding: gzip通常能再压掉60%以上用optimization.minimizer配置CSS和JS的压缩器去掉注释和冗余代码。这个回答顺序本身就呈现了一个完整的性能排查思路先量化分析再定位问题最后验证效果。即使你没有任何调优经验按照这条路径去说阅卷人也会认定你有基本排查能力。7.3 为什么需要代码分割和按需加载代码分割Code Splitting在有赞这类中后台和商城项目中几乎是必做的优化。商城系统里一个商品详情页同时要用到图片懒加载、视频播放、地图定位、优惠计算模块如果把这些全部打进首屏页面加载就完了。实现代码分割的方式分两种一种是多入口配置把不同页面作为独立入口互不影响另一种是动态import()在代码中按需加载。后者更灵活也是路由懒加载的基础。在React中配合React.lazy和Suspense可以把某个路由对应的组件单独打包在Vue中则在路由配置里用() import(./view/Home.vue)的形式声明异步组件。笔试中有时候会让你解释“动态import为什么能够影响构建产物”。答案可以概括为webpack在分析依赖时遇到import()语句会把它当作分割点自动生成独立的chunk文件浏览器只在需要时请求该文件。理解这个机制之后你看network面板里那些0.js、1.js文件就不会觉得神秘了。8. 算法与数据结构笔试题里的中等难度陷阱8.1 数组相关的高频题型有赞笔试算法题不会很偏基本都是“数组字符串链表”这几种类型。常见的有“合并两个有序数组”、“找出数组中第K大的元素”、“两数之和”、“三数之和”。以“两数之和”为例其实有两种不同层次的解法// 暴力解法 O(n^2) function twoSum(nums, target) { for (let i 0; i nums.length; i) { for (let j i 1; j nums.length; j) { if (nums[i] nums[j] target) { return [i, j]; } } } return []; }// 哈希表解法 O(n) function twoSum(nums, target) { const map new Map(); for (let i 0; i nums.length; i) { const complement target - nums[i]; if (map.has(complement)) { return [map.get(complement), i]; } map.set(nums[i], i); } return []; }如果在笔试中只写了暴力解法通常只能拿一半分。有赞阅卷时更关注时间复杂度的优化。你多写一个哈希表版本并在注释里点明为什么复杂度是O(n)这会让阅卷人认为你具备基本的优化意识。8.2 手写深拷贝之外的“对象扁平化”除了常规算法题有赞有时候会出场景化的算法比如“将嵌套对象扁平化为单层结构”function flattenObject(obj, parentKey , result {}) { for (let key in obj) { const newKey parentKey ? ${parentKey}.${key} : key; if (typeof obj[key] object obj[key] ! null !Array.isArray(obj[key])) { flattenObject(obj[key], newKey, result); } else { result[newKey] obj[key]; } } return result; }这道题的业务背景很清晰后端返回了一个多级嵌套的JSON配置结构前端需要把它拍平后才有办法在表格里展示、在表单里编辑、在规则引擎里匹配。所以算法题不一定非要“高深”能解决业务问题的算法才是这类公司真正想要的算法能力。8.3 刷题建议针对前端岗位做减法如果你想要针对性准备有赞这类公司的算法题我建议不要一头扎进LeetCode海量刷题。正确做法是按专项来突破数组和字符串双指针、滑动窗口、哈希表这三种技巧吃透能覆盖80%的题目链表和二叉树掌握递归和迭代两种遍历方式再学一个复杂问题“二叉树最近公共祖先”就够动态规划优先搞定背包、最长递增子序列、编辑距离这三类题型优先队列和排序算法重点掌握堆排序、快排以及如何用堆解决Top K问题。前端岗位的算法题核心是考察逻辑能力和代码落地能力不是考察数学天赋。前面提到的数组、对象、字符串操作恰好是日常开发里最常用到的数据形态也是投入产出比最高的准备方向。9. 业务场景题如何设计一个商城优惠券组件9.1 把需求拆解成技术方案有赞笔试的最后部分有时会出一道半开放的业务设计题。它不会让你写完整代码而是让你给出设计方案。这类题非常考验综合能力同时也是最能拉开差距的地方。以“设计一个商城优惠券组件”为例我不会一上来就写代码而是先列表输出需求分析和模块拆分展示层不同优惠券状态未领取、已领取、已使用、已过期要展示不同样式交互层点击领取按钮后需要处理异步请求的中途状态避免重复点击数据层要考虑并发场景下同一张券是否允许用户多次领取前后端如何同步状态业务规则折扣、满减门槛、有效期、使用范围这些字段如何映射到前端交互规则。然后我会把组件设计成三步外层是容器组件负责管理数据状态中间是业务组件负责处理优惠券的领取和展示逻辑内部是纯展示组件只负责根据状态渲染UI。这样设计的好处是当优惠券从“满减”扩展成“折扣”时只需要增加一个类型配置项而不用改动整个组件链路。9.2 状态管理的选择和竞态处理在优惠券组件的状态管理上如果项目是中小型应用直接用组件的局部state就足够只有当多个页面共享用户优惠券数据、且需要跨路由同步状态时才考虑引入全局状态管理。真正值得展开的是“重复点击领取按钮”的竞态问题。用户在弱网环境下点击领取按钮请求还没返回状态码如果接口响应慢用户可能会连续点击好几次导致创建多个重复的领取订单。前端最简单的处理方案是加“请求中”锁state { isSubmitting: false }; async handleClaim(couponId) { if (this.state.isSubmitting) return; this.setState({ isSubmitting: true }); try { await claimCoupon(couponId); // 更新领取状态 } finally { this.setState({ isSubmitting: false }); } }更健壮的做法是加入请求序号只有最后一次请求的响应能更新状态防止旧响应覆盖新响应。这个细节在有赞这类交易场景里面能体现出你真正思考过用户体验和接口一致性。9.3 这套设计能力如何在笔试中展示如果笔试时时间充裕我建议你在答题末尾画一个极简的状态流转图用文字描述代替箭头即可未领取→领取中→已领取→已使用未领取→领取中→已过期。画完这个状态流转的语义阅卷人就能非常直观地看出你对业务规则的理解。文字描述之间注意逻辑闭环不要出现“领取成功”之后没有“已使用”的结局也不要出现“未领取”状态直接跳到“已使用”的不合理路径。我个人的经验是这类业务设计题优秀答案不一定是“代码写得多好看”而是“你有没有把边界场景和异常流程考虑清楚”。边界越多说明你越像一个在真实业务里待过的人而这也是有赞笔试最想筛选出的人。10. 应对策略与实战建议10.1 拿到卷子后先做这三件事有赞笔试有一定题量而且主观题需要花时间组织语言所以时间管理显得格外重要。我建议拿到卷子后不要从第一题开始顺序做而是用2分钟把整张卷子快速浏览一遍标记出自己最有把握的题和不确定的题按“先易后难”的原则做题先做输出题和选择题再做代码实现题最后做开放性设计题给每道题设定好时间上限一旦超时先跳过做下一道千万不要卡在一道题上超过15分钟。之所以不建议顺序做题是因为笔试时人容易在前面的基础题上反复犹豫导致后面分值高的大题没有时间写。反过来如果你先把有把握的分数拿稳即使最后开放题没写完整整体分数也不会太难看。10.2 做题时容易丢分的四个地方根据我自己的经验笔试丢分往往不是因为不会而是因为一些“小失误”不写解题思路代码题即使没完全bug-free但你在注释中写出了思路阅卷人还能给过程分什么注释都不写代码又有bug基本等同于零分只写答案不写原因选择题判断“对”或“错”之后最好补一句简单的原因说明方便阅卷人确认你不是蒙的忽略边界条件数组为空、对象为null、金额为负数、字符串为空串这些情况写代码时最容易漏掉。拿到代码题先问自己“这个函数的边界条件是什么”卷面代码格式混乱笔试中写代码要养成从第几行开始写、缩进统一、变量命名有意义的习惯。如果代码缩进乱成一团阅卷人很难有耐心看下去。10.3 针对有赞的专属准备清单结合有赞这家公司的业务特点和技术栈我最后给一份可直接执行的准备清单方便你考前一周对照自查JavaScript事件循环、Promise、async/await、this指向、闭包、原型链、深拷贝、防抖节流、数组去重浏览器缓存策略、输入URL到页面渲染的完整过程、重排重绘、性能优化指标框架React生命周期包含旧版、setState机制、组件通信方式Vue响应式原理、Vuex状态管理二选一或都准备工程化Webpack的loader和plugin原理、代码分割、体积优化、常见配置项数据结构和算法数组去重、两数之和、三数之和、手写深拷贝、对象扁平化、链表逆序、二叉树层序遍历业务设计优惠券组件、购物车设计、活动页配置系统、错误监控上报。按照这张清单复习再去应对有赞的笔试你会发现题目基本都在射程范围之内。10.4 笔试之后如何复盘和递进笔试结束后千万不要对完答案就丢掉。我的习惯是把每道错题按照“知识点、错误原因、正确解法、同类题目的规律”整理成一张复盘表。这个过程比多刷十道新题更有价值因为你是在修正认知盲区而不是在拓宽知识面。比如如果你错了事件循环的输出题不要归结为“我粗心”而是去深挖一下当时卡在哪一个环节是不知道await之后的代码何时执行还是不知道微任务队列怎么排序找到这个底层原因再找两三道同类题做练习巩固直到你闭着眼睛也能画出执行顺序图为止。这套从“应试”到“能力”的转化过程才是校招笔试对你职业生涯真正有用的地方。它不只是决定你能不能进面试还顺带帮你把前端工程师最核心的基本功系统重建了一遍。最后再分享一个我自己面试过很多候选人后得出的感受面试官真正想找的不是刷题机器而是能通过一套笔试表现出“我理解代码背后原理、我能处理边界场景、我有真实项目经验”的人。有赞2019校招前端笔试第一批当然有它的历史背景和具体题目但考察的底层能力放到今天依然是前端工程师最核心、最不该丢掉的那部分。希望这篇文章能让你少走一些弯路也祝你笔试顺利。