☰
用友2017Web前端试题解析:B端大厂面试考点深度拆解
2026/10/31 8:45:48 网站建设 项目流程

这份“用友2017web前端试题(四)”,不是一道题,而是一张通往B端大厂前端岗位的地图。很多人第一次看到这类真题,第一反应是“我靠,怎么考这么细”,第二反应是“网上答案靠谱吗”。我翻出这份旧题重新做了一遍,又结合这些年带团队、面候选人的实际感受,把它拆成了几个真正值得反复琢磨的知识块,顺手把当年踩过的坑也一起写出来。

先说结论:这套题即使放在现在,依然有很强的参考价值。前端技术迭代快,但底层逻辑没变。用友作为国内老牌企业服务厂商,它的Web前端笔试有很大的代表性——不考花架子,不炫新名词,考的几乎全是“能不能在真实业务里扛住事”的基础功。适合谁看?正在准备B端/中大厂前端面试的求职者,带初、中级前端的团队负责人,还有那些想系统性自查前端基础是否扎实的同学。

1. 这套2017年试题到底在考什么

1.1 用友出题的核心思路:稳定大于一切

用友这种体量的公司,核心业务是ERP、云服务、数据管理平台,前端面对的场景几乎都是数据密集型页面——复杂表单、多级联动、超大表格、权限分配、审批流。在这种业务下,页面崩一次、数据错一条,损失都是真金白银。所以它的前端笔试,会天然地倾斜向三类能力:

  • JavaScript语言基础是否扎实,尤其作用域、闭包、原型链、异步这些底层机制;
  • 浏览器工作原理是否清晰,包括渲染过程、缓存策略、性能优化手段;
  • 工程化意识和代码组织能力,能不能写出可维护、可扩展的代码。

这套题之所以经典,就是因为它鲜明地体现了“业务倒逼技术”的选人逻辑。它不会直接问你“最新框架的API”,而是考察你在纯原生环境下如何解决问题,然后再看你对框架机制的底层理解。2017年恰好是Vue 2和React逐渐扛起大旗的年份,但用友这种老牌厂商的代码库里,存量jQuery/原生JS项目依然很多,新老技术栈并存是常态。所以试题里会大量出现“既考察现代语法、又要求原生实现”的内容,这正是当时真实工作的缩影。

1.2 2017年前端技术栈的时代背景

要把这份试题的价值吃透,必须先还原一下2017年的技术环境。那一年,ECMAScript 2015(ES6)已经普及了两年多,Babel成了标配,let/const、箭头函数、解构赋值、模板字符串这些不再算“新特性”。同时,Webpack 3是主流,Vue 2.4/2.5、React 15/16正在大面积落地,TypeScript还没有成为大多数团队的刚需,CSS预处理器(Sass/LESS)已经是常规操作,但CSS-in-JS只是极少数人的玩具。

这个时代背景是怎么映射到试题里的?核心观察是:这份题既没有停留在“能不能写出兼容IE8的ajax”这种远古层面,也没有激进到“考察Server Component、Hooks源码”这种极新层面。它的落点在“新语法你会不会用,以及用的时候知不知道原理”。比如,它可能让你用ES6语法实现某种继承,但又追问原型链的底层关系;它可能让你谈谈Promise与async/await的关系,但重点考察的是宏任务与微任务的执行顺序。这种出题方式,说明了当时中大型企业前端团队的真实状态——已经在使用新工具链,但必须对旧浏览器和传统架构负责,所以“知其然,更知其所以然”是面试的核心筛选标准。

1.3 从2017年到现在的考点演变观察

我后来重新复盘这份试题时,发现一个很有意思的现象:近七年过去,前端面试的“道”没有变,“术”却一直在更新。用一句话概括就是——考点永远在变,但考察底层能力这件事从不改变。

比如,2017年问“如何实现数据监听与双向绑定”,是因为Vue 2的Object.defineProperty是当时最热门的话题;现在你再问这个问题,面试官会追问Proxy和defineProperty的差异、在Vue 3里为什么被替换。2017年问“描述一下从输入URL到页面渲染的完整过程”,答案里要包含DNS解析、TCP连接、构建DOM树、CSSOM、渲染树、回流重绘这些步骤;现在提问,可能还要补充HTTP/2多路复用、QUIC协议、页面预渲染等新维度。

