一个下午的时间,我在电脑前把用友2016校招的web前端笔试题从头到尾梳理了一遍。这套题放在今天看,难度不算变态,但覆盖面相当全,JavaScript基础、DOM操作、浏览器原理、性能优化全都有涉及。更关键的是,这套题反映了那个时代前端校招的典型考察思路——不考框架API的死记硬背,而是看你基本功扎不扎实、能不能用原生JS解决实际问题。我结合当年的参考答案和这几年的实际开发经验,把每道题的考点、答题思路、易错点都拆开揉碎聊一遍。准备校招的朋友可以拿这套题自测一下,工作三五年的老前端也可以看看自己还能答上来多少。
1. 这套题背后的考察逻辑:老牌企业校招到底想要什么人
很多同学拿到笔试题的第一反应是赶紧找答案、背答案,但我建议你先花十分钟想想出题人到底在考什么。用友不是互联网大厂,是典型的企业服务软件公司,业务以ERP、云服务为主。这类公司的前端团队规模不会特别大,但负责的业务系统往往逻辑复杂、生命周期长,一个页面可能要用好几年。所以他们的校招笔试题有几个很明显的倾向。
首先,极度看重原生JavaScript的掌握程度。2016年的时候,jQuery还是绝对主流,Vue和React刚开始普及但远没有形成垄断。企业级项目里有大量存量代码是原生JS和jQuery写的,你进来之后最可能要做的就是维护这些老系统。所以笔试题里大量出现this指向、作用域、闭包、原型链这类纯语言层面的问题——这些不会因为框架更迭而过时。今天来看,虽然大家写Vue、React多了,但原生JS的基础考察依然是前端面试的保留项目,足以说明这套题的生命力。
其次,注重考察解决问题的思路而非单纯的知识记忆。这套题里有不少是代码输出题或手写实现题,比如让你判断一段代码的输出结果、让你实现某个数组去重。这类题目如果只是背过答案,换个形式就会露馅。出题人想看到的是你分析代码执行过程的思路,比如遇到this指向问题,你会不会先看调用方式;遇到闭包题,你会不会先标出变量的引用关系。
第三,关注工程实践中的常见场景。事件绑定、DOM操作、数据请求、字符串处理,这些都是做企业级前端每天都会遇到的事情。笔试题不考脑筋急转弯,而是把实际开发中的典型场景抽象成题目。我在做这套题的时候就有这种感觉——很多题不是“会不会”的问题,而是“平时写代码的时候有没有想过为什么会这样”。比如事件委托,如果你只是在框架里写click绑定,从来没想过事件冒泡的机制,遇到这类题就很容易卡壳。
所以,如果你现在正在准备前端校招,看到一套笔试题不要急着刷完对了答案就完事。先看题型分布,再对着每道题问自己三个问题:它在考什么知识点?这个知识点在实际开发中解决什么问题?我能不能用大白话把原理讲清楚?这三关过了,笔试面试基本就稳了。
2. 题目逐题拆解:从JavaScript基础到浏览器原理
2.1 数据类型与类型判断:typeof与instanceof的相爱相杀
第一类必考题就是数据类型相关的,用友这套题里也出现了。给你一段代码,让你用typeof判断各种值会输出什么,或者让你说说null、undefined、NaN的区别。这类题看着简单,但想答得全面并不容易。
typeof的返回值有string、number、boolean、object、function、undefined这几个,2016年那时候还没有symbol。很多人知道typeof null返回的是object,但不知道这是JavaScript语言设计时留下的历史bug——早期的实现中,对象类型用低三位表示,null恰好是000,所以被typeof当成了对象。知道这层底层原因之后,你就不容易忘记了。
NaN的判断是另一个高频点。NaN有一个很反直觉的特性——它不等于自己。所以你不能用n === NaN来判断,标准做法是调用全局的isNaN函数,或者更严谨地用n !== n。用友的题里就有一道类似的判断,当时很多人栽在null和NaN上。还有一种判断方式是ES6的Number.isNaN,它跟全局isNaN的区别在于,后者会先把参数转成数字再判断,前者直接判断类型,所以Number.isNaN("abc")是false,而isNaN("abc")是true。这个细节在实际开发中也很容易踩坑。
再说instanceof。它的原理是判断构造函数的prototype是否出现在实例的原型链上。所以[] instanceof Array返回true,[] instanceof Object也是true。但有个坑是,跨iframe的情况下,一个iframe里的数组在另一个iframe里用instanceof判断可能是false,因为原型链不同了。这也是为什么后来有了Array.isArray这个方法。我在实际开发中遇到过一次类似的问题,页面里嵌了个iframe,用它传数据过来,结果instanceof判断把所有数组都当成了非数组,排查了半天才想起来还有这个历史遗留坑。
2.2 作用域与闭包:为什么你的for循环i等于最后一次的值
用友这套题里最经典的一道就是循环里绑定事件的题目。类似这样:一个for循环里给三个元素绑定click事件,循环变量是var i,问点击每个元素的时候弹出来的是几。答案是每次都是最后一个,因为var声明的i是函数作用域,循环结束之后i已经是最终值了,事件回调执行时访问的是同一个变量。
这个问题的本质是闭包捕获的是变量本身而不是变量的值。解决方式有几种:用let声明循环变量,让每次循环创建独立的作用域;或者用立即执行函数(IIFE)传入当前的i;也可以用bind方法给回调函数绑定参数。用友当年的题标准答案是IIFE方案,因为那时候ES6的let还没那么普及。现在做这类题,直接写let就完事了,但你最好把三种方案都掌握,因为面试官很可能追问。
闭包还有一个非常实用的场景就是私有变量。比如实现一个计数器,外部不能直接修改内部的值,只能通过暴露的方法来操作。用友也考了这个——让手写一个简单的模块或计数器。这种题的本质就是考察你能不能理解和运用闭包的两个特性:一个是被闭包引用的变量会常驻内存,另一个是闭包可以形成对变量的独占访问。
这里要提醒大家一个容易忽略的点:闭包并不神秘,它就是函数嵌套函数时,内层函数可以访问外层函数变量的机制。很多初学者一听到闭包就紧张,其实你每天写的回调函数绝大多数都是闭包。判断是不是闭包,就看函数是否访问了它自己作用域之外的变量,仅此而已。
2.3 原型与原型链:new一个对象到底发生了什么
原型链是JavaScript里最绕的一块内容,但也是老牌公司特别喜欢出题的部分。用友的题目里有一道给你一个构造函数,然后让你判断某个对象有没有某个属性、属性能不能访问到,或者让你说说new操作符到底做了什么。
标准的new过程分四步:创建一个新对象;把这个新对象的原型指向构造函数的prototype;把构造函数的this绑定到新对象上并执行构造函数代码;如果构造函数有返回对象,则返回这个对象,否则返回第一步创建的新对象。这个流程理解之后,很多题就能推出来了。
还有一个高频考点是hasOwnProperty,用来判断属性是实例自有的还是从原型链上继承来的。这在实际开发中很重要,比如你用for in遍历对象时,最好用hasOwnProperty过滤掉原型上的属性。用友的题里就有一道辨析,让你说出for in会遍历到什么样的属性。
原型链还有一个常见用法是做继承。ES6的class本质还是基于原型链的语法糖。题目可能会让你用ES5实现一个继承,一般有构造函数继承、原型链继承、组合继承几种方案。组合继承是当时的标准答案,它既能通过call在子类构造函数里继承实例属性,又通过把子类的prototype指向父类实例来继承方法。现在虽然都用extends了,但原理掌握了对理解React组件的继承关系还是很有帮助的。
2.4 DOM操作与事件机制:浏览器页面的读写规则
企业级前端写的最多的就是DOM操作,用友这套题里DOM相关的题目占了相当比重,主要围绕节点查找、事件绑定、事件委托。题目类型包括:给你一个ul>li的结构,让你实现点击某个li的时候弹出行号;或者让你删除一个节点,问有几种方式。
节点查找那几个方法——getElementById、getElementsByClassName、getElementsByTagName、querySelector、querySelectorAll——在那个年代是基本功。注意getElementsBy系列返回的是HTMLCollection,是动态集合,意思是每次访问它都会重新查询DOM。querySelectorAll返回的是NodeList,是静态集合,页面变化不影响它。这个区别在面试中问出来很容易区分有经验和没经验的人。实际开发中的坑在于,你用getElementsByClassName拿到一个集合,然后在循环里做删除操作,会发现索引错乱,就是因为集合是动态的。
事件机制的核心是捕获、目标、冒泡三个阶段。用友考了冒泡和捕获的差异,以及如何阻止冒泡。当年标准答案是e.stopPropagation(),现在还有人用e.cancelBubble = true这种老写法。事件委托就是充分利用冒泡机制的经典实践——给父元素绑定事件,通过e.target判断实际点击的元素。这在动态生成的列表里特别实用,新添加的子元素不用重新绑定事件。
这里需要特别注意一个细节:很多面试者能说出事件委托的概念,但实现的时候没有考虑边界情况。比如做输入框防抖的时候,如果输入框在事件委托的目标范围内,你需要判断e.target是不是input元素,否则会在用户点击input标签时也触发。这类细节通常决定了面试官对你能力的评价。
2.5 浏览器机制与网络基础:一个前端的基本常识储备
用友的笔试题还涉及了不少浏览器和网络的基础知识,比如URL从输入到页面展示的过程、常见的HTTP状态码含义、cookie和localStorage的区别、前端性能优化方案等。这类题目在那个年代属于加分项,现在则是必考内容。
URL从输入到展示的过程,标准回答链路是:DNS解析 -> 建立TCP连接 -> 发起HTTP请求 -> 服务器处理并返回 -> 浏览器解析HTML构建DOM树 -> 解析CSS构建CSSOM -> 执行JavaScript -> 合并渲染。这里面可以展开的点很多,比如DNS解析是递归的还是迭代的、TCP的三次握手和四次挥手、浏览器的渲染流程里什么时候会阻塞DOM解析。用友当年的题没问那么深,但你回答得越有层次,面试官对你的评价就越高。
HTTP状态码是必须知其然且知其所以然的:200是成功,301是永久重定向,302是临时重定向,304是走缓存,403是禁止访问,404是找不到资源,500是服务器内部错误,502是网关错误,503是服务不可用。做题的时候最怕遇到的是304和重定向的区分——很多项目上线之后出现资源不更新的情况,就是因为没有正确处理缓存头导致走了304。
localStorage和cookie的区别也是高频题:存储大小不同、会不会随请求发送到服务器、过期时间设置方式不同、作用域差异。特别要记住,随着业务越来越复杂,cookie里尽量不要塞太多非必要数据,因为每个请求都会带上它们,极大地浪费流量。后来有了HttpOnly和SameSite属性,这个知识点也在校招中出现过,你需要知道它解决了什么问题。
3. 写代码的题:看着简单但最容易翻车的几类
3.1 数组去重与字符串处理:一题多解是核心竞争力
用友这套题里有几道手写代码题,数组去重算是非常典型的一道。这类题的神奇之处在于,每个人都能写出来,但写法不同体现了水平差距。
最简单的方案是双重循环去重:外层遍历数组,内层逐个比较是否已存在于新数组中。这个方法时间复杂度是O(n²),数据量小的时候无所谓,但作为笔试题,这个答案只能说明“你能写代码”,不能说明“你写得好”。那时候的主流优化方案是用对象作为哈希表来去重——把数组的值存为对象的key,利用对象key的唯一性判断是否重复。性能快到飞起,但有个历史坑:对象key只能是字符串,所以数字1和字符串"1"会被当成同一个key。解决方式是obj[typeof item + item]来区分类型。
用indexOf去重是当时很多人爱用的:遍历原数组,如果新数组里没有当前项就push进去。这在大多数场景下没问题,但遇到NaN就失效了,因为indexOf内部用的是严格相等比较,而NaN不等于任何值。用includes可以解决NaN的问题,用ES6的Set更是只要一行代码。但笔试的时候不要一上来就写Set,我建议你先写出多种方案,再和面试官聊聊每种方案的优劣,反而能展示你的广度。
字符串处理题目也经常出现,比如统计字符串中出现次数最多的字符、判断回文、反转单词等。这类题的核心是能不能熟练使用split、join、reverse、replace、match这些API,以及能不能处理边界情况。以统计字符次数为例,标准思路是遍历字符串,用对象记录每个字符的出现次数,最后找出最大值对应的key。这道题本身不难,但它能考验你能不能快速想到用对象当计数器的方法。
3.2 数组与对象的方法混用:map、filter、reduce不只是语法糖
2016年的时候,ES6的数组新方法已经慢慢普及了,用友的题目里也出现了map、filter、reduce相关的题。形式一般是给你一段代码,让你说出输出结果,或者让你用指定的方法实现某个功能。
map和forEach的区别是常考的:map会返回一个新数组,forEach只遍历不返回。很多人能用它们写出代码,但一问区别就卡壳。filter的作用是筛选出满足条件的元素,返回新数组。reduce是最灵活但最难掌握的——它可以把一个数组归约为任意值,常用于求和、对象分组、多维数组扁平化。用友有一道题就要求用reduce实现数组扁平化,本质是在遍历的过程中把每一次的当前项拼接进累加器。这个题可以一层层追问:如果数组是任意深度的呢?加个递归就可以解决。
需要特别提到的知识点是parseInt和map组合的坑。有名的一道题是[1, 2, 3].map(parseInt),输出结果是[1, NaN, NaN]。原因很简单,当map调用parseInt时,传给它的是两个参数——当前值和索引,而parseInt接收两个参数——字符串和进制基数。所以第二次调用相当于parseInt(2, 1),进制基数最小是2,传1返回NaN。这个坑到现在依然常见,它考察的是对函数参数的敏感性——有没有意识到map的回调函数会被传入额外参数。
3.3 深浅拷贝:为什么直接赋值不是拷贝
深浅拷贝是笔试常客,用友这套题里也有。考法通常是:给你一个嵌套对象,让你实现深拷贝,或者让你说说直接赋值、浅拷贝、深拷贝三者的区别。
直接赋值obj2 = obj1时,obj2和obj1指向同一个内存地址,改任何一个都会影响另一个。浅拷贝只拷贝第一层属性,如果属性值是对象或数组,那拷贝的还是同一个引用。深拷贝则连嵌套的对象也拷贝,完全独立。
实现深拷贝有几种思路。最简单粗暴的是JSON.parse(JSON.stringify(obj)),但它有几个致命缺点:函数、undefined、symbol会被忽略;日期对象会变成字符串;正则表达式会变成空对象;存在循环引用的时候会直接报错。所以面试时如果你用了这个方法,最好主动把坑说出来,然后再手写一个递归深拷贝:遍历对象的每一项,如果是对象或数组就递归处理,否则直接赋值。
手写递归深拷贝也需要考虑循环引用问题,用一个WeakMap记录已经拷贝过的对象,遇到重复就直接返回引用,避免死循环。这个改进点能体现工程经验,在面试中属于加分项。
3.4 防抖与节流:性能优化的基础武器
前端性能优化里,防抖(debounce)和节流(throttle)是绝对不能不知道的两个概念,用友的题也考到了。防抖的思路是:事件触发之后不立即执行,而是等待一段时间,如果这段时间内又触发了,就重新计时。典型的场景是搜索框输入联想——用户停止输入一段时间后再发请求,避免每敲一个字母都请求一次。
节流跟防抖不同:它是保证在一段时间内最多执行一次。典型场景是滚动事件里做懒加载——用户滚动速度很快,但你不想每次都执行计算,保证每200毫秒最多执行一次就行。
手写实现的话,防抖的核心是一个定时器变量。每次事件触发先清除旧的定时器,再重新设置定时器延迟执行。节流的实现方式有两种——时间戳方式和定时器方式。时间戳实现是记录上次执行的时间,如果当前时间减去上次时间超过设定值就执行;定时器实现类似防抖,但要配合一个标志位,确保定时器存在的时候不再创建新的定时器。我在项目里用的基本是自己封装的这两个函数,Node环境或者浏览器环境通用,处理滚动、输入、窗口resize这些场景非常好用。
4. 从笔试题延伸出去的前端面试准备策略
4.1 版本差异和时代背景:2016年的题放到现在该怎么看
回顾这份笔试题,有些题今天已经属于基础中的基础,有些则因为技术演进不再是主导。比如jQuery相关的内容,2016年几乎是必考,现在反而很少出现在校招笔试题里了。但你不能完全不管它——很多老项目依然在用jQuery维护,你简历上写了熟悉jQuery的话,面试官依然可能揪住你对源码的理解不放。
ES6+的新语法在2016年算是加分项,如今则已经变成默认要求。let/const、箭头函数、解构赋值、Promise、async/await、模板字符串、模块化这些东西,不只是能用,还得明白它们要解决什么问题。比如箭头函数没有自己的this,它继承外层作用域的this,这在React类组件的回调里和事件监听器里特别重要——你不需要再写var that = this了。
现在的笔试题跟2016年相比,多了这些方向:Vue和React框架的原理题、工程化工具链的配置题、TypeScript类型推导题、Node.js相关的后端题。但从本质上看,考察的还是你对前端基础原理的理解深度。框架更新换代很快,但HTML、CSS、JavaScript的核心原理变化不大。所以准备的时候,我的建议是:以原生技术为主干,框架理解为枝叶。
4.2 题型的底层共性:如何用一套方法应对所有校招笔试题
我在看这套题的时候发现,虽然题目很杂,但解题方法有一个共通路径。第一步,读懂题目在考什么——是纯粹的知识记忆,还是需要你现场推理。知识记忆题送分题必须全拿,比如状态码含义、API用法。需要推理的题,比如代码输出类,一定要在草稿纸上把执行过程一步一步写出来,不要凭直觉答题。
第二步,写代码的时候先想边界条件。数组为空怎么办?传参类型不对怎么办?要排序的数组里有重复元素怎么办?很多同学笔试翻车不是不会做,而是代码里没有处理空值和异常。面试官看你的代码,最先看的就是边界情况的处理能力,因为在真实业务中,接口返回的数据千奇百怪,处理不了空数据的前端代码上线必崩。
第三步,做错题之后一定要复盘。这个复盘点不是“哦答案是这个”,而是“我为什么没想到这里是这个”。大部分时候是因为某个知识点存在盲区,或者是对某个API的参数理解不透彻。这时候把这个知识点单独拎出来,找三五道相关的题练熟。
第四步,也是最容易被忽略的——把知识点串成体系。比如闭包连着作用域链和this指向一起理解,事件委托连着事件冒泡和DOM结构一起理解。单点知识容易忘,体系化的知识才不容易忘记。
4.3 每一道题都是一个面试聊天的引子
用友这套题还有一个很有意思的用法——把每一道题当成一个面试话题的起点。比如数组去重那道题,你可以主动展开:如果数据量特别大该选哪个方案?如果数组里有对象元素呢?如果是类数组呢?实际上,这些问题在真实业务中都可能遇到。你提前多想几步,面试的时候就能更主动地引导话题,展现出你有独立思考和深入研究的习惯。
再比如for循环绑定事件的题,你可以顺带说一下现在用let解决了但当时的痛点是什么,也可以提到React和Vue的列表渲染为什么不需要手动处理这个问题——因为框架做了虚拟DOM的diff,事件统一绑到了根节点上。如果你能把这些知识点串起来讲,面试官会觉得你不仅懂原理,还有框架层面的视野。
准备前端校招本质上就是建立知识体系和提高解题速度的过程。这套2016年的用友笔试题值得认真做,但不建议只对着标准答案背,而是要理解每个题目背后的考察逻辑。离面试还有时间的同学,按我上面总结的路径来一遍,效果会很明显。
最后分享一个我自己摸索出来的小技巧:准备笔试的时候,不要只做题,每道题做完之后给自己出一个“变形题”,然后尝试解答。比如原题是数组去重,你可以问自己:去重后还要保持原顺序怎么办?去重的时候要返回重复次数最多的元素怎么办?这样把一个点扩成一类题,覆盖面就大了很多。