每一年的校招季翻出来看看,前端岗位的笔试题风格都会有些变化,但有一类题目哪怕过了好几年,拿出来再看依然觉得“真敢考”。用友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,两边都会转成数字比较,[]转数字是0,false转数字也是0,所以结果是true。
这种题在真实笔试里得分率极低。不是因为难,而是大部分人压根没系统梳理过JS类型转换的规则。
我后来复盘时整理了一张速查表,基本覆盖了这套题里可能出现的所有类型转换场景:
| 表达式 | 结果 | 原因 |
|---|---|---|
[] == false | true | 双方转数字为0 |
null == undefined | true | 特殊规则 |
null === undefined | false | 类型不同 |
'' == 0 | true | 字符串转数字 |
'0' == false | true | 两边都转数字为0 |
NaN == NaN | false | NaN不等于任何值 |
typeof null | object | 历史遗留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'])会得到错误结果。
所以更稳妥的进阶方案是用Map:
function 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 用真题训练复盘的正确方式
最后再分享一个我自己复盘笔试的方法。每次做完一套题,我不会只看答案对不对,而是做三件事:
第一,把每道错题对应的知识点写下来,整理成一个清单。第二,挑出里面最薄弱的三项,花一周时间系统补。第三,隔两周再把错题做一遍,看是不是真的记住了,而不是当时看懂了。
这套方法听起来很笨,但坚持下来效果非常明显。因为校招笔试考察的知识点范围其实是有限的,你只需要把每个考点的每一次出错都补上,知识网络自然会越来越完整。用友这套题即便再过几年,你再拿出来做一遍,依然能发现自己在某些基础细节上的盲区——这其实就是它的价值所在。