所以,这份旧试题不应该被当成“背题资料”,而更应该被当成“前端基础能力体检表”。用它的考点逐项自查,比刷一百道新题更能暴露自己的薄弱环节。下面我把这份试题涉及的核心考点、答题思路和典型的“送命题”拆解一遍。

2. 核心考点拆解与答题思路

2.1 基础语言必考点:闭包、作用域、原型链

这类题目几乎占据了用友试题三分之一的篇幅。看起来是基础,但拉开差距的恰恰是这些地方。

先说闭包。面试题里最常见的形态是“某段循环代码里用var声明变量,点击每个按钮输出索引,结果都是最后一个”。这道题的变形版本有很多,但核心考察点是:你是否理解闭包捕获的是变量的引用,而不是值。当年我面过不少候选人,能背出闭包的定义“函数内部访问外部作用域变量的能力”,但一落实到实际代码就答错,或者只会在循环里用let解决,说不出原理。注意,用let当然可以,因为let有块级作用域且每次迭代都会创建新的绑定;但如果面试官追问“如果项目必须兼容旧浏览器怎么办”,你需要流畅地答出用IIFE(立即执行函数)或再包一层函数来保存每次循环的i值。

再说原型链。常见的考察方式是给出一个对象/函数的层层访问关系,让你判断输出。这里我提供一个比较稳妥的答题顺序:先画出原型链的链路图(实例的__proto__指向构造函数的prototype,构造函数的prototype又指向其原型的__proto__,最终到null),然后逐个属性查找,注意“自有属性优先于原型属性”的规则。当年这道题的扩展考点还包括instanceof的原理、new操作符的执行过程、class语法和传统构造函数的等价关系。我强烈建议你把new的四个步骤背到条件反射:创建空对象、把该对象的__proto__指向构造函数的prototype、改变this指向并执行构造函数、如果函数返回的是对象则返回该对象否则返回新对象。这四步搞清楚了,很多题都能迎刃而解。

关于this指向,简单总结就是:谁调用就指向谁,没人调用就指向window(严格模式下undefined),箭头函数不绑定this、往外层找。但常见的笔试陷阱是“调用时被拆成方法引用”或“setTimeout/事件回调里的this”,这两个场景即使工作了多年的人也容易翻车。我的建议是答题时先标明“该位置是普通函数还是箭头函数”,再判断调用者,一步步来就不会乱。

2.2 浏览器与性能优化:渲染原理、缓存、网络

用友的Web产品大多部署在企业内网或通过私有云交付,页面加载速度和稳定性是客户直接感受的“门面”。所以试题里必有浏览器工作机理和性能优化相关的考察,而且往往以“场景题”的形式出现。

经典的“从输入URL到页面渲染完成”,答题时建议按阶段展开:

  1. DNS解析,得到目标服务器IP;
  2. 建立TCP连接(可能还要加上HTTPS的TLS握手);
  3. 发送HTTP请求,服务器返回HTML;
  4. 浏览器解析HTML,生成DOM树;同时解析CSS,生成CSSOM树;
  5. 两者合并生成渲染树(Render Tree),注意display:none的元素不会出现在渲染树上,但visibility:hidden会;
  6. 根据渲染树计算布局(Layout/Reflow);
  7. 绘制(Paint),最后呈现在屏幕上。

关于重排(Reflow)与重绘(Repaint),最常见的追问是“哪些操作会触发重排”。我的回答习惯是:凡是修改元素的几何属性(尺寸、位置、display显示隐藏)都会触发;读取某些属性(如offsetHeight、getBoundingClientRect)也会强制同步重排。所以优化策略是尽量批量修改样式、用class切换代替逐条修改style、把动画元素移到独立图层(transform和opacity不会触发布局)。这道题现在问得少了,但作为笔试填空或简答还是很常见,值得好好准备。

缓存策略这块,2017年问得最多的是“强缓存与协商缓存的区别”,以及Expires、Cache-Control、Last-Modified、ETag这四个头部字段的优先级配合。要特别注意两个易错点:Expires是绝对时间,Cache-Control: max-age是相对时间,后者的优先级更高;Last-Modified精度是秒,ETag能更精准地应对文件内容没有变化但修改时间变了的情况。再深入一点,还会考你“刷新页面(F5)强缓存还会生效吗”“点击链接跳转和手动输入地址时缓存策略有什么差异”。这些细节直接用表格背下来即可:

