2019年那会儿,前端校招的笔试题目已经相当有区分度了。用友这套题我印象里不算偏怪,但覆盖面很扎实,属于那种“基础不牢就露馅”的类型。今天不聊空泛的面经,直接拿这套笔试题当切片,拆一拆它背后到底在考什么、为什么要这么考,以及如果你现在要准备类似的笔试,该往哪些方向使劲。
1. 整体设计与思路拆解:用友这套题到底在筛选什么样的人
先说结论:用友2019校招的前端笔试题(三),整体定位是“基础能力扫描+工程素养初筛”。它没有故意出偏题怪题,也没有追当时已经有点过热的新框架细节,而是把精力集中在计算机基础、JavaScript语言特性、浏览器工作原理、CSS布局能力和基础算法这五块。
为什么是这个组合?用友的业务线以企业级应用为主,这类产品对前端的要求不是“炫技”,而是稳定、可维护、团队协作时不容易踩坑。所以笔试环节重点看三件事:
- 候选人有没有扎实的语言基本功,尤其是JavaScript的核心机制是不是真懂。
- 能不能理解浏览器环境下页面是怎么渲染的,会不会处理异步、性能这类实际问题。
- 有没有基本的算法和逻辑建模能力,能不能用代码解决明确的问题。
这套题里还有一个隐藏的考察维度:审题能力。很多题目题干不长,但设置了边界条件,比如排序要“稳定”、去重要“保持顺序”。能不能快速、准确地提取这些约束,直接反映了以后接需求时会不会漏掉关键点。
对比当时其他大厂的笔试题,字节跳动和快手的题目更偏向算法和数据结构,出题风格接近LeetCode中高难度;阿里和腾讯更看重场景设计,会给你一个业务场景让你设计方案;用友的题则更接近“课本知识+常见实践”的组合,难度适中,但对基础概念的精确度要求很高。换句话说,这套题是给“基础扎实但还没刷太多算法题”的同学准备的,它更看重你是否能稳定输出可靠代码。
所以,如果你现在打算投企业服务类公司,或者想检验自己的前端基础是不是有漏洞,拿这套题当自测非常有价值。通过它你能快速定位出自己在JS语言机制、浏览器原理、CSS细节、编码规范这几个维度上的短板。
2. 核心考点解析:从题目回推前端必须掌握的知识体系
这套题覆盖的知识点如果归类,大致可以分为四个方向。每个方向我都会结合当年题目里常出现的形态,讲讲它背后的考察重点和应对思路。
2.1 JavaScript基础与手写代码:考察的是“你真的理解”而不是“你背过”
这一块在笔试题里占比最重。常见形态有:var和let的作用域差异、闭包的实际应用场景、原型链的查找过程、深浅拷贝的区别、this指向的几种绑定规则,以及手写实现防抖节流、数组去重、深拷贝、函数柯里化等函数。
当年这套题里有一道关于闭包的经典变形题,大致意思是:在for循环中用var声明变量并绑定事件,最终点击时输出的值是什么。很多人知道答案是“最后一次循环的值”,但题干再追问一句“怎么改才能得到预期值”,这里就分层了:只知道用let替换不够,你得说清楚let是创建了块级作用域,每次迭代都会绑定一个新的变量副本;如果面试官追问“不用let怎么实现”,你还得能写出用闭包IIFE传参的方案。
再比如手写深拷贝这道题,很多人都能写个递归,但只写递归是不够的。你得考虑循环引用问题,否则深拷贝一个包含环形引用的对象时会爆栈;你还得考虑Date、RegExp这类内置对象,普通复制只会得到空对象或字符串,丢失原型方法。更细致的考察点还包括:Symbol作为key时能不能遍历到、函数要不要特殊处理、原型链要不要保留。
我建议准备这类题目时,不要背答案,直接用Node把每一种边界情况跑一遍,亲眼看看结果,印象会深得多。
2.2 浏览器与网络基础:考察的是“页面性能的底层直觉”
这套题里浏览器相关的题目集中在几个方向:从输入URL到页面展示的完整过程、重排和重绘的区别及触发条件、HTTP缓存机制(强缓存和协商缓存)、HTTP和HTTPS的区别、TCP三次握手四次挥手。
从URL输入到页面展示这个题几乎是所有校招笔试的必考题,用友也不例外。这个题目表面考“流程”,其实考的是你有没有把“网络请求”和“页面解析渲染”这两个阶段完整串联起来。很多人写这个题只写到“拿到HTML然后渲染”,但漏了关键细节:DNS解析是有层级的,浏览器会先查本地缓存再逐级向上查询;TCP连接是三次握手,断开是四次挥手,为什么握手要三次而挥手要四次,这个“为什么”比“多少次”更重要;HTTP请求发出后,浏览器解析HTML时遇到CSS会阻塞渲染,遇到script标签会阻塞解析,所以才有CSS放头部、JS放底部的实践建议。
按顺序完整答出这些阶段,说明你有系统性的知识结构,而不是零散地看过几篇博客。重排和重绘也是常客。出题方式通常是给几个操作场景,问你哪些会触发重排、哪些只触发重绘。读offsetWidth、写style.width这些操作,前者会强制同步布局,后者如果前面攒了一批待处理的样式变更,再立刻读布局信息,浏览器会被迫提前做一次重排,这就是layout thrashing的原理。能答到这一层,基本就通过筛选了。
HTTP缓存题当年也出现过,通常给你一组请求头字段,让你判断命中强缓存还是协商缓存,以及服务端返回304后浏览器怎么处理。这里容易混淆的是:强缓存不需要发请求,直接从本地读取,对应Expires或Cache-Control的max-age;协商缓存一定有一次请求,如果资源没变,服务端返回304,浏览器再使用本地缓存。要注意Cache-Control优先级高于Expires,以及no-cache并不是“不缓存”,而是“每次使用前都要到服务端验证”。
2.3 CSS布局与样式细节:考察的是“页面还原能力”
CSS在笔试题中的呈现方式比较传统,但很能拉开差距。常见的有:flex布局的几种核心属性及其计算规则、水平垂直居中的实现方案、两栏和三栏布局的实现、盒模型(标准模型和IE模型的区别)、margin塌陷和BFC。
垂直居中这个题,面试官一般不会满足于你只会一种方案。你得至少掌握:flex方式(父元素display:flex,子元素margin:auto或父元素设置justify-content和align-items为center)、绝对定位加transform、grid布局、以及老式的table-cell方案。每种方案的适用场景和限制也要说得清楚,比如绝对定位的方案要求父元素有明确高度,transform方案在子元素有文字模糊的极少数情况下会有字体渲染问题。
两栏布局、三栏布局在当年也是高频题,虽然现在flex已经非常简单,但笔试还是会问。圣杯布局和双飞翼布局就是那个时代的产物,它们考查的核心是:浮动定位的原理、负margin的理解、以及如何让中间列优先渲染。虽然现在实际开发中已经很少手写这两种布局了,但理解它们的历史背景和原理,能让你更清楚flex和grid到底解决了什么问题。
margin塌陷和BFC这道题也几乎年年出现。父元素没有padding或border,子元素的margin-top会把父元素一起顶下来,这就是经典的塌陷。解决方法就是给父元素创建BFC:overflow:hidden、float、display:inline-block、position:absolute等都能触发。理解了BFC的规则,你就能解释清楚为什么overflow:hidden能解决这个问题:因为它创建了一个独立的渲染上下文,子元素的margin就不会溢出到外部。
2.4 框架与工程化基础:考察的是“开发效率意识”
2019年这个时间点,Vue和React已经是绝对主流,用友的笔试题里也会涉及框架层面的基础认知。不过这套题里的框架题难度不大,更多是概念性的:Vue中v-if和v-show的区别、Vue组件间通信的方式、computed和watch的区别、虚拟DOM的作用,React中state和props的区别、key的作用等。
v-if和v-show的区别是最基础的一类。v-if是惰性的,初始条件为false时元素不会被渲染,切换时会销毁和重建DOM;v-show则始终渲染,通过display:none来控制显隐。所以高频切换用v-show,低频、初始不展示用v-if,这是一条非常实用的经验。
computed和watch的区别则是高频进阶题。computed有缓存,只有依赖的响应式数据变化时才重新计算,适合“由一个状态推导出另一个状态”;watch是监听某个值的变化然后执行回调,适合“一个值变化后需要做异步操作或副作用”。很多人把这两个混用,其实设计理念完全不同。
虚拟DOM和key的作用也值得认真准备。虚拟DOM不是“比真实DOM操作快”这么简单,它的真正价值在于:用JS对象描述DOM结构,在状态变化后通过diff计算出最小更新范围,再批量更新真实DOM。key的作用是帮助diff算法识别哪些节点是复用的,如果用index做key,列表中间插入或删除元素时,容易导致组件状态错乱。这个考点直到今天依然是高频面试题。
工程化相关的题目,2019年还没有现在这么卷。一般会问你webpack构建的基本流程、loader和plugin的区别、常用loader有哪些、怎么做代码分割。现在再准备的话,还需要补充Vite的整体思路、ESM和CommonJS的区别等,因为工具链迭代太快了。
3. 实操过程与思路复盘:用真题视角拆解题思路
笔试不是面试,没有追问机会,所以答题方式很重要。下面拿几个经典类型的题目,走一遍完整的解题思路。
3.1 手写防抖函数:从基本实现到边界优化
当年笔试里经常出现类似“手写一个防抖函数”的题目。首先你要理解防抖的应用场景:搜索框输入联想、窗口resize事件、按钮点击防重复提交。防抖的核心思想是,事件触发后不立即执行,而是等待一个时间窗口,如果窗口内再次触发,就重置计时器。
基础版实现大概长这样:
function debounce(fn, delay = 300) { let timer = null; return function (...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; }这里有两个细节需要注意:第一个是this指向,如果内部直接用fn(args)而不是fn.apply(this, args),那么当事件处理函数里需要访问this时就会出问题。第二个是参数透传,用...args收集参数再透传给原函数,保证原函数能拿到事件对象。
如果你还想展示一点进阶能力,可以补充一个“立即执行”版本的防抖,即第一次触发时立即执行,之后在延迟窗口内的触发都被跳过:
function debounceImmediate(fn, delay = 300, immediate = true) { let timer = null; return function (...args) { const callNow = immediate && !timer; if (timer) clearTimeout(timer); timer = setTimeout(() => { timer = null; }, delay); if (callNow) fn.apply(this, args); }; }笔试时如果能写出带immediate选项的版本,明显比只写基础版更有竞争力。
3.2 数组去重:从暴力解法到多种边界方案
数组去重也是一道经典手写题。最直接的解法是用Set:
function unique(arr) { return [...new Set(arr)]; }一行代码看起来很漂亮,但笔试的边界条件往往会升级:如果要求去重后保持元素相对顺序?用Set天然满足。如果数组里包含对象,以某个属性为去重依据呢?这时候Set就不够用了,得用Map或对象做标记:
function uniqueBy(arr, key) { const seen = new Map(); return arr.filter(item => { if (!seen.has(item[key])) { seen.set(item[key], true); return true; } return false; }); }如果数组里有NaN,set的去重结果是能正确去重的,因为Set内部使用SameValueZero算法,但普通对象作为key时会有字符串化的坑。这些细节都能体现你是否真的遇到过这些问题。
3.3 经典排序:不只写出来,还要知道怎么选
排序题在笔试题里也很常见,常见要求是手写快速排序或归并排序。快速排序的普通实现很多,但要注意:如果每次递归都创建新数组,空间复杂度是O(n log n),原地排序的空间复杂度是O(log n)。笔试时最好写原地版本,不然面试官追问起来容易露怯。
一个常见考点是:sort函数默认是按字符串编码排序的,所以[10, 9, 8].sort()会得到[10, 8, 9],要传比较器函数。另外稳定排序这个概念,在企业级应用里很重要,比如表格先按时间排序,再按城市排序时,如果排序算法不稳定,相同城市的记录可能会打乱之前的时间顺序。
3.4 场景手写题:设计一个“带并发限制的异步调度器”
用友当年这类题虽然不常出现,但设计题能很好地体现工程能力。比如让你实现一个调度器,保证最多同时执行N个异步任务。这个题考的是Promise、队列和控制并发,实现思路大致如下:
class Scheduler { constructor(limit) { this.limit = limit; this.running = 0; this.queue = []; } add(task) { return new Promise(resolve => { this.queue.push({ task, resolve }); this.run(); }); } run() { while (this.running < this.limit && this.queue.length) { const { task, resolve } = this.queue.shift(); this.running++; Promise.resolve(task()).then(value => { resolve(value); }).finally(() => { this.running--; this.run(); }); } } }这里的关键是,每个add返回一个Promise,让外部可以await到任务真正完成;内部维护一个队列,把task和resolve包装在一起;当某个任务结束后,running减一,再自动从队列取出下一个任务执行。这样整个调度器就是一个“队列+水龙头”的模型,控制同时运行的数量。
4. 常见问题与排查技巧:这些坑几乎人人都踩过
4.1 拿到一道题,没思路怎么办
笔试时最怕的就是看到题目大脑空白。我自己踩过几次坑之后总结出的经验是,先看输入输出,确定边界条件,再想算法。如果算法一时想不出来,先写暴力解法,通过率和代码可读性都比空着不写强太多。很多笔试题是按测试用例得分,跑通几个算几个,先保底再优化。
4.2 代码能跑但题目要求“手写实现”怎么办
有些题目,比如new的实现、bind的实现、instanceof的实现,考查的就是你能不能脱离语法糖,从底层原理实现它。这时候只写const instance = new Foo()是不行的。你需要对JavaScript的底层机制有真正的理解,比如new的过程是创建一个新对象、把原型链指向构造函数的prototype、把构造函数的this绑定到新对象上、如果没有显式返回对象则返回新对象。
4.3 浏览器缓存题总是记混怎么办
我建议不要死记字段名,而是用“要不要发请求”作为分界线。先判断这个请求到底发没发出去:没发出去,就说明命中了强缓存,涉及Cache-Control/Expires;发出去了,再看服务端返回什么:如果返回304,说明命中了协商缓存,涉及ETag/If-None-Match、Last-Modified/If-Modified-Since;如果返回200,就是重新拉取资源了。这个思路在写题时非常好用,抽象成“是否发请求”这个维度,就不会混。
4.4 布局题怎么写才能让阅卷人觉得你熟练
首先是方案数量,至少两种以上;其次要写清楚每个关键属性的作用,不要只是堆代码;最后一定要说适用场景。比如flex适合一维布局,grid适合二维布局,float布局主要用在图文环绕场景。阅卷人看的是你“会选型”,而不是“会堆代码”。多写一点设计思路的说明,往往比代码本身更占优势。
4.5 时间分配与答题顺序
这也是一个非常实际的技巧。笔试时间有限,我一般建议先快速浏览一遍所有题目,做到心里有数;然后优先做会做的、能拿到分的题,不要在一道难题上死磕超过15分钟。手写代码的题,先把主流程写出来,再去补边界条件。如果最后还有时间,再回头优化。这里有一个很现实的问题,很多笔试题即使你答得不够完美,只要把思路写清楚,也会有不小的概率进入下一轮。写一点“思路是xxx,由于时间原因先这样实现”之类的注释,有时候会很管用。
4.6 不同公司的笔试风格差异
如果时间充裕,最好针对不同公司做差异化准备。用友这类企业服务公司更看重基础、规范和工程意识;互联网大厂更看重算法和系统设计;外企可能会考英文题目和更多数据结构题。你提前研究一下目标公司的历年真题,大概就能摸清出题风格。准备笔试不是把所有知识点平均用力,而是根据目标公司集中突破。
5. 后续可以怎么继续深挖这套题的价值
这套题虽然来自2019年,但它的知识点体系到现在依然没有过时。JavaScript核心机制、浏览器渲染原理、HTTP缓存、CSS布局、框架基础,这些依然是前端面试的核心范围。变化的是工具链和框架细节。
如果你刚做完这套题,我建议你做一次“错题归因”,不要只改答案,要把每道错题背后的知识点重新梳理一遍。具体操作是:准备一个错题本,写成“题目描述+我当时怎么想的+错在哪+正确思路+相关延伸知识点”的格式。这个习惯坚持下来,比反复刷三遍题都管用。
另一点是,把笔试里的手写题整理成一个独立清单,包含:防抖节流、深拷贝、Promise相关、数组去重、几种排序、new/bind/instanceof实现等。这个清单就是你的“考前冲刺手册”,面试前半小时过一遍,效果很明显。
这套题也让我想起一个当年一起准备校招的室友,他不刷题,而是把每一个知识点都推演一遍,能做到“看到题目知道出题人想考什么”。这种能力需要大量的积累才能练成,但只要从一套真题开始,一个知识点一个知识点地啃,你也能慢慢建立这种感觉。前端这个领域更新快,但底层知识反而是最保值的那部分投资,值得花时间把它打牢。