有赞前端笔试题解析:JavaScript到工程化核心考点
2026/8/29 22:04:30 网站建设 项目流程

很多同学在准备校招笔试的时候,习惯性按“大厂题库”刷题,结果一到真正笔试就蒙了。原因很简单:头部互联网公司(尤其是做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/await

3.1 从一道Event Loop题目说起

有赞笔试题中,异步编程相关的输出题基本不会缺席。常见的形式是给出一串混有setTimeoutPromiseasync/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.allSettledPromise.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)),然后就被判错了,因为这种写法有一堆限制。笔试中,这些限制通常被包装成选择题或者判断题出现:

  • 值为undefinedfunctionSymbol的属性会被丢弃;
  • Date对象会被转成字符串,而不是Date类型;
  • RegExpError对象会被转成空对象{}
  • 嵌套循环引用会直接报错;
  • NaNInfinity会被转成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类型的键也不遗漏。这个版本虽然还有MapSet等类型没有处理,但作为笔试答案已经足够体现能力了。

4.3 深拷贝在业务中的真实用武之地

我一直强调,笔试题目不是死题,它们背后有真实业务场景。深拷贝在有赞这类SaaS公司里的典型应用场景是:购物车数据快照、表单编辑器撤销重做、以及运营后台组件的配置状态回滚。

比如用户在商城后台拖拽配置一个商品活动页,每一步操作都会改变组件的配置对象。为了支持“撤销”功能,我们需要在每次操作前保存一份配置快照,如果用户点撤销,就把当前配置替换成上一次的深拷贝结果。如果在这里只是做浅拷贝,那么快照和当前状态会共享底层对象,改当前配置会直接污染历史快照,整个撤销功能就废了。所以深拷贝不是“造轮子学概念”,而是实打实的功能基础。

5. 浏览器与网络:缓存策略、页面加载过程

5.1 浏览器的三层缓存机制

有赞的商城页面中对静态资源的加载速度要求很高,所以笔试针对浏览器缓存会考得很细。选择题出现频率最高的有三个:强缓存与协商缓存的区别、Cache-ControlExpires的优先级、ETagLast-Modified的优先级。

理解浏览器缓存,我建议用“过期时间”和“资源变更验证”这两条线索串起来:

  • 第一次请求资源时,服务器可以通过响应头告诉浏览器“你能缓存多久”。在缓存有效期内,浏览器直接从本地读资源,不再发请求,这就是强缓存,它靠Cache-Controlmax-ageExpires来标识。Cache-Control优先级高于Expires,因为Expires是绝对时间,容易受客户端本地时间修正影响,max-age是相对时间更可靠;
  • 当强缓存过期后,浏览器不是直接放弃缓存,而是带着缓存的标识(If-Modified-SinceIf-None-Match)去问服务器“这个资源有没有变化”。服务器返回304 Not Modified就会让浏览器继续用本地缓存,这就是协商缓存。ETag优先级高于Last-Modified,因为Last-Modified只能精确到秒,同一秒内多次修改文件时,可能误判为未修改。

实际操作中,静态资源的文件名带上hash值,配合Cache-Control: max-age=31536000来缓存,文件内容变化后文件名变化导致URL变化,浏览器自然请求新文件。这套方案在几乎所有中大型前端项目里都是标准做法,笔试时你可以直接写出来作为加分项。

5.2 从输入URL到页面渲染,讲清楚“浏览器做了什么”

这个题目太经典了,但很多人回答得很乱。有赞阅卷时最看重的,是你能不能按时间顺序把事情讲得有层次、不遗漏关键步骤。

我自己的回答框架,分为五个阶段:

  1. DNS解析:把域名解析成IP地址,优先查浏览器缓存、操作系统缓存,再到本地DNS服务器和根域名服务器逐级查询;
  2. 建立TCP连接:经过三次握手建立可靠的传输通道,如果是HTTPS,还需要进行TLS握手协商密钥;
  3. 发送HTTP请求并获取响应:浏览器构造请求报文,服务器处理后返回HTML文档和其他资源;
  4. 解析和渲染:HTML经过解析构建DOM树,CSS构建CSSOM树,两者合并生成渲染树,然后进行布局和绘制;
  5. 页面资源加载:HTML解析过程中遇到<script>标签会暂停解析,先下载并执行脚本,所以现在普遍在脚本标签上加deferasync

