很多人问我当年用友2016校招web前端笔试题考了什么。2016年正好是前端圈一个很有意思的节点:jQuery还统治着大量存量项目,AngularJS热潮稍退,React开始快速占领新项目,Vue也进入了不少团队的技术选型名单。在这股框架热潮里,用友的笔试题反而显得很“冷静”,通篇没有追新框架,而是拿HTML、CSS、JavaScript的基本功开刀。考完出来我印象很深,这套题不靠偏题怪题拉分,核心就是看你有没有把web前端的地基打牢。
和近几年动辄问源码、问工程化的笔试题相比,2016年的题目更“朴素”,但朴素不代表简单。它里面藏着大量可以连环追问的原理点,后来我自己参与前端招聘出题时,也沿用了一部分这套逻辑。这篇文章会把这份试卷的考点拆开来讲,每一类题怎么做、背后原理是什么、现在找工作还能不能参考,一次说清楚。
1. 拆开卷子看整体:这套题考的不是框架,是地基
1.1 先说命题思路:为什么用友敢拿基础题撑全场
2016年求职前端,简历上没放点jQuery项目、不会两手AngularJS,心里都会发虚。但用友这份笔试题恰恰相反,通篇没有要求你写某个框架的API,也没有让你比较Angular和React的优劣,而是安安静静地考HTML、CSS、JavaScript的核心机制。我现在回头想,这个出题思路很有代表性,因为用友主要做企业管理软件,这类产品页面复杂、表单密集、浏览器兼容要求高,一个前端如果只是会调用框架,很难在这个业务里站住脚。
框架年年换,但浏览器解析HTML、布局CSS、执行JavaScript的方式并没有变。基础稳的人,就算第二天从jQuery切到React,也能快速上手;而只会背框架的人,换个场景就露馅。用友这场的命题人显然想得很清楚:校招里指望你做过大型项目不现实,但考察你有没有吃透语言底层的能力,是完全可行的。
这也是为什么2016年很多公司开始在校招里加重基础题的比重。框架的知识随时可以补,但语言层面的理解需要时间沉淀。笔试是最低成本、最高效率的筛选方式,出一道闭包题,能淘汰一半人;出一道手写深拷贝,又能淘汰剩下的一半。
1.2 题型分布:笔试考什么,答案很清晰
从卷面结构看,用友2016校招前端笔试基本分成了四类题型:基础选择题、简答概念题、代码阅读题、手写编程题。虽然具体题目记不全了,但各大模块的比例我记得很清楚,因为考完出来我专门复盘过。
| 模块 | 主要考点 | 分值占比 |
|---|---|---|
| HTML与CSS | 盒模型、浮动、定位、选择器优先级、语义化 | 20% |
| JavaScript语言特性 | 变量提升、作用域、this、闭包、原型链、数组API | 35% |
| 浏览器机制与网络 | 事件流、异步、HTTP状态码、缓存 | 20% |
| 手写编程题 | 数组去重、防抖、深拷贝、函数封装 | 25% |
这个比例其实可以看成是一个信号:JavaScript语言能力是前端面试的胜负手。HTML/CSS是“门槛”,JS才是“分水岭”。大部分应届生卡在JS,因为HTML/CSS的记忆量多但逻辑门槛较低,JS则是大量的抽象概念堆在一起。
用友这种企业软件公司还有一个特点,就是大量页面需要处理表格、表单、弹窗、树形控件,数据渲染和交互逻辑非常重。笔试题里JavaScript占比高,和他们的真实业务高度相关。所以如果你准备的是B端、企业服务类公司的前端笔试,JS基础一定要放在复习优先级的第一位。
2. HTML与CSS:看着简单,其实是最能拉分的部分
2.1 盒模型:必须算出精确的数值
用友试卷里盒模型这道题我印象很深,因为它考得非常直白,典型考法是这样的:
<style> .box { width: 200px; padding: 20px; border: 5px solid #333; box-sizing: content-box; } </style> <div class="box"></div>问:这个元素在页面上实际占据的宽度是多少?
我估计很多人现在一眼就能看出是250px,但在现场笔试、时间紧张的情况下,还是有不少人会答成200px,因为脑子里的第一反应是“width就是宽度”。正确答案是200 + 20×2 + 5×2 = 250px,标准盒模型下width只包含content,padding和border要另外加。
这道题真正的加分点在第二个小问:如果把box-sizing改成border-box,实际宽度是多少?这时候width会包含padding和border,所以实际占据宽度仍是200px,但content会被压缩成200 - 40 - 10 = 150px。这就是在考察你不仅会用content-box,也知道border-box和“全局重置”的来龙去脉。
我当时在答题时顺手画了一个三层结构的示意图,标准盒模型在content外面包padding,再包border,最后是margin。别小看这个图,阅卷人看到你画图,说明你脑子里有建模能力,给分也会更宽松。
这里强烈建议所有准备前端笔试的人把下面这个表记牢:
| 盒模型模式 | width含义 | 实际占位宽度 |
|---|---|---|
| content-box(默认) | content宽度 | width + padding + border |
| border-box | content + padding + border | 直接等于width |
现在很多工程里会用* { box-sizing: border-box; }做全局重置,就是因为border-box更容易让布局尺寸可控,不会因为加了padding就撑破容器。这个操作习惯你现在去看那些成熟项目的样式表,基本都能找到。
2.2 浮动与BFC:别只会背“overflow: hidden”
浮动题在用友卷子里考得也挺扎实。场景是这样的:父容器里有两个浮动的子元素,问为什么父容器高度塌陷,怎么解决。
塌陷的原因要说清楚:浮动元素脱离了普通文档流,父容器在计算高度时不再把它们算进去,所以父容器高度就变成0。解决方案至少有4类:
- 父容器也设置浮动
- 父容器设置overflow: hidden或auto,触发BFC
- 在子元素最后加一个clear: both的空元素
- 使用伪元素::after配合clear: both
这道题只要答出“BFC”三个字,基本就能拿一半分。我当时把什么是BFC详细写了一段:BFC全称Block Formatting Context,块级格式化上下文,它决定了块级盒子的布局规则。一个元素触发了BFC之后,它内部的子元素和外部的元素会相互隔离,不会互相影响,同时BFC的高度计算会把浮动的子元素也算进去。
触发BFC的条件很多,常见的有:float不为none、overflow不为visible(hidden、auto、scroll都可以)、display为inline-block、flex、grid、table-cell等,position为absolute或fixed。这些条件不用死背,理解成“让元素变成一个独立的布局容器”就行。
2016年考浮动的题特别多,因为当时flex布局还没有全面普及,企业产品里大量页面还需要用浮动来搭多栏。用友这类软件公司的系统里,最常见的后台页面就是左侧导航、右侧内容的两栏布局,用浮动可以很轻松做出来:
<div class="container"> <aside class="sidebar">菜单</aside> <main class="content">内容区</main> </div>.sidebar { float: left; width: 200px; } .content { margin-left: 200px; }但如果只让sidebar浮动,容器高度同样是塌陷的,得在main后面加清除,或者给container触发BFC才能撑起来。笔试里如果在代码后面补一行注释“这里需要清除浮动,因为容器高度会塌陷”,会很加分,因为这体现出你写过真实页面,而不是只背概念。
用友的题里还有一个我很欣赏的点:考CSS的时候不追求炫技,而是强调布局的稳定性。这也提醒我们,刷题的时候别只盯着高级特效,把浮动、清除浮动、BFC掌握扎实,比会写几个CSS3动画要实在得多。现在回头看,那些在移动端适配、打印样式、多栏布局里遇到的老问题,全是这些基础概念的延伸。
2.3 选择器优先级与层叠规则:一套公式走天下
选择题里通常会出现这类题:
#box .title { color: red; } .container .title { color: blue; }问元素最终是什么颜色。答案是红色,因为ID选择器优先级更高。
优先级计算可以当成一个四位数来记,从高到低是内联样式、ID选择器、类/属性/伪类选择器、元素/伪元素选择器。我在笔试里习惯直接算出权重写在旁边,比如#box .title是1个ID加1个类,权重是(1, 1, 0),.container .title是2个类,权重是(0, 2, 0),明显前者更大。
这里有个当年的经典坑:!important写在哪儿都能覆盖普通优先级,但如果是两个!important对决,还是要回到优先级比较。另外,后来出现的:is()、:not()这类选择器,本身不参与优先级计算,但写在括号里面的选择器参与计算。这个点现在也常考。
选择器优先级这件事,直接用一套规则算下来最稳。很多应届生靠“背下来”去做题,遇到复杂的组合选择器就慌了。我的建议是,所有优先级题都养成写权重值的习惯,哪怕题目没要求写计算过程,你心里也要有这个步骤。用友这份卷子里的CSS题不算多,但每道都在提醒你:基本功必须过硬,因为企业级产品页面里选择器一旦写乱,维护成本极其可怕。
3. JavaScript是重头:校招笔试拉开差距的部分
3.1 变量提升与作用域:选择题的常客
JS部分占比最大,而且很多是输出题。比如这种经典题:
var a = 1; function test() { console.log(a); var a = 2; } test();输出是undefined,而不是报错,更不是1。因为var声明会被提升到函数作用域顶部,但赋值不提升,所以test内部顺序变成了var a; console.log(a); a = 2。
这道题考察的是执行上下文和变量环境。ES6普及之前,var的变量提升几乎是必考项。哪怕放到现在,理解提升仍然非常重要,因为很多遗留代码和面试题里还会出现。
我当时复习时总结过一句话:JS代码不是一行一行按字面顺序执行的,而是先经过编译阶段,把变量和函数声明放进对应的作用域,再进入执行阶段。理解了这个底层逻辑,很多输出题就不会错。你可以把编译阶段想象成先给整个屋子贴好门牌号,执行阶段才是真正进屋开门。变量在编译阶段已经被登记了,但它真正的值要等到执行到那行赋值语句才会被确定。
ES6之后的let和const又引入了暂时性死区,同一个变量在声明之前都不能使用:
console.log(b); let b = 1;这个会直接报ReferenceError。所以现在再遇到这类题,要区分var的提升和let/const的暂时性死区,这也是后来前端笔试题的新增长点。现在很多考生一看到let就以为“不会提升”,其实是“会创建但不初始化”,这种细微差别正是笔试喜欢抠的地方。
3.2 this绑定与闭包:经典中的经典
用友卷子里出现了一道很经典的this题:
var name = 'window'; var obj = { name: 'obj', getName: function() { return this.name; } }; var fn = obj.getName; console.log(fn()); console.log(obj.getName());第一个输出window,第二个输出obj。原因在于this不是定义时决定的,而是调用时决定的。fn()是普通函数调用,this指向全局对象;obj.getName()是以对象方法的形式调用,this指向obj。
放在浏览器里还要注意严格模式,普通函数调用里this会是window,严格模式下是undefined。这个细节在笔试里也常常挖坑。我后来参加面试时发现,出题人特别喜欢在严格模式和非严格模式之间切换,同一个函数,加不加'use strict',答案就完全不同。
闭包考的是这段经典代码:
for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 0); }问输出什么。答案是5个5。因为setTimeout里的回调函数在循环结束之后才执行,而var没有块级作用域,i是同一个变量,此时已经变成5了。
我当时在答题卡上写了三种解决方式:用let声明、用立即执行函数包一层、用bind传参。如果时间充足,建议把三种都写出来,因为这道题考的不是“会不会背答案”,而是“你真的理解作用域和闭包吗”。
闭包的核心概念,可以用一个通俗类比:函数是一张渔网,定义它的环境是海域,闭包就是“渔网+海域”打包带上船。即使船开到别的地方,这张网依然记得原来的海域。理解了这个比喻,再看防抖、节流、模块化私有变量、柯里化,会发现全是闭包在不同场景下的运用。
用友这个软件公司为什么爱考闭包?因为企业前端代码里有大量数据绑定、事件回调、定时器,全是闭包的使用场景,比如封装一个计数器、做防抖节流、模块化隐藏私有变量。笔试里考闭包,不是在刁难人,而是在筛选能写出可维护代码的人。
3.3 原型链与继承方式:画图是最高效的记忆法
原型链几乎是任何一个正规前端笔试的保留节目。考试题目一般是给一个构造函数,然后问某个对象能不能访问某个属性,或者要求实现继承。
用友的卷子里见到了一个简答题:请简述new一个对象时发生了哪些步骤。答出四步基本就能拿分:
- 创建一个新对象
- 新对象的__proto__指向构造函数的prototype
- 构造函数内部的this指向这个新对象并执行
- 如果构造函数返回对象,则返回该对象,否则返回新对象
实现继承的写法有好几种,但建议至少掌握一种组合寄生式继承:
function Parent(name) { this.name = name; } Parent.prototype.say = function() { console.log(this.name); }; function Child(name, age) { Parent.call(this, name); this.age = age; } Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child;这里要注意Child.prototype.constructor = Child这一行,很多人会漏掉,但不修复会导致对象实例的constructor指向错误。
我当时复习原型链的方法是画图。把构造函数、prototype、proto、实例之间的关系画一遍,比背十遍文字都管用。笔试时间宽裕时,我甚至会在答案旁边画一张原型链简图,阅卷人一眼就能看出你懂。这个方法我现在做面试官时也经常建议候选人用,因为画图本身就是在建模。
原型链题目还有一个常见变体,就是给一串属性访问代码,让你判断某个值。这类题只要记住一条:对象查找属性时,先找自身,再沿着__proto__往上找,直到Object.prototype,还没有就返回undefined。把这条规则吃透,原型链题基本不会错。
3.4 数组与字符串API:刷题型分值高
这套卷子里的数组方法题分值不小,题目常这样出:
var arr = [1, 2, 3, 4, 5]; var result = arr.map(item => item * 2).filter(item => item > 4).reduce((sum, item) => sum + item, 0);问result是多少。这种题考的是链式调用和API的返回逻辑,map返回新数组、filter返回新数组、reduce返回累加值。答案就是(4+6+8+10) = 28。
用友这类企业软件的页面上,表格数据处理非常频繁,比如从接口拿到列表数据后,需要过滤、排序、映射出展示字段,这些操作就是map、filter、reduce的实战场景。所以在笔试题里考数组API,背后是真实的业务需求。
还有一个高频考点是数组去重:
var arr = [1, 2, 2, 3, 3, 4, 5, 5];我当时写的是Set去重一行版:
[...new Set(arr)]但我会在后面补一句“经典写法还可以用indexOf判断或者对象哈希”,展示自己知道多种思路。笔试时多写一种解法不是浪费时间,而是让阅卷人看到你的知识面。扩展思路在这里成为你的第一价值,因为你展示的不仅是一道题的答案,而是面对问题时能想到几种路径的习惯。
排序也是常客,特别是sort的坑:[10, 9, 8].sort()会返回[10, 8, 9],因为sort默认按字符串的Unicode码点排序。这个坑我当年考试时遇到过,那次之后我再也不敢不传比较函数了。
4. 浏览器机制与网络基础:不仅考知识,还考应用能力
4.1 事件冒泡、捕获与事件委托
浏览器机制这部分,用友卷子考了事件流。一个常见的题目是:点击子元素,父元素和子元素的click事件触发顺序是什么。
标准事件模型分三个阶段:捕获阶段、目标阶段、冒泡阶段。如果使用addEventListener且第三个参数为false,那就是冒泡阶段触发,子元素先触发,父元素后触发。如果第三个参数为true,则是捕获阶段,父元素先触发。
事件委托是更实用的考点:
document.getElementById('list').addEventListener('click', function(e) { if (e.target.tagName === 'LI') { console.log(e.target.textContent); } });这样可以只给父容器绑定一次监听,后续动态添加的列表项也能响应事件,同时减少内存占用。在后台管理系统里,列表、菜单、树控件都是动态生成的,事件委托用得好,能省下大量绑定代码。
当时答题时我在后面写了一句“注意e.target和e.currentTarget的区别”,这个细节很加分,因为很多人只知道事件委托的写法,却分不清目标元素和绑定监听器的元素。e.target是真正被点击的元素,e.currentTarget是绑定监听器的元素,这两个一旦混淆,调试事件相关bug时就会满头雾水。
事件机制还经常和内存泄漏挂钩。我记得后来在项目里遇到过给DOM绑了一堆匿名函数,页面一关,监听器没解绑,内存就泄了。所以笔试里如果出一段绑定事件的代码,让你找问题,记得优先找removeEventListener或者abortController。
4.2 事件循环与异步执行顺序
2016年的时候,事件循环的题目开始变多,但还没那么多Promise微任务的题。现在已经成了笔试题里的标准配置。用友的卷子里有一个简化版:
setTimeout(function() { console.log('timeout'); }, 0); Promise.resolve().then(function() { console.log('promise'); }); console.log('sync');问输出顺序。正确的是sync、promise、timeout,因为promise.then属于微任务,会在当前宏任务结束后立即执行;setTimeout属于宏任务,要等下一轮事件循环。
复习的时候我把事件循环类比成“排队+插队”:同步代码是一队,微任务队列可以插到当前队伍末尾,宏任务队列则是另一个新队伍,必须等当前这个队伍清空了才能轮到。这个类比后来在面试里也救过我很多次。
异步这块现在已经升级了,Promise、async/await、generator都会考。但万变不离其宗,只要明白微任务优先于宏任务,再复杂的输出顺序题都能推出来。我建议准备的时候自己手写一张事件循环完整流程表,把同步、微任务、宏任务的执行顺序画清楚,考前看一遍,比刷10道题都管用。
4.3 网络与缓存的基础题
网络基础在校招笔试题里常见,用友也考了HTTP状态码,比如200、301、302、304、404、500分别代表什么。这道题不难,但很考察信息的准确度。我记得当时答到304时多写了一句:304 Not Modified表示服务器资源未变化,浏览器可以使用本地缓存,不需要重新返回资源内容。这个细节比只写状态码名称要值钱得多。
缓存这块更常考的是强制缓存和协商缓存的区别:
| 缓存类型 | 相关头 | 特点 |
|---|---|---|
| 强制缓存 | Cache-Control、Expires | 命中后不发请求,直接读本地 |
| 协商缓存 | Last-Modified/If-Modified-Since、ETag/If-None-Match | 每次发请求,服务器来判断是否可用304 |
用友的软件里有大量后台列表页、报表页,数据变化没那么频繁,合理利用缓存能显著提升页面加载速度。笔试里考这个,其实是在试探你有没有性能优化的基本意识。
还有一个我后来才想明白的细节:HTTP缓存里Expires是绝对时间,Cache-Control的max-age是相对时间,后者更靠谱。笔试里如果能主动答出“Cache-Control比Expires更可靠”这个结论,就比单纯堆头字段的考生高出一截。写答案时多写一句“为什么”,是得高分的通用秘诀。
5. 手写编程题:整张卷子得分差距最大的一块
5.1 三道典型手写题怎么写
这套卷子的编程题质量挺高,不考复杂的算法,而是考前端工程里真正每天在写的函数。我印象比较深的有三道。
第一道是防抖函数。要求实现一个debounce,在事件高频触发时不立即执行,而是等触发停止N毫秒后再执行:
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,很多人漏掉这个,返回值就变得不可靠。当时我在函数后面补了一句“这里不能直接用箭头函数,因为箭头函数会丢失this”,这个细节帮我在阅卷人那里拿了不少印象分。
第二道是数组去重。除了Set写法,手写题里还应该能写出来:
function unique(arr) { var result = []; for (var i = 0; i < arr.length; i++) { if (result.indexOf(arr[i]) === -1) { result.push(arr[i]); } } return result; }写完之后我一般会补一句时间复杂度O(n²),再用对象哈希优化到O(n)。笔试时把复杂度分析写上,是非常有效的加分行为。后来我阅卷时,只要看到候选人主动写复杂度分析,都会在印象分上往上拉一点,因为这代表他有性能意识。
第三道是深拷贝。2016年很多人第一反应写JSON.parse(JSON.stringify(obj)),但这样会丢失undefined、function、Date等特殊值,也不能处理循环引用。我当时用一种递归实现:
function deepClone(obj) { if (obj === null || typeof obj !== 'object') return obj; if (obj instanceof Date) return new Date(obj); if (obj instanceof Array) return obj.map(item => deepClone(item)); var clone = {}; for (var key in obj) { if (obj.hasOwnProperty(key)) { clone[key] = deepClone(obj[key]); } } return clone; }这里用hasOwnProperty是为了避免把原型链上的属性也拷贝进来,也是一个容易被忽略的细节。现在用友可能还会在深拷贝题后面追问“怎么解决循环引用”,那就需要WeakMap缓存已经拷贝过的对象。
5.2 手写题的答题节奏和加分动作
手写题最重要的不是一次写对,而是展示出工程思维。我的习惯是拿到题先花30秒读题,圈出输入、输出和边界条件,然后在草稿区写几个测试用例,再动手写代码。写完之后再把测试用例跑一遍,看看边界值处理得对不对。
笔试现场时间紧张,不可能每道题都完美,但有几个高性价比的细节值得坚持。第一,函数命名清晰,变量名不写a、b、c。第二,核心逻辑后面加一行注释,说明设计思路。第三,如果忘了某个API,直接在注释里写“这里也可以用XXX实现”,让阅卷人看到你的知识储备。第四,如果确实不会,把思考过程写上去,至少有过程分,空着是零分。
我还记得一个真实案例,有次模拟测试里让写防抖,有个同学知识点是会的,但函数叫f(),参数叫x,不写注释,也没有返回值说明。阅卷的人根本不知道他在写什么。后来我给他的建议是,所有手写题都当成一次小型代码评审来对待,你写出来的代码,是要给一个不了解你思路的陌生人看的。
5.3 用这些题反推备考重点
从上面这几道手写题能反推出一条备考路线:先是原生API的熟练度,再是边界条件的敏感度,然后是作用域和this的运用,最后是性能意识。这四个层次刚好对应答题时的四个加分点。
当年我复习手写题时,专门建了一个小文档,把防抖、节流、深拷贝、数组去重、Promise.all、事件订阅发布这类题目每个都写了三种解法,然后按频率刷。后来我发现,很多公司的笔试题会互相借鉴,题量不大但很经典,刷一刷是能显著提升通过率的。
手写题本质上考的是“你会不会把一个需求翻译成代码”。所以备考时不要光看代码,要试着把题目场景化:防抖用在哪、深拷贝用在哪、事件订阅发布用在哪。想清楚业务场景,代码就不是背出来的,而是写出来的。
6. 多年后回头看:这套题对现在找工作的价值
6.1 旧题是不是过时了
这几年前端笔试题已经变了很多,框架源码、工程化配置、TypeScript、性能优化、Node.js等比重越来越高,纯基础题似乎没有2016年那么“统治”了。但你仔细看现在的笔试题,很多都是在用新的包装考旧的底层。比如React的Hooks考点,背后仍然是闭包和状态更新时机;比如工程化方面的tree-shaking,背后仍然是模块系统的作用域分析;再比如Vue的nextTick考法,本质还是事件循环和微任务。
所以用友2016年这套题并没有过时,它只是把前端最持久的那部分知识先筛了一遍。现在的应试策略应该是在这套基础上再叠加框架和工程化的内容,而不是完全抛弃基础去追新。我见过太多候选人被问“闭包”时会心一笑,觉得太简单了,但让他现场写一个防抖函数时就开始卡壳。基础知识的“会”和“熟”是两码事。
6.2 平时怎么有针对性地巩固
我建议把基本功复习分成三层。第一层是HTML/CSS,重点掌握语义化、盒模型、BFC、布局方案,不需要死记冷僻标签。第二层是JavaScript语言本身,重点是执行上下文、this、作用域、闭包、原型链、异步模型、常用API。第三层是浏览器和网络基础,重点是事件机制、渲染流程、HTTP缓存和一些经典性能指标。
每个层次的复习都配一个“输出型练习”,也就是不看资料默写一遍。比如关掉编辑器手写一个防抖函数,或者拿出一张白纸把事件循环的宏任务和微任务队列画一遍。只有能输出,才说明真的掌握了。这个方法不仅适用于笔试,也适用于面试中的现场编程题。
用友这份笔试题里还有一点值得学:出题人刻意绕开了“背诵型”考点,所有题目都偏理解。这意味着备考时要尽量减少死记硬背,多问自己为什么。为什么这样输出?为什么这个