缓存类型判断依据优点缺点优先级
强缓存Cache-Control/Expires不发请求,速度最快资源更新可能不及时先判断
协商缓存ETag/Last-Modified能确认资源是否变化需要发送请求验证强缓存未命中时
无缓存Cache-Control: no-cache每次请求都会验证加载速度受影响—

2.3 框架与工程化:数据绑定、组件通信、构建流程

2017年的面试题里,已经大量涉及MVVM框架的使用与原理。用友的试题不会直接考“Vue和React谁更好”这类主观题,而是考更实操的东西:数据绑定的实现机制、父子组件通信方式、生命周期函数的使用场景、模块化规范差异。

以Vue为例,当年最高频的考点是:“Vue双向数据绑定的原理是什么?”答案是:通过Object.defineProperty劫持数据对象的属性访问,结合发布订阅模式,当数据变化时通知Watcher更新视图。这里有一个当年非常容易扣分的点:Object.defineProperty无法劫持新增属性和通过索引修改数组元素,所以Vue 2提供了Vue.set(this.$set)来解决。现在你还可以补充:Vue 3改用Proxy后,新增属性和数组索引都可以被直接监听,这也是Proxy对defineProperty的重要优势。这种“新旧对比”式的回答,在技术演进类问题上非常加分。

组件通信也是必考内容。我建议按“父子、兄弟、跨级”三个场景来准备作答:

  • 父子通信:父传子用props,子传父使用事件触发($emit);
  • 兄弟通信:借助父组件作为中转,或者用事件总线(EventBus);
  • 跨级通信:Vue 2.x可以用provide/inject,React则可以用Context。

工程化方面,最经典的是“webpack的Loader和Plugin有什么区别”。这题我会先用一句话下定义:Loader是加载器,用于对模块的源代码进行转换,所以它是“文件处理流水线”;Plugin是插件,范围更广,可以监听打包过程中的任何事件,执行任何自定义任务,比如资源清理、代码压缩、Bundle分析。再举几个典型例子:babel-loader把ES6转成ES5,file-loader处理图片和字体资源,HtmlWebpackPlugin自动生成HTML并根据依赖关系注入script和link标签,MiniCssExtractPlugin把CSS抽离成独立文件。这个回答顺序既体现了概念区分,又展示了实际使用经验。

2.4 综合应用与手写代码:编程题的答题策略

这套试题的末尾,通常有一两道手写代码题,常见的是数组去重、防抖节流、深拷贝、排序算法、Promise串行执行等。这些题形式固定,但有不少细节坑,写之前一定要先想清楚。

我拿数组去重举例。最直观的方案是使用Set:[...new Set(arr)],一行搞定,但很多面试官会追问“如果元素是NaN怎么办”“Set去重的判断规则是什么”。实际上Set内部使用严格相等(SameValueZero)机制,NaN会被认为是相同的,所以new Set([NaN, NaN])只有一个NaN,这一点很多人不知道。另一种常见方案是用filter和indexOf,但注意indexOf对NaN无法正确判断(因为indexOf内部使用严格相等),所以[NaN].indexOf(NaN)的结果是-1。如果要兼容NaN,可以用includes或findIndex替代。这种“一道简单题里能挖出多个底层知识点”的考察方式,正是用友这类厂子偏爱的。

再说防抖与节流。别看这两个函数被写烂了,真正能在笔试中一次写对的人不多。以一个简易防抖为例:

function debounce(fn, delay = 300) { let timer = null; return function (...args) { const context = this; clearTimeout(timer); timer = setTimeout(() => { fn.apply(context, args); }, delay); }; }

关键点有三个:每次调用都要清除上一次的定时器、必须用apply保留this指向、返回值要作为新函数返回。节流也一样,常见的实现方式是时间戳或定时器,更需要关注的是“第一次触发是否立即执行”“最后一次触发是否补执行”这两个边界。如果笔试时间够,我建议把这两种版本(立即执行版和尾执行版)都背下来,面试时能说出来龙去脉,会明显比只写一个通用版本更有竞争力。

3. 实操过程与核心环节实现

3.1 拿到一套笔试题后,先做什么

很多同学拿到试卷就急着从头做到尾,这个习惯在面试场合很吃亏。我自己在面人的时候,最怕看到“前面选择题磨叽太久,最后一整道编程题空白”的答卷。正确的做法是:先花2-3分钟快速扫一遍全部题目,对题型和分值做心理预估,优先把会做的、分值大的题目做稳,再回头啃难题。