关于第四、第五阶段,有赞笔试可能还会追问“重排和重绘的区别”。重排是指布局变化触发重新计算几何位置,重绘是指像素级更新但布局不变。实际优化时,可以通过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只会加一次。正确做法是传函数给setState

this.setState(prevState => ({ count: prevState.count + 1 })); this.setState(prevState => ({ count: prevState.count + 1 }));

这个解法背后的原理是:React会把传入的函数排入更新队列,按顺序执行,前一个函数的结果会传给下一个函数。这不仅是一个API用法,更体现了对React更新调度机制的理解。面试官出这道题,其实是想看你是否真的调试过这类问题,而不只是查过文档。

6.3 Vue的响应式原理和组件通信选型

如果你用的是Vue,有赞笔试会考到Vue 2Object.definePropertyVue 3Proxy的区别。核心点有两个:一是defineProperty只能拦截对象已有属性的读写,新增或删除属性需要额外调用Vue.setVue.delete;二是Proxy可以拦截整个对象的13种操作,包括属性新增、属性删除、in操作符、Object.keys等,所以Vue 3不再需要特殊处理新增属性。

组件通信方面,有赞业务里最常见的场景是“父子组件传值”和“跨层级共享状态”。笔试中会让你手写组件间通信方式。统一的答案框架如下:

  • 父子通信:父传子用props,子传父用$emit触发事件;
  • 跨多级通信:先考虑provide/inject,适合祖先向后代注入依赖;
  • 复杂状态共享:使用VuexPinia做集中管理,避免$emit层层传递造成维护灾难。

同样的问题,如果你用的是React,对应答案就是props回调、Context、以及ReduxZustand。把这些核心方案答全,框架题目基本不会失分。

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,页面加载非常慢,请给出优化方案。这种题最怕空谈大道理,好一点的答法是给出可执行的排查链路:

  1. 先用webpack-bundle-analyzer插件生成依赖树分析图,观察到底是哪个包占用了大头;
  2. 如果是第三方库(比如momentlodashecharts)体积过大,考虑按需引入、替换成更轻量的库、或者用splitChunks拆出单独chunk;
  3. 如果是业务代码中引入了多余的模块,检查路由懒加载是否真正生效,防止把整棵component tree都打进首屏chunk;
  4. 开启Gzip压缩,服务端配合返回Content-Encoding: gzip,通常能再压掉60%以上;
  5. optimization.minimizer配置CSS和JS的压缩器,去掉注释和冗余代码。

这个回答顺序本身就呈现了一个完整的性能排查思路:先量化分析,再定位问题,最后验证效果。即使你没有任何调优经验,按照这条路径去说,阅卷人也会认定你有基本排查能力。

7.3 为什么需要代码分割和按需加载

代码分割(Code Splitting)在有赞这类中后台和商城项目中几乎是必做的优化。商城系统里,一个商品详情页同时要用到图片懒加载、视频播放、地图定位、优惠计算模块,如果把这些全部打进首屏,页面加载就完了。

实现代码分割的方式分两种:一种是多入口配置,把不同页面作为独立入口,互不影响;另一种是动态import(),在代码中按需加载。后者更灵活,也是路由懒加载的基础。在React中,配合React.lazySuspense,可以把某个路由对应的组件单独打包;在Vue中,则在路由配置里用() => import('./view/Home.vue')的形式声明异步组件。

笔试中有时候会让你解释“动态import为什么能够影响构建产物”。答案可以概括为:webpack在分析依赖时,遇到import()语句会把它当作分割点,自动生成独立的chunk文件,浏览器只在需要时请求该文件。理解这个机制之后,你看network面板里那些0.js1.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 拿到卷子后,先做这三件事

有赞笔试有一定题量,而且主观题需要花时间组织语言,所以时间管理显得格外重要。我建议拿到卷子后,不要从第一题开始顺序做,而是:

  1. 用2分钟把整张卷子快速浏览一遍,标记出自己最有把握的题和不确定的题;
  2. 按“先易后难”的原则做题:先做输出题和选择题,再做代码实现题,最后做开放性设计题;
  3. 给每道题设定好时间上限,一旦超时,先跳过做下一道,千万不要卡在一道题上超过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校招前端笔试(第一批)当然有它的历史背景和具体题目,但考察的底层能力,放到今天依然是前端工程师最核心、最不该丢掉的那部分。希望这篇文章能让你少走一些弯路,也祝你笔试顺利。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询