前阵子整理旧电脑,翻到自己当年备战校招时存的一堆笔试题,其中就有一套用友2018年的web前端笔试题。虽然时间过去好几年了,但前端笔试的核心考点其实变化不大——JavaScript基础、CSS布局、浏览器机制、手写代码,这些至今仍然是校招面试的高频区。把这套题重新过了一遍,发现里面好几道题放到现在依然有很强的参考价值,值得拿出来逐题拆一拆。
这篇文章我打算按照当年的做题思路来组织:先聊聊这套题整体在考察什么,再逐类拆解核心题目和背后的知识点,然后分享一些实战的做题策略和常见丢分点。不管你是正在准备校招的应届生,还是想查漏补缺的初级前端,这篇都能帮你摸清大厂笔试题的出题套路。
1. 用友这套题到底在筛什么人
1.1 岗位画像与笔试筛选逻辑
先说个比较现实的问题:校招笔试不是用来招天才的,而是用来做减法、滤掉基础不牢的候选人的。用友作为老牌企业管理软件厂商,它的前端团队主要做B端业务系统,这类系统的特点是页面复杂、交互多、兼容性要求高、维护周期长。所以笔试题目天然偏向基础扎实、逻辑清晰、能写干净代码的人。
看完这套题的整体结构,你能明显感觉到出题人不是想考你多新潮的技术,而是想确认三件事:第一,你的JavaScript基础到底扎不扎实;第二,你写页面的时候是真的理解布局原理,还是只会拖组件;第三,你遇到问题的时候有没有一套清晰的排查思路。这套考题基本就是围绕这三个维度来设计的。
另外,B端业务的特殊性决定了他们特别看重代码的健壮性和可维护性。笔试题里关于变量提升、闭包、事件循环这些知识点的考察,本质上是在测试你对语言运行机制的理解深度。一个能准确说出闭包内存回收机制的人,和只知道“闭包就是函数套函数”的人,写出来的代码质量是完全不同的。
1.2 试卷结构的高频模块分布
基于这套题的整体印象,我重新梳理了一下涉及的考察模块。为了让你更直观地看出重点在哪里,我做了一张分布表。
| 考察模块 | 所占比重 | 典型题型 | 考察目标 |
|---|---|---|---|
| JavaScript基础 | 约35% | 变量提升、闭包、原型链、this指向 | 语言理解深度 |
| CSS与布局 | 约25% | 浮动、居中方案、盒模型、Flexbox | 页面实现能力 |
| 浏览器与网络 | 约20% | 事件循环、事件冒泡、缓存、HTTP状态码 | 整体运行机制认知 |
| 手写代码与逻辑 | 约20% | 数组去重、防抖节流、递归等 | 实际编码能力 |
这份分布很有代表性。JavaScript基础占比最高,而且考察的不是单纯背概念,而是让你看代码写输出结果。这类题最考验功力,因为你能背出“闭包是函数和其词法作用域的组合”这句话一点用都没有,你得能判断出一段具体代码运行时会发生什么。
CSS布局那部分也很有意思,不是问你“Flexbox有哪些属性”,而是给你一个具体的页面效果图,让你用几种不同的方案实现同样的布局。这种考法更像是在模拟真实的开发场景——毕竟实际工作中没人会问你flex-grow的默认值是多少,但经常需要你实现一个复杂的页面布局。
2. 核心题目拆解与知识点解析
2.1 JavaScript基础:变量提升与闭包
JavaScript基础部分有一类非常经典的题目,就是给你一段代码,让你写出运行结果。这类题目考察的核心是变量提升和函数提升。我记得当时有一道题大概长这样:
var a = 1; function foo() { console.log(a); var a = 2; } foo();如果你回答输出1,那就掉坑里了。正确答案是undefined。原因在于,在函数foo内部,var a的声明会被提升到函数作用域的顶部,但赋值不会提升。所以函数内部实际的执行逻辑是:
function foo() { var a; // 声明提升,此时a是undefined console.log(a); // 输出undefined a = 2; // 赋值 }这道题特别经典,因为它考察的是你对JavaScript作用域和执行机制的理解,而不是简单的记忆。很多人在平时的开发中不太会写出这种代码,但这道题能很好地区分“背过概念”和“真正理解引擎行为”的候选人。
接着就是闭包,几乎每一套前端笔试题都会有一道闭包相关的题。常见的是让你解释循环中使用闭包的陷阱:
for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 1000); }如果你知道正确答案是连续输出5个5而不是0到4,说明你对闭包和异步执行顺序是有概念的。因为var声明的i是函数作用域的,循环结束后i已经变成了5,当setTimeout的回调函数执行时,它通过闭包引用的是同一个i。如果要让它输出0到4,可以用let声明(块级作用域),或者用立即执行函数把每次循环的i值保存下来。
我当年复习这类题的时候,总结了一个比较笨但很有效的方法:凡事遇到setTimeout、setInterval、Promise混在一起让你输出顺序的题,先在草稿纸上画出“调用栈、任务队列、微任务队列”三列,再一步一步模拟执行。这个方法虽然慢,但对理解事件循环和异步机制特别有效。
2.2 原型链与this指向
原型链的考察方式通常有两种:一是直接问你某个对象查找属性的顺序,二是让你手写一个简单的继承。这道题的常见表现形式是:
function Person(name) { this.name = name; } Person.prototype.sayName = function() { console.log(this.name); }; var p1 = new Person('Tom'); var p2 = new Person('Jerry'); p1.sayName(); // 输出什么? p2.sayName(); // 输出什么? console.log(p1.sayName === p2.sayName); // 输出什么?这里前两个输出结果都是各自的name,分别是Tom和Jerry。第三行输出true,因为sayName方法定义在Person.prototype上,p1和p2通过原型链找到的是同一个函数。这正好引出了通过原型链实现方法共享的核心思想。
关于this指向,我建议直接记住一条规则:函数中的this在调用时确定,谁调用了它,this就指向谁。严格模式下,独立调用的函数内部this是undefined;非严格模式下,会指向全局对象。箭头函数则完全不同,它没有自己的this,沿用的是定义所在作用域的this。
原型链和this这两块知识,笔试考的是选择题和简答题,面试就喜欢让手写继承。不管怎么考,关键是理解new关键字到底做了什么:创建一个新对象、把新对象的原型指向构造函数的prototype、把构造函数的this绑定到新对象上、如果构造函数没有显式返回对象就返回这个新对象。
2.3 数组方法详解与手写实现
用友这套题里数组相关题目出现的频率很高,这也是为了考察数据处理的实战能力。面对一道数组题,我的判断依据是:这道题有没有更合适的数组方法可以用?是选择map、filter、reduce这类函数式方法,还是用for循环处理复杂逻辑。
比如数组去重的题目,我印象中有这样的典型考查:
var arr = [1, 2, 2, 3, 4, 4, 5]; console.log([...new Set(arr)]);这道题今天看起来很简单,但当年ES6的Set刚普及不久,能写出这一行的候选者说明对ES6特性是熟悉且有意识在用的。如果要求不用Set,也可以借助一个空对象来记录已出现的属性,达到去重效果。
还有个常见题是数组扁平化,考查点在于递归和reduce的使用。如果要求不依赖flat()方法,可以手写递归遍历,碰到子数组就继续展开,最终拼接成一个新数组。这个题的进阶版本是要求用深度参数控制展开层数,写起来就更有意思了。
数组题我建议不要只停留在“会调API”,尽量去理解每个方法内部的行为,比如map和forEach的区别、reduce的初始值是否传对、sort的默认字典序陷阱等。曾有道题问[10, 5, 40, 25].sort()的运行结果,如果不传比较函数,JavaScript会把元素先转成字符串再按字典序排序,结果就是[10, 25, 40, 5],这个陷阱当年骗了不少人。
2.4 CSS布局:水平垂直居中与经典布局方案
CSS部分最能拉开差距的就是“水平垂直居中”这种看似简单实则多样的题目,特别适合拿来当作基础考察题。套在这套题里,它通常以“请用至少三种方式实现一个元素的水平垂直居中”的形式出现。
我当时总结了5种常用方案,每种方案的适用场景和原理都不同:
- 使用flex布局:父容器display: flex,子元素margin: auto,简单高效
- 使用position + transform:父容器相对定位,子元素绝对定位,配合translate(-50%, -50%),适合已知父容器尺寸的场景
- 使用position + 负margin:子元素绝对定位后设置top/left为50%,再用margin-top和margin-left拉回自身尺寸的一半,要求子元素宽高固定
- 使用table-cell:父容器display: table-cell + vertical-align: middle + text-align: center,适合老项目兼容方案
- 使用grid布局:place-items: center,代码最短,但需要考虑浏览器兼容性
这道题不出现在笔试题里我还真不习惯。从功能角度看,既能考察你对常用布局属性的掌握程度,也能看出你面对同一个问题时能否提供多个解决方案。
除了居中,经典的还有左侧固定宽度、右侧自适应的两栏布局。常见的实现方式有:左侧float+右侧margin-left(老办法)、父容器flex+右侧flex:1(现代推荐)、左侧绝对定位+右侧margin-left(适合左侧高度固定的场景)。实际项目中还经常要求左右两侧等高,这就涉及到flex的align-items属性的默认值问题,需要注意。
2.5 浏览器机制:事件流与事件委托
浏览器相关题目经常考事件流。要知道事件在DOM中传播分为三个阶段:捕获阶段、目标阶段、冒泡阶段。笔试题里最常见的就是让你判断给定代码的输出顺序:
<div id="parent"> <button id="child">点击</button> </div> <script> document.getElementById('parent').addEventListener('click', function() { console.log('parent冒泡'); }); document.getElementById('parent').addEventListener('click', function() { console.log('parent捕获'); }, true); document.getElementById('child').addEventListener('click', function() { console.log('child'); }); </script>这里点击child的时候,输出顺序是parent捕获、child、parent冒泡。因为捕获阶段从document根节点向下传播到目标节点,先经过parent的捕获监听器,然后到达目标节点执行目标监听器,最后进入冒泡阶段从目标节点向上传播回根节点,再次经过parent时触发冒泡监听器。
事件委托也是一个高频考点,它的核心思想是利用事件冒泡,将子元素的事件处理委托给父元素来绑定。这样做的好处有两个:一是可以处理动态添加的子元素;二是减少事件监听器的数量,提升性能。笔试题常见的考法是要求你给一个列表的每一项绑定点击事件并获取对应索引,而实际开发中我还观察到,很多人一提到事件委托就只能想到click,其实scroll、mouseover、keydown等事件也都可以做委托。
2.6 手写代码:防抖节流与对象深拷贝
这部分通常作为压轴题出现,它的存在是为了考察候选人脱离工具库后,能不能用原生JavaScript解决实际问题。用友这套笔试题里,我印象比较深的是防抖和节流的手写实现。它们都是性能优化的重要手段,很多初级开发知道概念,但写不出完整可用的函数。
先说说防抖,它的思想是触发事件后延迟执行,如果在延迟期间又触发了事件,就重新计时。典型场景是搜索框输入,用户停止输入后500毫秒才发起请求。
function debounce(fn, delay) { let timer = null; return function() { const context = this; const args = arguments; if (timer) { clearTimeout(timer); } timer = setTimeout(function() { fn.apply(context, args); }, delay); }; }节流的思想则相反,不管触发多频繁,保证在固定时间间隔内只执行一次。典型场景是滚动事件监听、窗口resize,避免在短时间内反复进行高消耗操作。
function throttle(fn, interval) { let previous = 0; return function() { const context = this; const args = arguments; const now = Date.now(); if (now - previous >= interval) { previous = now; fn.apply(context, args); } }; }除了防抖节流,手写深拷贝也几乎成了标配。考察的核心点是:如何区分数组和普通对象,如何处理循环引用,以及是否需要处理Date、RegExp等特殊类型。如果笔试只要求基础版本,处理好数组和对象的情况基本就能过关。想要更完整的话,可以加一个WeakMap缓存来实现循环引用检测,这样就算对象里存在互相引用也不会出现死循环。
3. 做题顺序与时间分配策略
3.1 拿到试卷先做三件事
校招笔试的时间通常都挺紧张,尤其是同时包含选择题、填空题和手写代码题的综合卷。用友这套题的总体量不算小,我当年做的时候最大的感受是:如果按顺序从头做到尾,很容易卡在中间某道难缠的题上,导致后面的代码题没时间写。所以开场不要急着做,我的习惯是先花三到五分钟把整份试卷扫一遍,做三件事:
第一,快速判断题型分布和分值占比,心里有数哪些题值得花时间。第二,先看一眼手写代码题,因为它可能在你不注意的时候已经在潜意识里酝酿解法了,等做到那题时会更快有思路。第三,标记出自己不太有把握的题目,先跳过,保证能拿的分全部拿到手。
这个方法其实有点像周赛做题的战术,本质是时间管理。笔试看的是整体得分,不是单题MVP,一道不会做的题纠结15分钟,不如把后面三道会的题稳稳拿下。
3.2 各题型的时间预算与取舍
根据我多年实战和身边同学的反馈,我整理了一份针对类似前端笔试题的时间分配参考表,你可以结合试卷实际分值和难度微调:
| 题型 | 建议用时 | 策略 |
|---|---|---|
| 选择题/判断题 | 10-15分钟 | 读完题就要有答案,超过1分钟没把握就先标记跳过 |
| 填空题/读代码写结果 | 25-30分钟 | 这类题最考基础,用草稿纸模拟执行,别凭感觉猜 |
| CSS布局/简答题 | 15-20分钟 | 能画图说明就画图,保持条理清晰 |
| 手写代码题 | 30分钟以上 | 留足时间,先写主流程再补细节,函数签名和边界条件也要写完整 |
关于取舍,有一条原则我特别想分享:宁可把一道手写代码题写得完整、有注释、考虑边界,也不要为了多写半道题而在每道题上都留个半成品。因为面试官看你的手写代码时,更看重的是思路清晰度和代码的完整性。
3.3 手写代码时的答题规范
这部分特别容易被忽略,但恰恰是很多人的失分点。手写代码题虽然不需要真正跑起来,但你写的代码要尽可能接近能直接运行状态。我总结了几个约定俗成的注意事项:
代码首先要缩进清晰,变量命名要让读者一看就懂,不要写一个字母的变量名除非是for循环的临时变量。其次,写函数时先在脑中过一遍三种情况:正常情况、空值情况、边界情况。比如手写数组去重时要考虑输入是空数组怎么办,手写防抖要考虑不传delay参数怎么办。
最后一点建议是,如果时间允许,在代码里写上简单的注释,说明你的关键步骤是在做什么。面试官一天要批几十份试卷,一份清爽、有条理、有注释的代码,即使不是最优解法,也会留下更好的印象分。这些细节,实际工作里写代码也一样重要。
4. 备考思路与常见失分点复盘
4.1 五个容易丢分的题目类型
这些年帮不少朋友模拟过笔试题,也搜集过很多同学的真实反馈,整理出了五个最容易丢分的题目类型。你把这几类练熟了,基本就能覆盖大部分校招前端笔试的坑。
第一个就是读代码写输出结果题,尤其是涉及变量提升、this指向和异步执行顺序的。这类题丢分的原因通常不全是“不会”,而是凭直觉猜答案,没有在草稿纸上认真模拟执行过程。我的建议是平时练习时就要养成手写执行流程的习惯,把每一行的调用关系理清楚。
第二个是CSS居中布局题。常见误区是只会一种flex方案,一旦题目附加了兼容性条件就束手无策。多准备几种不同思路的方案,无论对笔试还是面试都很有帮助。
第三个是事件委托相关题目。很多人只知道概念,但动手写代码时忘了处理事件参数和指定选择器匹配的逻辑,导致代码不完整或逻辑不严谨。
第四个是数组相关题,尤其是sort的字典序陷阱、map与parseInt组合的坑。这类题一旦踩坑几乎全军覆没,但又是平时开发中经常遇到的小陷阱。
第五个是手写代码题的边界处理。整体逻辑写对了,但没考虑参数为空、输入类型不对、返回值格式不对等情况,被扣了细节分,非常可惜。
4.2 针对校招刷题的高效复习清单
关于如何准备这类笔试题,我结合自己当年的复习路径和后来面试新人的经验,整理了一个清单,按优先级排列:
- JavaScript核心概念:变量提升、作用域、闭包、原型链、this指向、事件循环、深浅拷贝,这些是重中之重,每一个都要既能说清原理又能手写演示。
- ES6+常用特性:let/const、箭头函数、解构赋值、模板字符串、Set/Map、Promise、async/await,要做到看到代码就能说出行为。
- CSS布局能力:盒模型、浮动、定位、Flexbox、Grid,每种布局至少能手写两到三种方案。
- 浏览器机制:事件流三个阶段、事件委托、垃圾回收、存储方式(localStorage/sessionStorage/cookie)的区别、常见的HTTP状态码和缓存策略。
- 手写算法与工具函数:数组去重、扁平化、防抖节流、深拷贝、数组去重、字符串翻转、简单排序,这些是校招笔试的最高频手写题。
4.3 从笔试成绩反推知识体系的薄弱环节
如果你做过一套题之后感觉不太理想,别急着否定自己,也别急着再去刷十套新题。我建议反过来做一次“错题归因”,这比题海战术高效得多。把错题分分类,你是基础概念题错得多,还是代码阅读题错得多,或者手写实现题写不出?
如果是基础概念错,说明你对语言机制的理解还是偏表面的,这种只能靠系统性回炉,看看经典的JavaScript教程,把执行上下文、作用域链这几章反复读。如果是代码阅读题错,说明你读代码量不够,可以去GitHub上随便打开一个开源项目,刻意去读那些你没有写过的代码,遇到不理解的就断点调试。如果是手写实现题写不出,说明平时动手少,这个没有捷径,就是练,把高频手写题逐个做到能默写。
我见过很多同学准备笔试时有个误区,就是疯狂搜集各种面经和题库,但大部分题稍微换个形式又不会了。说到底,笔试考的是基本功的扎实程度,而不是见过多少题。一套经典题目能做到融会贯通,比走马观花看十套题有效得多。
4.4 这套题对实际开发的启发
从这套题往回想,其实很多考点在真实的用友B端开发场景中都有对应。比如原型链和闭包,写老旧的jQuery插件或者维护历史遗留代码时必须得过这一关;比如事件委托,后台管理系统的动态表格里每行都要绑定操作事件,直接给每行加监听器会产生大量冗余监听函数,事件委托在这种场景下几乎就是最优解;再比如防抖节流,搜索框自动补全和列表滚动加载这两个场景,你几乎每天都会用到。
我个人的体会是,笔试虽然看起来像是“纸上谈兵”,但它在很大程度上能反映一个前端工程师的日常积累。你在实际开发中养成的思维习惯和踩坑经验,恰恰就是做这些笔试题时的隐形优势。所以如果你想在校招笔试中拿高分,与其临时抱佛脚刷题,不如平时多思考“为什么这样写”,把每一次报错和排查都变成你知识体系的一部分。
最后分享一个我用着很顺手的复盘小技巧:每次做完一套笔试题,不要只对答案,而是把所有错题整理成一个Markdown文件,按照“题目—我的错误答案—正确答案—错误原因—知识点链接”五个字段记录。坚持整理几套之后,你会发现自己经常在同一个知识点上重复犯错,这时候重点突破就有了依据。这个方法也推荐给你。