以用友这套题(四)来模拟,一般结构是:单选/多选、简答、编程、以及一到两道场景设计题。建议分配时间是这样的:选择题控制在总时间的30%以内,简答题不要长篇大论,按“定义+原理+用途/例子”的结构写三到五行,把最核心的得分点放在第一句。编程题是最大的分水岭,一定要留足40%的时间,即使写不出完整代码,也要把思路、伪代码、边界条件写清楚。要知道,面试官看卷子时,最在意的不是完美答案,而是“这个人遇到问题时的抵达过程”。一个写了一半但思路正确、注释清晰的人,比一个留白的人要强太多。

3.2 一道典型编程题的全过程拆解

我挑一道当年这套题中很典型的手写实现题,展开讲讲我的完整作答过程——“实现一个简单的深拷贝”。

第一步,不要急着写代码,先在草稿区列清需求边界:要拷贝的对象可能包含数组、普通对象、函数、Date、RegExp、循环引用等特殊情况,代码要能尽量兼容。

第二步,设计主逻辑。可以用递归判断类型,基本类型直接返回,引用类型判断是数组还是对象,并创建对应容器。如果是Date、RegExp这类特殊对象,要有独立的分支处理。循环引用可以用WeakMap(或Map)来记录已拷贝过的对象,避免栈溢出。

第三步,写出一版相对完整的代码。参考实现如下:

function deepClone(target, map = new WeakMap()) { if (target === null || typeof target !== 'object') { return target; } if (target instanceof Date) { return new Date(target.getTime()); } if (target instanceof RegExp) { return new RegExp(target.source, target.flags); } if (map.has(target)) { return map.get(target); } const cloneTarget = Array.isArray(target) ? [] : {}; map.set(target, cloneTarget); Reflect.ownKeys(target).forEach((key) => { cloneTarget[key] = deepClone(target[key], map); }); return cloneTarget; }

这里要注意几个细节:用Reflect.ownKeys()可以同时遍历字符串属性和Symbol属性;用WeakMap做记录,既能解决循环引用,又不会像Map那样强引用导致内存泄漏。写完后,还要留一句注释说明“生产环境可使用lodash的cloneDeep”,既展示了原理功底,又体现了工程意识,这对面试官来说是非常加分的。

3.3 简答题的“金字塔”回答结构

简答题最怕写“作文”,洋洋洒洒一大堆,却抓不住重点。我推荐一种金字塔回答结构:结论先行,原因其次,例子垫后。这样即便面试官没有时间读完所有内容,也能一眼抓到你最核心的认知。

举个试题中可能出现的例子:“简述浏览器事件代理(事件委托)的原理。”

我的回答逻辑是:

  • 第一句给结论:事件代理是利用事件冒泡机制,将子元素的事件处理函数绑定到父元素上,统一处理;
  • 第二句说原理:当子元素触发事件后,事件会沿着DOM树向上冒泡到父元素,父元素通过event.target判断实际触发元素;
  • 第三句举使用场景:列表中有大量相同按钮时,使用事件代理可以避免给每个按钮绑定监听器,减少内存占用,同时新添加的子元素无需重新绑定事件;
  • 第四句点注意事项:有些事件(例如focus/blur)不冒泡,不适合直接用事件代理,可以用捕获阶段或focusin/focusout处理。

这样四句话,信息密度高,逻辑清晰,就算遇到追问,也有足够的展开空间。

3.4 面对“超纲题”的临场应对

笔试时最崩溃的一刻,不是遇到不会的题,而是遇到“这个名词我压根没听过”的题。我在多年面试经验里总结出的一个原则是:绝对不空题,绝对不假装会。空题意味着零分,假装会则可能被追问后彻底暴露。正确策略是把题目改写成自己掌握的知识。

举例:如果题目问“请解释CSS中的BFC”,你即使一时回忆不起全称“Block Formatting Context”,也可以先写出“块级格式化上下文”,再列出触发条件(overflow不为visible、float、position为absolute/fixed、display为inline-block等),最后说它的作用是隔离布局、避免margin重叠、阻止元素被浮动元素覆盖。哪怕只写出两三条,也比完全不写强。再比如问“谈一谈virtual DOM”,你可以从“新旧vnode的diff对比”“为什么需要key”“批量更新”等任意一个角度切入,只要展示出真实的思考链条即可。

