用友前端校招笔试题复盘:从JavaScript基础到Vue底层原理
前端校招的笔试题某种程度上比面试更能暴露一个人的基础功底。前段时间整理旧电脑里的资料翻出自己备考时专门留存的一套用友2018校招web前端笔试题四重做了一遍之后依然觉得这套题的考察风格很有代表性。它不追求偏题怪题而是把前端工程师日常工作中最需要稳定的那些知识点变换着角度反复敲打。对于准备传统软件企业和互联网公司前端岗位的同学来说这套题折射出的备考方向到现在看依然有参考价值。在真正展开复盘之前我先把这套题的整体观感说一下。用友的业务以to B为主前端日常要面对的更多是表单、审批流、报表、复杂列表这类高交互密度场景所以笔试不会花太多篇幅考花哨的CSS特效或者冷门框架API重心基本压在JavaScript基础、浏览器工作原理、DOM操作和框架底层逻辑上。题量不算小我当时做完大概花了70分钟左右其中选择题耗时最多编程题反而是拿分最稳的部分。这套题让我印象最深的是它把“基础”两个字考得很透。很多题目表面上在考一个API实际上是在考你对边界条件的理解。下面我按考点分类把当时印象比较深的几类题重新梳理一遍顺手把解题思路和容易踩的坑也写出来。1. 从整套试卷看用友前端校招的考点优先级和分值逻辑先看题型分布。这套试卷整体分成选择、填空、简答、编程四块其中选择题占了大头而且多选题的迷惑性很强。我当时做完之后粗略统计了一下各模块的分值权重大概是这样的题型分值占比侧重点单选题30%基础语法、浏览器API、常见方法的使用多选题20%边界条件、细节语义、框架底层机制填空题/简答题20%概念解释、输出结果、方案优劣对比编程题30%手写实现、算法思路、工程场景落地这个分值结构说明了一个问题笔试筛选的是“基础扎实、能直接干活”的人而不是“知道很多黑魔法”的人。用友这类企业级软件公司前端要处理大量数据录入和交互逻辑如果候选人连闭包、事件循环、数组方法的边界都没吃透即使会用框架上线后也容易埋坑。所以单选题考细节点多选题考边界点简答考语言组织能力编程考动手能力四个维度正好把前端工程师的基础素养筛了一遍。我当时做完这套题之后有一个感受选择题的陷阱并不在“这个API是什么”而在“这个API在这种情况下会返回什么”。比如有一道多选题问哪些操作会导致浏览器回流选项里同时出现了“修改DOM样式”“读取offsetWidth”“添加class”“修改transform”如果不清楚回流和重绘的触发条件很容易少选或多选。这类题没有捷径只有把浏览器渲染机制梳理成自己的知识体系考场上才能快速判断。另外这套题里有几道填空题看起来只是让填一个API名称实际上是在考记忆的精确度。比如让写“怎么将一个类数组对象转成真正的数组”这题有多个答案Array.prototype.slice.call、Array.from、扩展运算符但不同答案在不同场景下是有差异的。填空题的好处是它逼着你对API的理解足够精确而不是看到四个选项能认出哪个对就行。这类题给我的提醒是刷题不能只看选择题一定要自己动手写一遍答案能写出来和能选出来是两种完全不同的掌握程度。还有一个值得注意的点整套试卷没有出现特别复杂的算法题编程题更多是“用代码解决一个明确的工程问题”。这和互联网大厂的高强度算法考察不同更贴近业务开发的实际需求。后面我会专门拆编程题的复盘过程这里先不做展开。2. JavaScript基础选择题的失分点作用域、闭包、异步和执行上下文这套笔试的选择题里JavaScript基础部分占的比重最大而其中反复出现的核心考点就是那几块作用域与提升、闭包与内存、this指向、事件循环。下面我按题目类型详细剖开讲。2.1 循环中的闭包陷阱进阶版本的结果预判这是我印象最深的一道题当时看到题干就笑了因为这种题在校招笔试里太经典了。题目大概长这样var arr []; for (var i 0; i 3; i) { arr[i] function () { console.log(i); }; } arr[0](); arr[1](); arr[2]();答案是 3、3、3。原因是var声明的i是函数级作用域循环结束时i已经变成3三个函数引用的是同一个i变量。这道题的变形也很多比如把console.log(i)改成console.log(i * i)或者把var改成let输出就变成0、1、2因为let每次循环都会创建一个新的绑定。用友这道题给了一个更深的追问如果必须使用var怎么改成期望的输出0、1、2这就涉及闭包的经典解法了。最简单的办法是用立即执行函数包一层把每次循环的i作为参数传进去var arr []; for (var i 0; i 3; i) { (function (n) { arr[n] function () { console.log(n); }; })(i); }另一种思路是用bind绑定参数var arr []; for (var i 0; i 3; i) { arr[i] function (n) { console.log(n); }.bind(null, i); }这个考点为什么重要因为在真实业务里我们经常在循环里给一组按钮绑定点击事件如果直接用var声明循环变量点击后拿到的永远是最后一个值。很多同学在项目里见过这个问题但不一定知道根因是作用域共享笔试考的就是你能不能把根因讲清楚而不是背答案。2.2 this指向的判断普通函数、箭头函数和apply方法选择题里有一道很具迷惑性的this指向题。我简化一下var name window; var obj { name: obj, fn: function () { console.log(this.name); }, arrowFn: () { console.log(this.name); } }; obj.fn(); obj.arrowFn();答案依次是obj和window。普通函数的this由调用位置决定obj.fn()调用时this指向obj箭头函数的this在定义时继承外层作用域也就是全局对象所以输出window。这道题还进一步变形成var fn obj.fn; fn();这时候this指向变成了window输出window因为fn被赋值后调用时的位置已经脱离了obj的上下文。这种“把方法拆出来调用”的场景在项目中非常常见比如把组件里的方法赋值给一个变量然后单独调用很容器踩到this丢失的坑。我当时备考总结的经验是判断this只有一个主线先看是不是箭头函数是的话看定义环境不是的话就看函数被谁调用了谁调用this就指向谁。只要这条路不混乱大部分选择题都能做对。2.3 异步输出顺序宏任务和微任务的执行时机这套题里有一道关于事件循环的输出题代码大概是这样的console.log(start); setTimeout(() { console.log(timeout); }, 0); Promise.resolve().then(() { console.log(promise); }); console.log(end);输出顺序是 start、end、promise、timeout。原因是同步代码先执行微任务队列在同步代码结束后立即清空宏任务在下一轮事件循环中执行。Promise.then属于微任务setTimeout属于宏任务所以Promise的回调虽然是在后面注册的却会比setTimeout先执行。这道题的干扰点在于很多人会认为setTimeout延迟是0就立刻执行其实它的最小延迟时间是被浏览器强制的而且即使不考虑延迟宏任务的执行时机也永远在微任务之后。我在复盘时习惯把事件循环画成一条线同步代码 → 微任务队列 → 宏任务队列每一步都对应具体的输出。脑子里有这条线之后类似的输出题基本都能拿下。2.4 数组方法的隐形坑map跳过空位与reduce初始值有一道多选题问下列哪些数组方法的回调不会被稀疏数组的空位触发。选项里有map、forEach、filter、find。答案是map、forEach、filter都会跳过空位find不会它会遍历包括空位在内的所有索引。这个问题在业务里不常见因为正常情况下我们很少手动创建稀疏数组但笔试考的是对数组方法论的理解深度。还有一题问reduce如果没有传递初始值第一次回调的prev是什么。答案是数组的第一个元素current是第二个元素。很多同学记得这句话但不知道如果数组为空且没传初始值reduce会抛TypeError。这种边界条件就是多选题最喜欢的陷阱它把一个API的常规用法和边界行为放在一起考察你是否真正使用过这个API而不是背过文档。3. DOM 与浏览器机制题越“基础”的题目越容易暴露细节盲区DOM操作是前端日常工作中绕不开的内容但这套笔试里的DOM题并不是简单问“getElementById怎么用”而是把事件机制和渲染性能结合起来考。这部分我认为是整套试卷里区分度最高的模块之一。3.1 事件流的三个阶段与事件委托的写法有一道简答题要求描述事件传播的三个阶段并且要说明为什么利用事件委托可以优化性能。三个阶段的标准答案是捕获阶段、目标阶段、冒泡阶段。addEventListener的第三个参数传true表示监听捕获阶段默认false表示监听冒泡阶段。事件的起点是window一路向下到目标元素再一路向上回到window。事件委托的原理是利用事件冒泡把子元素的事件处理函数绑定在父元素上通过e.target判断具体触发元素。我当时在试卷上写的示例是这样的document.getElementById(list).addEventListener(click, function (e) { var target e.target; if (target target.nodeName LI) { console.log(target.textContent); } });为什么要用事件委托两个核心原因一是减少事件绑定的数量如果列表有1000个li每个都绑定click事件会创建大量函数对象内存占用高二是动态插入的子元素不需要重新绑定事件天然支持动态列表。这两个优势在to B业务里非常实用因为审批流、任务列表这类界面经常需要动态渲染数据。这道题还有一个变体如果子元素内部还有span、a等其他标签直接判断e.target.nodeName可能会失效因为用户点击的是span而不是li。正确的做法是用closest方法往上找最近的livar li e.target.closest(li); if (li list.contains(li)) { console.log(li.textContent); }这个细节是我后来在实战中才发现并补充的笔试当时只写到了nodeName那一步。复盘时我把两种写法都整理进了笔记因为真实场景里嵌套标签的概率非常高。3.2 回流与重绘哪些操作会触发哪些不会选择题里最典型的一道是问下列哪些操作会导致浏览器回流。选项包括修改元素的width、读取offsetHeight、给元素添加class、修改transform、修改color。答案是修改width、读取offsetHeight、添加class修改transform和修改color不会触发布局计算。这里的关键认知是回流一定引起重绘重绘不一定引起回流。修改color只触发重绘因为元素的位置和大小没变浏览器只需要更新视觉样式修改transform会触发合成器的图层变换不会干扰正常文档流所以也不会引起回流。而读取offsetHeight这类属性看起来只是读取但浏览器为了保证数值准确会强制同步计算布局所以也会引发回流。我在项目里优化长列表时就是踩了“读offsetWidth”的坑。写了一个自适应布局的组件每次在循环里读offsetWidth再设置另一个元素的宽度导致页面交互明显卡顿。后来改成用ResizeObserver监听容器或者把读写操作分开让浏览器有机会批量处理布局性能才正常。这个经验在面试里也很加分笔试答完回流题之后如果能再聊一个真实优化案例会显得理解更扎实。3.3 本地存储三兄弟cookie、localStorage、sessionStorage这张存储对比表是简答题里出现过的我到现在都觉得很值得整理成口诀类型容量生命周期请求携带适用场景cookie4KB左右可设置过期时间每次请求自动携带会话标识、登录态localStorage5MB左右永久保存不携带本地缓存、偏好设置sessionStorage5MB左右标签页关闭即清除不携带表单暂存、单页会话cookie的请求携带特性是它的最大优势也是最大负担。优势是http请求时会自动带上配合服务端session实现登录态负担是每次请求都会增加额外流量所以不应该在cookie里塞大块数据。localStorage是同步API写入大量数据时可能阻塞主线程这也是一个隐藏考点如果用localStorage存一个几十MB的字符串页面可能会卡顿甚至崩掉因为主线程要花时间做序列化和写入。sessionStorage有一个容易被忽略的特性如果用户复制标签页新标签页并不会复制sessionStorage的内容但如果是浏览器崩溃后恢复标签页sessionStorage会被恢复。这个细节当年我面试时被追问过虽然笔试没考但建议准备校招的同学顺手了解一下属于“多说一句能加分”的储备。4. 框架选择题的底层逻辑Vue 二版响应式原理、Diff 和生命周期细节用友2018年那会儿校招笔试里Vue是主流考察对象React会出现在选择题里做对比代码题基本是Vue为主。这套试卷的框架题不算难但没有一道是直接问“Vue生命周期有哪些”而是把生命周期、响应式原理和实际使用场景粘在一起考。这种出题方式恰恰是真实开发中会遇到的思维模式。4.1 Vue 2 响应式原理的边界为什么动态添加属性不生效有一道多选题问在Vue 2中以下哪些方式可以给data对象新增一个响应式属性选项有直接赋值obj.newProp 1、使用this.$set(obj, newProp, 1)、使用Object.assign、使用重新赋值整个对象。答案是用this.$set和重新赋值整个对象。直接赋值不行因为Vue 2的响应式系统通过Object.defineProperty在初始化时遍历data中的每个属性把它们的getter和setter重写了。新属性在初始化时不存在自然不会经过这个处理后续赋值也无法触发视图更新。Object.assign那个选项也有迷惑性Object.assign(obj, { newProp: 1 })并不会让newProp变成响应式的因为Object.assign只是复制属性并不会走Vue的响应式拦截。正确的做法是把Object.assign的结果重新赋给data里的字段this.obj Object.assign({}, this.obj, { newProp: 1 });这样整个对象被替换Vue检测到引用变化会重新对新对象的属性做响应式处理。这个知识点放到现在的角度来看已经属于Vue 2的经典“坑”了但在2018年的笔试里是非常核心的考点。如果当年的候选人能同时说出Vue 3用Proxy解决了这个问题并且解释Proxy可以代理整个对象而不是单个属性那这题的区分度会非常明显。4.2 computed、watch、methods三兄弟的定位差异这种题在多选题里出现率极高我当时遇到的变体是问“哪些说法是正确的”。我直接总结成一张表写在这里类型触发时机缓存适用场景computed依赖的响应式数据变化有缓存派生状态比如计算结果、过滤列表watch监听的源变化无缓存异步操作、开销较大的操作、同步逻辑methods每次调用都执行无缓存事件处理、纯函数调用computed和watch怎么选是Vue面试题里必问的一道。最简单的原则是能写成computed的就不要用watch。因为computed有缓存依赖不变不会重新计算性能上更优而且computed的语义是“声明一个派生值”代码更易读。watch适合处理“变化之后需要做一件事”的场景比如搜索关键词变化后发起异步请求、路由参数变化后重新拉取数据、表单数据变化后做校验。笔试里经常出现“根据a和b计算总价”的题正确答案几乎都是computed因为总价是根据依赖自动派生的状态不是需要手动触发的一段逻辑。4.3 key 的作用和 Diff 算法的关系有一道简答题问v-for渲染列表时key的作用是什么如果不写key会有什么影响标准回答是key给每个虚拟节点一个唯一标识帮助Vue在diff过程中判断节点是复用还是重建。没有key时Vue会采用就地复用策略对列表中间插入一条数据可能导致后续DOM元素状态错乱。我当时在卷子上写了一个经典的坑例子列表项是一个输入框你会输入内容然后用v-for顺序渲染。如果给input添加了默认值当列表数据顺序变化时输入框内的值可能会串到其他行。原因就是diff时Vue认为节点类型相同直接复用了DOM没有为每个节点建立正确的对应关系。加一个唯一且稳定的key就能让Vue按key精确匹配节点避免状态错乱。key真正最忌讳的是用index。虽然index在大多数静态列表里没有问题列表更新也不会出现视觉异常但一旦列表支持删除、排序、过滤用index当key就会让节点复用错乱。最好的key是业务数据里天然唯一的字段比如id、编号。4.4 nextTick 和生命周期里的异步更新这道题也是选择题里的常客修改响应式数据后立刻读取DOM为什么拿到的还是旧值原因是Vue的DOM更新是异步的。数据变化后Vue不会立即操作DOM而是把更新任务放到一个队列里在下一个tick统一执行。这样做是为了避免同一事件循环中多次修改数据时频繁操作DOM提升性能。如果要拿到更新后的DOM有两个办法一是用this.$nextTick回调二是用Vue.nextTick在组件外使用。这两个方法本质一样都是回调在DOM更新完成后执行。我在写项目时经常在修改数据后立刻操作DOM比如弹窗打开后设置焦点不写nextTick就会失效。这个经验在笔试里也很适用因为选择题经常会问“哪个API可以在DOM更新后执行回调”。Vue 2的生命周期题也喜欢和nextTick结合。比如mounted里修改了数据updated不会立即触发而是要等DOM更新后才会触发。如果候选人能把异步更新队列和生命周期的关系讲清楚简答题的得分会明显高一档。5. 编程实现题复盘从题意拆解到边界处理的完整答题思路这套笔试的编程题整体不难但有几个要求让我印象深刻代码要能直接运行、需要处理明显边界情况、不能用ES5以外的语法后面这道题其实有点老了但2025年的读者可以把它理解成“尽量用基础语法实现”。我选了三道最有代表性的题讲一讲完整的解题过程和我在复盘时补充的优化思路。5.1 手写防抖函数场景理解比代码本身重要题目描述很简短实现一个防抖函数debounce使得传入的函数在连续触发时只执行最后一次。我当时的解法是function debounce(fn, delay) { var timer null; return function () { var context this; var args arguments; clearTimeout(timer); timer setTimeout(function () { fn.apply(context, args); }, delay); }; }这段代码里需要完整保留this和arguments因为setTimeout回调里的this指向window如果直接fn(args)this就丢了。虽然很多场景下防抖函数内部不需要使用this但作为通用工具函数保留this是专业性的体现。后来我在项目里发现防抖还有一个细节第一次触发时是应该延迟执行还是立即执行比如搜索框场景用户输入第一个字时候选列表先出来后面连续输入时不频繁请求这种需求要用“立即执行节流”的思路。我一般把防抖区分成两种延迟执行版和立即执行版。上面这个是延迟版立即版是第一次触发时立即调用然后开始计时计时结束前再次触发不会执行。两种写法在面试中都很常考建议都背熟。5.2 深拷贝从“能用”到“不炸”的边界演进笔试题原题是实现一个函数对普通对象进行深拷贝。这个要求可以很简单function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); }但这种写法在遇到function、undefined、Date、RegExp、循环引用时会出问题所以笔试的加分点在于能不能写出递归版本。我的完整答案是function deepClone(source, map) { if (typeof source ! object || source null) { return source; } if (source instanceof Date) { return new Date(source.getTime()); } if (source instanceof RegExp) { return new RegExp(source.source, source.flags); } if (Array.isArray(source)) { map map || new WeakMap(); var result []; map.set(source, result); for (var i 0; i source.length; i) { result[i] deepClone(source[i], map); } return result; } map map || new WeakMap(); if (map.has(source)) { return map.get(source); } var keys Object.keys(source); var cloned {}; map.set(source, cloned); for (var j 0; j keys.length; j) { cloned[keys[j]] deepClone(source[keys[j]], map); } return cloned; }这里最核心的优化点是循环引用处理。如果对象里有属性指向自身不处理循环引用的话递归会无限栈溢出。WeakMap的作用是把“源对象”和“克隆对象”的映射关系存起来遇到已经拷贝过的对象直接返回映射结果相当于给递归加了一个记忆。笔试考深拷贝考察点其实在“对象类型判断”和“递归终止条件”这两件事上。第一版先判断基本类型第二版判断Date和RegExp第三版处理数组和循环引用第四版可以用Reflect.ownKeys处理symbol键名。面试中能一步步演进比一次性写出最终版的效果好得多。5.3 数组去重时间和空间复杂度权衡数组去重是编程题里的元老级选手这套试卷里的题目是给一个数字数组返回去重后的数组。最简单的写法是function unique(arr) { return Array.from(new Set(arr)); }但试卷要求手写实现禁止用Set这就需要另想办法。我用的是空间换时间的方案利用对象键名唯一的特性function unique(arr) { var map {}; var result []; for (var i 0; i arr.length; i) { var item arr[i]; if (!map[item]) { map[item] true; result.push(item); } } return result; }这个方案的问题也很明显如果数组里同时有数字1和字符串1它们会被当成同一个键导致误去重。所以更稳妥的方案是使用Mapfunction unique(arr) { var map new Map(); var result []; for (var i 0; i arr.length; i) { if (!map.has(arr[i])) { map.set(arr[i], true); result.push(arr[i]); } } return result; }笔试如果只要求去重写Set版本就够了如果想展示思考深度可以补充说明Map和Object的差异。这个扩展点能体现出你实际写过代码而不是背了几个API。5.4 二分查找模板化写法与临界值处理这道编程题要求在一个有序数组中查找目标值返回下标找不到返回-1。平时业务里大家可能直接用indexOf但笔试考的是算法基础。我当时写的版本是function binarySearch(arr, target) { var left 0; var right arr.length - 1; while (left right) { var mid Math.floor((left right) / 2); if (arr[mid] target) { return mid; } else if (arr[mid] target) { left mid 1; } else { right mid - 1; } } return -1; }这里最容易犯的错是while条件写成left right导致当数组只有一个元素且等于target时直接跳出循环返回-1。另一个容易出错的是mid的计算方式如果直接写(left right) / 2在数字很大的时候可能超出安全整数范围更稳妥的写法是left Math.floor((right - left) / 2)。这道题虽然简单但它考察的是基础思路是否清晰每次比较都能排除一半数据复杂度是O(log n)。如果候选人能在代码里加上注释解释边界条件的含义即使运行结果不完全正确阅卷者也会认为思路是通的。6. 从这套题反推出来的前端校招备考清单试卷复盘到这里已经覆盖了大部分考点。最后这部分我想换个角度从我自己的备考经历出发说说做完这套题之后总结出来的复习框架以及临场答题的一些技巧。这些内容对准备校招的同学来说可能比题目本身更有实用价值。第一我认为备考前端校招不能只刷选择题。选择题做多了容易形成“眼熟错觉”看到选项觉得眼熟就以为自己会了实际在纸上默写的时候才发现写不出来。当时我给自己定了一个规则每道复习过的知识点必须能在白纸上画出代码结构图。比如事件循环我要求自己不看资料就能写出“同步代码、微任务、宏任务”的执行顺序示例比如闭包我要求自己默写出一个能说明问题的最小代码。通过这种方式才能在笔试时真正写出正确的结果。第二手写代码题不能只在编辑器里练。我发现一个很现实的问题在IDE里写代码会有智能提示、自动补全、语法高亮一旦切换到笔试场景这些辅助全部消失很多API的名字会突然想不起来。我当时练习的方式是用一个空文本文件手动敲完整个函数然后放到浏览器控制台里跑测试。刚开始很痛苦比如Array.prototype.slice.call和Array.from的区别我分不清哪个优先用哪个但练多了之后这类API就形成了肌肉记忆。第三时间分配很重要。这套笔试题我给自己打了个分基础选择题错了两道一道是事件委托里nodeName误判另一道是数组reduce边界条件编程题拿到了基本分。整体来看选择题的时间成本最高因为多选题每多选一个或少选一个都可能导致整题扣分。我的经验是单选题控制在1分钟内多选题控制在2分钟内遇到拿不准的先标记跳过最后留15分钟回来复查。这个时间分配对类似用友这套题风格的试卷非常有效。第四简答题一定要写“分点示例代码”。这一点在校招笔试中特别关键。阅卷的人一天要批大量卷子如果答案是一大段文字很难快速定位到得分点。我通常采取“结论先行、理由分条、代码补充”的模式。比如问“为什么用事件委托”我先写“为了减少事件绑定数量并支持动态元素”再分别展开两条理由最后附一段代码。这种结构化表达不仅方便阅卷也逼着自己把思路理清楚。第五一定要重视框架底层原理不能停留在API调用层。这套卷子虽然不难但对于只会写模板、不理解响应式原理的候选人来说多选题很可能栽跟头。备考时我建议把Vue 2的响应式原理、生命周期、nextTick、diff过程当成必背知识点不仅要记住概念还要能讲出“为什么”。只有理解了一个API存在的意义遇到变形题才能举一反三。这套题做下来我的感觉是它没有一道题是真正意义上的难题但每一道题都在检验你平时编码时有没有留意边界、有没有思考过底层。如今再翻出来复盘很多当年的知识点已经被新框架替代但复习过程中建立起来的那套“追问为什么”的方法才是最有价值的东西。如果你正在准备前端校招与其去背海量题库不如找一套像这样的题从选择题到编程题完整做一遍再逐题深挖背后的原理收获会比刷题量大得多。

相关新闻