
最近在整理旧电脑里的面试资料翻出了一套用友2017年校招的web前端笔试题正好是第三套。顺手刷了一遍发现虽然过去几年了但里面不少考点放到现在的校招里依然高频出现——只是换了个问法内核完全没变。这篇文章就借着这套题把里面涉及的web前端核心知识点、答题思路和容易踩的坑都梳理一遍。不管是正在准备校招的应届生还是想查漏补缺的初级前端这份“回忆版题目完整解析扩展思考”应该都能给你一些实打实的帮助。1. 试卷整体解读为什么一套老题还有参考价值先说一个结论一套好的笔试题考的从来不是死记硬背而是你对一门语言的理解深度。用友这套题在当年属于比较典型的企业级前端笔试题它不是纯粹考“能不能写页面”而是考“你对JavaScript这门语言到底懂多少”。1.1 这套题的出题风格与考察侧重点整套题给我的感觉是基础题占了六成但基础题并不简单。所谓“不简单”是指它不会直接问你“什么是闭包”而是给你一段代码让你说输出结果再让你解释为什么。这种问法把“背概念”和“真理解”直接区分开了。考察侧重点主要有三个方向。第一个是JavaScript语言本身包括变量提升、作用域、this指向、闭包、原型链、异步执行顺序。这块是绝对的大头也是整张试卷里最拉分的地方——会的人几分钟写出来不会的人憋半小时都拿不准。第二个是浏览器相关的知识比如事件冒泡与捕获、localStorage与sessionStorage的区别、Ajax原理、跨域问题的常见解决方案。这些属于“前端工程师吃饭的家伙”校招笔试里如果不考反而不正常。第三个是基础算法与代码规范这部分占比不大但一定会有一两道比如数组去重、字符串翻转、数组拍平flatten这类题目。它不要求你用多高深的算法而是看你写下代码是否严谨、边界是否考虑到位。1.2 技术栈与作答策略分析为什么用友会出这类题目这跟它的业务有很大关系。用友是企业级管理软件的厂商面向的是大型客户前端要处理大量复杂的表单交互、数据展示和权限控制逻辑。这种场景下前端的数据流处理和异步控制能力就特别重要体现在笔试题里就是大量关于闭包、this指向和异步顺序的题目。我刚拿到这套题的时候先整体扫了一遍估算了一下难度和题量发现时间是相对紧凑的。如果磨磨蹭蹭在第一部分的选择题上纠结太久后面的手写代码题肯定来不及。所以我的建议是**先做会的、分值大的、有把握的再回头抠那些需要思考的。**这种策略在职场上其实也适用——先把核心功能稳定跑通再处理优化和边角问题。2. 核心考点拆解JavaScript基础深度解析先来看这套题里最有含金量的部分——JavaScript基础。这一板块的题目特别能检验一个人对这个语言的掌握程度是“会用”还是“理解”。2.1 变量提升与作用域的典型题目这类题目在考卷里出现频率极高基本逻辑是给一段“看起来很怪”的代码问你输出什么。我记得有一道题是这样的var a 1; function test() { console.log(a); var a 2; console.log(a); } test();答案是先输出undefined再输出2。核心考点就是变量提升hoisting。JavaScript引擎在执行代码之前会先进行一次编译阶段把var a这个声明提升到作用域顶部但赋值保持在原位置不动。所以这个函数实际等价于function test() { var a; // 声明被提升了 console.log(a); // undefined a 2; // 赋值还在原地 console.log(a); // 2 }从我面试别人的经验来看这道题的正确率还挺高的但很多人解释不清楚“为什么第一个是undefined”。如果你觉得“变量提升就是把声明挪到顶部”就够了那只能算懂了皮毛。真正的理解是要知道JS代码执行分两个阶段编译阶段创建变量环境、初始化声明和执行阶段逐行赋值、调用逻辑。顺带提一个当年这套题里给我印象很深的一道变体——用let代替varlet a 1; function test() { console.log(a); let a 2; } test();这就不是输出undefined了而是直接报错ReferenceError: Cannot access a before initialization。因为let声明的变量虽然也会提升但在初始化之前访问会进入“暂时性死区”TDZ——这个概念是ES6之后笔试的高频考点直到现在依然是校招的常客。2.2 闭包与异步顺序校招笔试永远的神闭包几乎是每一套前端笔试题里都跑不掉的题目。用友这套题也出了一道经典的循环和闭包结合的题目for (var i 0; i 5; i) { setTimeout(function() { console.log(i); }, 100); }这道题问输出什么。答案是五个5。为什么会这样因为var i是函数级作用域循环结束后i已经变成了5而setTimeout里的回调函数是在100毫秒之后才执行的那时候它读取i时的值就是5。要输出0 1 2 3 4当年标准答案是改用IIFE立即执行函数来闭包保存每次循环的i值for (var i 0; i 5; i) { (function(j) { setTimeout(function() { console.log(j); }, 100); })(i); }不过如果放到今天更推荐的解释是直接用letfor (let i 0; i 5; i) { setTimeout(function() { console.log(i); }, 100); }let在每次循环迭代时会重新绑定一个变量所以每个回调捕获到的是不同的i输出就是0 1 2 3 4。我当时复习这套题的时候顺手总结了一个“异步题”的通用解题思路先看执行栈里同步代码的执行顺序再考虑微任务和宏任务的入队顺序最后按事件循环Event Loop的规则取出来执行。这套思路到今天依然是分析前端异步题的核心方法论。3. 高频题型的深入拆解与答题模板除了纯语言基础题这套试卷里还有一些题目属于“套路题”——考点固定解题思路也相对模板化。这种题一旦掌握套路基本就是送分题。3.1 事件冒泡与捕获的经典问法这套题里有一道简答题问“如果点击了一个嵌套子元素事件在浏览器中是如何传播的”标准答案是先捕获capture阶段从window往下传播到目标元素然后进入目标阶段最后再冒泡bubble阶段从目标元素往上传播回window。但如果你只答到这一层那只能拿一半分。真正能拿高分的答案还需要补充说明两点。第一点是关于addEventListener的第三个参数。addEventListener(type, listener, useCapture)第三个参数默认是false代表在冒泡阶段触发事件处理函数设为true代表在捕获阶段触发。第二点是用实际场景解释捕获和冒泡的意义。比如“事件委托就是利用了事件冒泡的特性在父元素上统一处理子元素的事件从而减少内存消耗”。我当年答题时写了一个经典的事件委托例子document.getElementById(list).addEventListener(click, function(e) { if (e.target.tagName LI) { console.log(你点击了, e.target.textContent); } });这种写法在处理动态列表时特别有用——新增加子元素不需要重新绑定事件因为事件最终会冒泡到父元素上统一处理。用友这种企业级应用里列表、表格组件特别多事件委托几乎是标配操作。3.2 跨域问题的常见解决方案对比这道题在当年的试卷里是一道简答题JavaScript如何解决跨域请求问题这也是个延续至今的经典考题。我先整理一下当年标准答案会提到的几种方案JSONP利用script标签的src不受同源策略限制的特点通过动态创建script标签来加载接口数据。只支持GET请求适合简单的数据拉取场景。CORS通过服务端设置响应头Access-Control-Allow-Origin来允许跨域请求这是目前主流的跨域方案。反向代理开发环境下通过webpack dev server的proxy配置将请求转发到目标服务器从而绕过浏览器的同源策略限制。不过我想多说一点当年我在解析这道题时的体会——这几种方案不是“随便选一个”的关系而是有明确的适用场景。JSONP虽然古老但至今还有一些老的业务系统在用因为它兼容性好不支持CORS的浏览器也能跑。CORS是目前的主力开发的时候需要在服务端配合设置响应头如果是公司内部的API平台一般会统一配置好跨域白名单。反向代理则是开发环境的“救火队员”——配置一次整个开发期都舒坦很多。我见过不少应届生在简历里写“熟悉跨域解决方案”但笔试遇到这一题时只答出“JSONP和CORS”就草草收场。如果你真想拿高分建议把“各自优缺点为什么需要这么设计”写进去这就比只有名词解释的答案高一个档次。4. 手写代码题从题目要求到完整实现这套题的最后一部分是手写代码题要求不止“能跑”还要求考虑边界条件和代码的可读性。这类题目最能体现一个候选人的实际编码水平。4.1 数组去重的多种实现思路数组去重几乎是各家公司前端校招笔试的“保留曲目”。用友这套题里的版本是给定一个数组[1, 2, 2, 3, 4, 4, 5]写一个函数去重。最基础的写法是用一个空数组来存唯一值function unique(arr) { let result []; for (let i 0; i arr.length; i) { if (result.indexOf(arr[i]) -1) { result.push(arr[i]); } } return result; }这套写法逻辑清晰但效率不算高因为indexOf每次都要遍历一次新数组时间复杂度是O(n²)。如果想在笔试中给出更优的解法用ES6的Set一行搞定function unique(arr) { return [...new Set(arr)]; }但这里有个重要的注意点Set的去重是基于“严格相等”SameValueZero的所以它无法区分1和1也无法处理{}这类引用类型。如果题目要求保持元素的类型区分Set就不适用了。我当时解析这套题时额外扩展了一个场景如果数组元素是对象怎么根据对象的某个属性去重这是在实际业务中更常见的情况——比如接口返回了一组数据要按id字段去重。推荐用Map或一个reduce来实现function uniqueById(arr) { let map new Map(); for (let item of arr) { if (!map.has(item.id)) { map.set(item.id, item); } } return [...map.values()]; }这种面向真实业务场景的扩展在笔试里虽然不会直接出现在题目中但面试官很可能会在追问环节补一枪——你只背了一个Set去重被追问就露馅了。4.2 防抖与节流经典中的经典这套卷子最后有一道综合题要求实现一个防抖函数debounce。我先说白了吧——市面上稍微有点规模的笔试环节考手写防抖的概率极高。所以这道题一定要掌握得滚瓜烂熟。防抖函数的思路是当事件被连续触发时只有在最后一次触发后等待指定的时间间隔才执行真正的函数。如果在这个时间间隔内又触发了事件就重新计时。标准实现如下function debounce(fn, delay) { let timer null; return function(...args) { if (timer) { clearTimeout(timer); } timer setTimeout(() { fn.apply(this, args); }, delay); }; }写完这段之后还要注意一个细节在setTimeout的回调里使用fn.apply(this, args)是为了让防抖后执行的函数保持原来的this指向——这一步经常被忽略但在真实项目中一旦丢了this排查起来会非常痛苦。我当时复习这道题的时候还扩展了节流throttle的实现因为面试官很容易接着问“防抖和节流有什么区别各自用在什么场景”。节流是让函数在指定时间间隔内最多执行一次用时间戳版可以实现“第一次立即执行”的效果function throttle(fn, interval) { let last 0; return function(...args) { let now Date.now(); if (now - last interval) { last now; fn.apply(this, args); } }; }扩展两个使用场景防抖适合“输入框实时搜索”“窗口大小变化resize后重新计算布局”节流适合“滚动加载更多”“按钮防连点”。我当时把这两个场景写在答案最后作为补充说明后来复盘时觉得这个习惯的性价比特别高——它向面试官传递了一个信号你不只会背代码还懂业务里怎么用。5. 常见失分点与答题技巧实录分析完具体题目我还想专门分享一些我从这套题里总结出来的失分点。这些坑在别的家的笔试题里也普遍存在值得单独拉出来说。5.1 这道题明明会做为什么没得分第一种失分是“只写结论不写过程”。比如闭包那道题只写一个5 5 5 5 5就完事完全没有解释为什么。笔试改卷大概率是踩点给分尤其是简答题考的就是你对原理的呈现过程写不完整等于主动放弃一半分数。第二种失分是“手写题没用标准代码格式”。我记得有些同学的答案里连分号都没写缩进混乱变量命名随意——这在笔试里是会扣“代码风格分”的。面试官通过代码风格判断一个候选人的职业素养如果笔试代码都写得乱很难让人相信你在团队协作中能写出好维护的代码。第三种失分是“不了解浏览器的兼容性历史”。比如很多人在回答事件绑定时只写addEventListener完全没提IE旧版的attachEvent——虽然2017年的时候IE的问题已经不再那么致命但企业级应用往往还有老浏览器环境的兼容要求这个考点在当时依然是加分的。5.2 时间分配与检查策略的实战建议以这套题的题量来测算满分大概是100分建议分配如下选择题部分控制在15分钟以内简答题25分钟手写代码题20分钟最后留10分钟检查。检查环节最容易忽略的是“看题面关键词”。举个例子题目说“要求用原生JavaScript实现”就绝对不能答成jQuery或Vue的写法题目要求“不可以使用new Set”就老老实实用循环。我见过太多因为没注意到限制条件导致丢分的情况这不是不会做是真的可惜。还有一个小技巧是“答案里留注释”。手写代码题如果总体时间来得够可以在核心逻辑旁边加一行批注比如“这里用了缓存变量来避免重复计算”。批注不会影响得分但会让面试官觉得你的代码思维是经过推敲的给人一种“这人是按可维护的标准来写题”的印象。5.3 面试追问时的应对方法笔试交卷之后如果顺利进入面试环节面试官经常会拿笔试里的题目继续追问。比如你写了防抖函数面试官可能追问“如果希望第一次立即触发第二次开始防抖怎么改”这类追问的应对技巧我在复盘这套题时总结出了一个核心原则不要死记实现要记住每段代码的“可替换设计点”。防抖函数的可替换设计点是它的定时器逻辑——无论怎么改本质都是围绕“清空旧计时器、建立新计时器”来调整。你只要抓住这一点所有追问都能回到这个核心上自然发散。6. 这套题带来的校招复习启示把整套题拆解完我想站在一个过来人的角度聊聊这套老题对准备校招前端的你到底有哪些启示。6.1 基础永远是校招笔试的第一生产力我拿这套2017年的题和我前两年帮学弟看过的校招笔试题做了个对比发现一个很明显的趋势框架题变多了场景题变多了但JavaScript基础题的比例不降反升。别被新概念绕花了眼很多公司其实都明白框架半天能上手语言基础不是一天两天能补起来的。所以如果你现在时间有限我建议把复习重心按这个优先级排JavaScript核心机制闭包、this、作用域、原型链优先于框架原理浏览器与网络基础事件、请求、存储、渲染流程优先于各种打包工具手写常见工具函数防抖、节流、深拷贝、数组去重优先于刷超级难的算法题。6.2 手写代码应当形成肌肉记忆为什么反复强调手写代码因为在真实笔试中没有IDE的自动补全没有搜索引擎连代码提示都没有——只有笔和纸。这种状态下能流畅写完一个防抖靠的不是“记得大概逻辑”而是写过几十遍之后形成的肌肉记忆。我建议准备校招的同学每天专门抽出半小时做“手写练习”。不是打字是真的拿纸笔写代码。写数组去重、写深拷贝、写事件委托、写一个简单的Promise——这些题目练到不用思考就能动手才是真的达标。7. 最后一页几个值得反复品味的注意事项最后再顺手写三个我当时反复踩的坑希望你能绕过去。第一个坑只刷题不理解。刷题是战术上的勤奋但很容易掩盖战略上的懒惰。如果你连“这段代码里的this为什么指向window”都没搞明白刷一百道同类型的题也只是碰巧背会了答案。下次换个包装照样不会。第二个坑代码风格要提前养成。缩进、命名、分号——这些是习惯问题不是能力问题。习惯一旦养成就很难改与其到笔试前刻意调整不如平时写代码时就从“这行代码别人能不能看懂”出发去约束自己。第三个坑要充分了解目标公司。用友的企业性质决定了它不会考太偏门的东西它对前端的核心要求是稳、扎实、能处理复杂交互。每次笔试前花半小时看看这个公司的业务方向想想它的前端团队最看重什么能力再针对性地查漏补缺我试过几次效果比盲目刷一百道题要好得多。我是从2016年底开始准备校招的那时候刷题资料远没有现在丰富。回想起来这套2017年的题目其实是我在“用友校招前”等笔试通知时反复练过、总结过的最有代表性的一套题之一。它让我真正意识到笔试不是在考你会多少API而是考你在没有工具辅助的状态下对这门语言的核心机制理解得有多深。希望你刷完这套题之后也能有这样的体感——不是“噢我知道这道题选A”而是“现在我理解了为什么A是对的并且在其他类似场景里也能用上这套分析思路”。