
每一年的校招季翻出来看看前端岗位的笔试题风格都会有些变化但有一类题目哪怕过了好几年拿出来再看依然觉得“真敢考”。用友2017校招web前端笔试题五就是这么一套。我印象里这套题在应届生里流传挺广不是因为题目有多偏恰恰相反它考察的全是前端最容易被忽视的底层基本功JS运行机制、原型链、作用域、手写代码的严谨度。这些知识点放在当时是标准考法放到现在依然是面试必问题。如果你正打算走web前端开发这条路或者已经在准备校招笔试这套题的复盘价值不低。它能帮你看清楚一个老牌企业在前端岗位上真正想要什么样的人不是会用几个框架就行而是基础扎实、写代码有逻辑、遇到边界情况能想明白。这篇文章我会从考试逻辑、考点拆解、手写题技巧、现场答题策略以及复习路线几个方面把关于这类笔试的经验一次讲透。1. 开始之前这套笔试题的基本盘1.1 2017年的前端笔试题为什么放到现在还有参考价值2017年那会儿Vue和React已经火了一轮但很多企业级软件厂商的核心产品还跑在jQuery和原生JS上。用友作为老牌企业管理软件厂商技术栈一向偏“稳”笔试题目不会追新但会很认真地考察候选人能不能在没有框架的情况下把JS写明白。这也决定了这套笔试题的调性不考什么新语法、新特性反而重点放在类型转换、闭包、this指向、事件循环、原型链这些“陈年考点”上。如果你只学过框架没正经读过JS红宝书做这套题会明显吃力。反过来如果你把这些基础搞透了放到今天去面任何中大型公司的前端岗一样吃得开。我后来带过不少新人发现一个规律框架可以速成但基础很难补。那些当年笔试能高分的人不是因为背题多而是基本概念形成了体系。这也是为什么我现在还愿意把这套老题拿出来讲的原因——web前端开发这个行业迭代快但底层逻辑一直没变。1.2 一套校招笔试题的命制逻辑你可能会好奇用友这类企业出题的时候背后到底在筛选什么样的人。我结合这套题和同期的几套试卷分析过它们的结构大体是固定的分为四个层次基础选择题/判断题覆盖HTML、CSS、JS基础语法主要过滤完全没学过的人。代码输出题给一段代码让你写输出考察运行机制、作用域、异步等关键概念。手写代码题数组去重、防抖节流、深拷贝之类的常见封装考察基本功是否扎实。简答/应用题比如页面性能优化、跨域方案、浏览器渲染流程考察知识面和工程思维。这套“由浅入深、从记忆到应用”的命制逻辑本质上是在区分两类人一类是背过知识点但不会用的一类是真正理解并动手写过代码的。你现在刷题的时候也可以按这个逻辑自查——看到一道题先问问自己这题考的是“我记过”还是“我理解过”1.3 这套题的整体难度定位从难度上看用友2017校招web前端笔试题五属于中规中矩偏上。它没有大厂那么爱抠边界条件但也不是随便背点面试题就能过的水平。举个例子基础题里会有一类很经典的“阴沟翻船”题var a 0; function test() { console.log(a); var a 10; } test();输出是什么新手大概率会答0因为直觉上觉得打印的时候 a 还是全局的0。但实际是undefined——因为函数内的var a会被提升到函数顶部赋值留在原处。这就是经典的作用域与变量提升考点。这套卷子里类似的题不少凡是输出判断类题目几乎都藏着类似的小陷阱。所以我说这套题的难度不在于“刁钻”而在于“细致”。它考察的是你写代码时有没有真正注意过这些细节而不是靠背题能混过去的。2. 考点逐项拆解真正的重头戏都在JS里2.1 作用域、变量提升与闭包笔试翻车重灾区打开这套题三分之一以上的分数都压在JS基础运行机制上。其中作用域和变量提升又是最容易被出卷人拿来“坑”人的点。先看一个典型的组合题套路for (var i 0; i 3; i) { setTimeout(function () { console.log(i); }, 0); }这个结果是多少如果平时只是照着React写业务代码很少碰到底层机制很容易写错成0, 1, 2。但事实上var声明的 i 是函数级作用域循环结束后 i 已经变成了 3而三个定时器的回调都引用了同一个 i所以输出的是3, 3, 3。这个题背后有两个知识点一个是作用域另一个是异步回调的执行时机。如果面试官想加大难度会再追问“怎么改成输出0、1、2”这就引出了闭包方案、let方案、bind参数方案三种解法。我在复习的时候会把闭包单独拉出来梳理因为它不是背定义就能会的。网上很多人把闭包解释成“函数里返回函数”这个说法太粗糙了。更准确的理解是闭包是函数与其声明时所在作用域的组合。只要一个函数被定义在某个作用域内并且在外部被引用它就带着整个作用域的“记忆”一起走了。笔试里闭包最常见的考法是“防抖节流手写”以及“循环里创建闭包输出问题”。这两个题型在这套卷子里都属于高分值题后面我会单独展开讲。2.2 数据类型与类型转换永远绕不开的“”噩梦2017年的笔试特别喜欢考JS类型转换因为这是新手最薄弱的一环。用友这套题也不例外几道选择题基本都围绕typeof、和的区别展开。有些结论你如果只是死记硬背很容易在做题的时候卡住。比如console.log([] ![]);直觉上肯定觉得是false因为数组和取反后的数组怎么会相等但实际输出是true。原因很简单![]会先转成布尔值空数组转布尔是true取反后是false。然后[] false两边都会转成数字比较[]转数字是0false转数字也是0所以结果是true。这种题在真实笔试里得分率极低。不是因为难而是大部分人压根没系统梳理过JS类型转换的规则。我后来复盘时整理了一张速查表基本覆盖了这套题里可能出现的所有类型转换场景表达式结果原因[] falsetrue双方转数字为0null undefinedtrue特殊规则null undefinedfalse类型不同 0true字符串转数字0 falsetrue两边都转数字为0NaN NaNfalseNaN不等于任何值typeof nullobject历史遗留bug遇到这类题我建议你养成一个习惯先看两侧类型是否相同类型不同才可能存在隐式转换如果一眼看不出结果就在草稿纸上把每一步转换拆开写。这套题里凡是类型转换相关的题都可以用这个方法解决。2.3 原型与原型链对象之间的隐形桥用友这套笔试题里原型链的题出得很有代表性。它不是让你直接背prototype和__proto__的区别而是给你一段代码让你判断实例属性查找的完整过程。举个例子function Person(name) { this.name name; } Person.prototype.sayName function () { console.log(this.name); }; var p1 new Person(lucy); p1.sayName();这题不难但如果往下追问就容易露馅了p1的__proto__指向什么Person.prototype.constructor又指向什么如果我们在p1上挂一个sayName和原型上的sayName同时存在调用的是哪一个正常笔试不会把这些拆成好几道题而是放在一道题里跟着代码流程一步步考你。我当年复习的笨办法是画图——把constructor、prototype、__proto__的关系画成一张三角结构图每次遇到原型链题就往图上套基本不会错。这套题里需要记住的一条核心结论是属性查找是按对象自身、再到其构造函数的原型、再到上级原型逐层查找的直到找到或者遇到Object.prototype为止。这条理解透了原型链相关的题基本能全对。2.4 DOM事件与跨域经验分的主要来源除了纯JS基础这套笔试题里还夹带了一些浏览器相关的常识题。比如事件冒泡和事件委托、DOM0级和DOM2级事件绑定的区别、常见的跨域解决方案。这些题目不算难但能区分出你是不是真的做过页面开发。如果只在模拟环境里写过代码可能连“事件冒泡到 document 上”这种常识都意识不到。事件委托是笔试高频考点建议你能熟练手写出来document.getElementById(list).addEventListener(click, function (e) { var target e.target; if (target.nodeName LI) { console.log(target.innerHTML); } });这段代码的核心优势是不需要给每个 li 单独绑定事件性能更好后续新增的 li 也天然支持点击。这套题里有很多类似的“场景题”——它不一定直接让你写事件委托但会在简答题里问“如何给动态生成的列表绑定事件”本质上就是考你懂不懂委托。跨域题的套路更固定一般答出 JSONP 的原理、CORS 配置、代理转发三种方式再简单解释各自的适用场景基本就能拿到大部分分数。3. 手写代码题高频题型的拿分模板3.1 数组去重永远不要只写第一种解法用友这套笔试题里手写代码题的第一大类就是数组去重。题目描述通常很简短“给定一个数组去除重复元素并返回新数组。”这种题看起来简单但评分的差距恰恰体现在“你会几种写法”。如果你只知道Set一种那在2017年可能会被扣分——因为ES6虽然已经发布了但企业项目里不一定敢直接用。而且如果只写出[...new Set(arr)]面试官会怀疑你根本没经历过兼容性问题的毒打。我在实际作答时一般会写一个相对通用的版本function unique(arr) { var result []; var obj {}; for (var i 0; i arr.length; i) { var item arr[i]; if (!obj[item]) { result.push(item); obj[item] true; } } return result; }这个写法的好处是兼容性好、思路直观。但你心里得清楚它的坑在哪里比如obj[item]会把数字和字符串混淆——unique([1, 1])会得到错误结果。所以更稳妥的进阶方案是用Mapfunction unique(arr) { var map new Map(); return arr.filter(function (item) { if (!map.has(item)) { map.set(item, true); return true; } return false; }); }笔试时我建议你写出你最有把握、逻辑最清晰的版本然后在旁边用一句话补充“如果项目支持ES6可以直接用Array.from(new Set(arr))。”这样既展示了你懂底层原理又表达了你知道现代写法属于典型的加分操作。3.2 防抖与节流写得出来不难难在“边界”防抖和节流是2017年前端笔试里的常客用友这套题里大概率也有一道让你手写其中之一的题目。很多人在准备阶段背过模板但一写就暴露问题——只记得大概思路细节全丢。我这边建议用两个版本记忆一个是能跑的简洁版一个是能拿高分的完整版。防抖关键点每次触发时清掉上一次的定时器重新计时。核心应用场景是输入框搜索、窗口resize。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); }; }注意这里我用apply把this和参数传递进去了很多新手写到这里会漏掉this绑定导致在对象方法上使用时崩溃。这在笔试里是最容易被扣的细节分。节流关键点一段时间内只执行一次。常见应用是滚动加载。function throttle(fn, interval) { var last 0; return function () { var now Date.now(); if (now - last interval) { last now; fn.apply(this, arguments); } }; }如果你有余力还可以在答案末尾补一句“以上是基于时间戳的版本第一次会立即执行还有基于定时器的版本最后一次会延迟执行。实际项目中要看场景选择。”这句话看起来很普通但在阅卷人眼里说明你真的理解了两者的差异而不是背了一个模板。3.3 深拷贝别把深浅拷贝混为一谈深拷贝是这套手写题里的另一个常客也是很多应届生丢分的重灾区。很多人只会写function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); }这在笔试卷面上可以作为应急答案但你得分不会高。因为稍微有点经验的人都知道JSON.stringify会丢掉函数、undefined、Symbol对循环引用的对象会直接报错。一个能在笔试里拿高分的版本至少要处理数组、普通对象和基本类型的区别function deepClone(source) { if (source null || typeof source ! object) { return source; } var target Array.isArray(source) ? [] : {}; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] deepClone(source[key]); } } return target; }写完之后如果还有时间可以补上“如果考虑循环引用需要用一个 WeakMap 记录已经拷贝过的对象”这样一个加分说明。阅卷人看到这句会觉得你不仅会写还想到了工程中的边界情况这在校招笔试里属于稀缺品质。3.4 经典输出题实战for循环加setTimeout这类代码输出题几乎每套笔试题都会出用友这套也不例外。核心代码如下for (var i 0; i 5; i) { setTimeout(function () { console.log(i); }, 100); }正确答案是5 5 5 5 5原因前面已经讲过了循环结束后i已经是5定时器回调执行时读到的都是同一个作用域里的 i。如果题里额外问“如何改进使得输出0 1 2 3 4”我建议你按照三种解法依次作答展示你的思路广度用let声明 i形成块级作用域。用 IIFE 创建闭包捕获每次循环时的 i。用setTimeout的第三个参数把 i 传进去。第三种方法很多人不知道但它是非常规解法里比较漂亮的for (var i 0; i 5; i) { setTimeout(function (j) { console.log(j); }, 100, i); }这类题给我的最大启示是笔试看的不是你背了多少题而是你在看到一个问题时能不能从底层运行机制出发推导出结果。如果你刷题只刷到“答案记住了”这个层面稍微变一下条件比如把var改成let、把0改成1000、把循环里加上一次i你就又不会了。4. 答题现场的那些坑失分往往不是因为不会4.1 时间分配和答题顺序做这套题的时候很多人第一个问题不是“不会写”而是“会写的没时间写完”。我当年参加的笔试一般是90分钟到120分钟题量在8~10道左右其中手写代码题占一半以上。我的建议策略是先花3分钟扫一遍全卷给每道题标注预估耗时。优先做基础选择题和简答题这类题得分性价比最高。手写代码题先写自己最有把握的哪怕它排在后面。如果有几道代码题都没完全把握先保证每道写出主体框架再回头补充细节。这套策略的核心依据是笔试评分很多时候是按步骤给分的。你写了一个只考虑正常情况、没考虑边界的防抖函数可能能拿70%的分但你花30分钟憋一个完美的深拷贝结果后面三道简答题全空着总分反而更低。4.2 手写代码的规范和加分细节手写代码题不是单纯的“能跑就行”阅卷人会看你的代码风格。我踩过几次坑之后发现以下几个细节特别影响印象分变量命名不要写a、b、tmp尽量用有语义的名字。缩进统一虽然是在纸上或者简单的编辑器里写但凌乱的缩进会让人觉得你不会写合作代码。区分var、let、const哪怕笔试题没有明确要求你在写新代码时主动使用const/let会显得你更新过知识体系。善用注释关键的一行逻辑上写一句简短注释比如“// 防止抖动的关键清掉上一次的定时器”这会让阅卷人更容易看到你理解题目。你能答对是一回事能不能让阅卷人一眼看出你“像个正经的程序员”是另一回事。这些细节在评分里权重不高但在大家答案差不多的时候就是拉开差距的地方。4.3 遇到完全没见过的题怎么抢救分数笔试一定会遇到没复习到的题或者题干描述绕来绕去看不懂的题。这时候直接写“不会”是最亏的因为哪怕你只写了一半思路也可能拿到步骤分。我这边的“抢救”套路是把题目抄一遍圈出关键词用自己的话重新描述一遍问题。写出一个最朴素的暴力解哪怕时间复杂度很高也没关系。如果连暴力解都不知道怎么写就详细描述你的解决思路比如“我大概会先遍历数组然后用一个对象记录出现次数最后筛选”“具体语法我记不清了”。千万不要空白。空白在阅卷人眼里等同于“完全没有思考”而一段围绕思路的废话反而可能让你拿到30%~50%的过程分。这个方法听起来有些取巧但校招笔试本身就是一个选拔工具。在有限时间内准确评估自己的知识边界、把会的部分尽量写出来也是一种工程能力。5. 常见问题速查与复习建议5.1 常错题速查表我把这套题里最容易翻车的几个考点整理成了一张速查表你可以把它当成考前最后一小时看的复习清单考点易错点正确思路var变量提升以为输出全局值函数内的var声明提升到顶部初始化留在原地this指向以为函数内 this 等于调用者看函数如何被调用独立调用指向全局/undefined隐式转换直接凭直觉判断按类型转换规则逐步拆解原型链查找只找自身属性沿__proto__链逐层查找Event Loop以为 setTimeout 立即执行同步代码先执行宏任务排到下一轮深浅拷贝认为JSON.parse(JSON.stringify())万能函数/undefined/循环引用都会出问题数组方法忘了哪些方法改变原数组push/pop/splice/sort会改原数组map/filter/slice不会5.2 基于这套题延伸的复习路线如果你正在准备校招我不建议你把时间全花在搜集往年的笔试题上。因为你永远猜不到目标公司今年会改什么题目但考点永远是那几大类。用友2017这套题其实可以当作一个知识索引照着它的考点清单逐项复习效率比盲目刷题高得多。我建议的复习顺序是吃透JS基础作用域、闭包、this、原型链、类型转换、Event Loop。把常见手写题练熟数组去重、防抖、节流、深拷贝、发布订阅、Promise。补全浏览器知识渲染流程、回流重绘、事件机制、存储方案。整理网络基础HTTP、跨域、缓存。如果还有时间熟悉一下ES6的常用语法。这套复习路线不仅适用于笔试也适用于面试。因为哪怕到了今天前端面试的核心还是在围绕这些底层概念做文章。框架可以换但基础不会过时。5.3 用真题训练复盘的正确方式最后再分享一个我自己复盘笔试的方法。每次做完一套题我不会只看答案对不对而是做三件事第一把每道错题对应的知识点写下来整理成一个清单。第二挑出里面最薄弱的三项花一周时间系统补。第三隔两周再把错题做一遍看是不是真的记住了而不是当时看懂了。这套方法听起来很笨但坚持下来效果非常明显。因为校招笔试考察的知识点范围其实是有限的你只需要把每个考点的每一次出错都补上知识网络自然会越来越完整。用友这套题即便再过几年你再拿出来做一遍依然能发现自己在某些基础细节上的盲区——这其实就是它的价值所在。