4. 常见问题与排查技巧实录

4.1 高频“翻车”点:基础理论张冠李戴

改过很多份应聘者的笔试卷,我发现大家翻车最集中的地方,其实是“基础理论张冠李戴”。最典型的是:把重排和重绘混为一谈、把深拷贝和浅拷贝的引用关系描述错误、把强缓存和协商缓存的触发时机说反、把map和forEach的区别讲不清楚。

这里我建议做一张容易混淆概念的速查表,考前快速过一遍:

易混淆概念核心区别
重排 vs 重绘重排涉及布局计算,重绘只涉及像素刷新,重排一定导致重绘,重绘不一定导致重排
深拷贝 vs 浅拷贝浅拷贝只复制对象第一层引用,嵌套对象仍然共享;深拷贝递归复制所有层级
函数声明 vs 函数表达式函数声明会提升整个函数体,表达式只提升变量声明,函数体按赋值顺序生效
target vs currentTargettarget是事件实际触发的元素,currentTarget是当前绑定事件的元素
宏任务 vs 微任务微任务在本次宏任务执行完立即执行,优先级高于下一个宏任务

自己动手整理这张表,本身就是一次记忆强化,比考前刷十道题还管用。

4.2 手写代码常见的三个隐藏坑

手写题的隐藏坑比很多人想象的多得多。第一个坑是不检查异常参数。比如手写数组扁平化时没考虑空数组,或手写节流时没处理fn不是函数的情况,健壮性差。第二个坑是没考虑this指向。尤其是对象的解构赋值后调用方法,this会丢失;回调函数里使用普通函数更是重灾区。第三个坑是忽略返回值。比如手写debounce时,有些版本内部没有return一个包装函数,导致调用后不执行。这些细节一眼就能看出写代码的习惯,所以写完一定养成“自测”的习惯——在脑子里跑一遍典型的输入输出,或用几分钟在草稿纸上写几个console.log验证。

4.3 时间不够时,如何保住基础分

笔试时间永远是不够用的,所以必须学会“战略性放弃”。我的经验是:如果做完选择题和简单题后只剩下10分钟,就不要纠结编程题能不能全对了。此时应该做两件事——第一,把当前已经写了一半的代码快速补上注释和基本结构,因为即使运行不通过,面试官也能从中看到你的思路;第二,在代码下方用两行话写清楚你的思路和坚持要使用的数据结构,比如“我的思路是先排序、再双指针去重”,这样阅卷时至少能拿到思路分。永远不要留白,这是我反复强调的。

4.4 考后的复盘方法:从做题到建立知识地图

考试结束后,比起对答案,更重要的是复盘。我通常建议求职者用半天时间做三件事:先把错题按“完全不会”“知道但记不清”“看错题”“时间不够”四类分类;再针对前两类,找到对应知识点的权威文档(MDN、官方文档、源码解析)重读一遍并写摘要;最后把知识点汇总成一张自己的思维导图或笔记,标注哪些知识点之间有联系。

举个例子,如果你在做题时发现“事件循环”掌握不好,那么复盘时不仅要看宏任务和微任务的基本规则,还应该延伸到async/await、Promise、requestAnimationFrame、Node.js中的process.nextTick等上下游知识点,把它们串成一条线。这样一来,一道旧题就不再是一个孤立的知识点,而是成为了你知识网络上的一个重要节点,下次再被问到,你能从容展开。

5. 后续可以怎么迭代这套真题的价值

这份用友2017年web前端试题(四)放到今天,最有价值的用法不是背题,而是当“体检报告”。我建议你按上面的模块做一遍,然后统计自己在哪个部分失分最严重。如果JavaScript基础失分多,就把重心放回闭包、原型链、异步;如果浏览器原理失分多,就系统补一遍Chrome DevTools的Performance面板和Network面板实操;如果工程化失分多,就亲手把webpack从零配置一遍,把Loader和Plugin的差异用到熟练。

从更长期的角度看,我建议你建立一份属于自己的“面试考点清单”,每个季度更新一次。这样不管技术栈怎么变、面试形式怎么换,你都能保持稳定输出。我在实际带团队时发现,那些能定期用旧题反哺自己知识体系的工程师,往往也是成长速度最快的人。希望你也能从这份旧题里,看到自己的短板,也看到自己的潜力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询