ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用友前端校招笔试解析:从事件循环到深拷贝,一篇搞定JS核心考点

用友前端校招笔试解析:从事件循环到深拷贝,一篇搞定JS核心考点 1. 这一轮笔试的出题风格企业软件厂商想筛出什么样的前端用友2019校招web前端笔试题四是当年那一轮校招里我印象最深的一套题。前面几套卷子多少还会考一些HTML标签语义、CSS选择器优先级、HTTP状态码这类常识性内容到了这一套重心几乎全压在JavaScript最核心的机制上作用域、闭包、事件循环、手写代码再加上一道浏览器从输入URL到页面展示的完整链路大题。整套题做完最大的感受是它不考你会不会用某个框架的API而是考你对一门语言和浏览器运行机制的理解有多深。文章适合两类人看正在准备用友或其他企业级软件厂商前端校招的同学以及想系统补一补JavaScript基础、对手写代码题没底的人。1.1 用友这类厂商和互联网公司前端考察的差异很多人备考前端校招习惯性按互联网大厂的节奏刷算法题这套思路放在用友这类企业服务软件厂商身上并不完全适用。用友的核心产品是ERP、财务、人力、供应链等企业软件前端每天面对的是复杂表单、数据表格、报表展示、权限控制、流程审批这类页面。这些页面有一个共同特点单个页面里JavaScript交互逻辑密度极高状态多、数据量大、兼容性要求严格。笔试题出什么往往就是这些真实业务倒推出来的能力要求。所以对比下来用友这套卷子和互联网公司有明显区别大厂可能拿一道hard级算法题来筛选用友更倾向于在JavaScript语言机制、手写代码、浏览器渲染原理这些前端基本功上做文章。题目本身不偏不怪但覆盖面很宽从变量提升到事件循环从防抖节流到深拷贝从DOM操作到渲染性能全都要懂。如果你只会框架用法而说不清底层原理在这套卷子里拿不到高分。1.2 第四套卷在整个校招笔试流程里的定位四这个编号通常代表题库里的第四套平行卷。校招高峰期企业会把多套试卷随机分发给不同批次的候选人题目难度做过均衡所以这套卷可以看作是当年web前端方向笔试题库的一个切片。从内容编排来看它承担的是第二轮筛人任务第一轮笔试往往考通识包括HTML/CSS、计算机网络、基础算法到了这一轮就要看候选人能不能在JavaScript语言层面独立解决实际问题。这套卷里没有太多死记硬背的概念题更多是给你一段代码说出输出结果和给你一个需求把代码补全这种形式。换句话说它筛的不是看过多少书的人而是真写过代码、踩过坑、理解运行机制的人。后面几章我会把这套卷里出现频次最高的几个考点逐项拆开每一类都给出典型题面、完整推导和答题要点。2. 作用域、闭包与事件循环JS输出题的两个层级输出题是这套卷的第一道大菜也是很多人在考场上最容易翻车的部分。它的考察方式很简单给你一段代码让你写出控制台输出顺序。但代码里往往同时埋了作用域、闭包、宏任务、微任务、async/await这些机制任何一环理解不透输出结果就会错。2.1 第一层级var、let和闭包共享变量先说最经典的闭包陷阱题目我在当年这套卷里见过它的多个变形版本。for (var i 0; i 5; i) { setTimeout(function () { console.log(i); }, 100); }答案不是0 1 2 3 4而是5 5 5 5 5。原因在于var声明的i是函数作用域整个循环共用同一个变量setTimeout的回调函数形成闭包引用的是同一个i。当100毫秒后回调执行时循环早已结束i已经变成5所以五个回调打印出来的全是5。题目改成let声明之后输出就变成了0 1 2 3 4。底层原因是let在for循环的每一次迭代中都会创建一个新的词法环境每个闭包绑定的是当次迭代的i互不干扰。除了改成let用IIFE也能达到同样效果for (var i 0; i 5; i) { (function (j) { setTimeout(function () { console.log(j); }, 100); })(i); }这里利用立即执行函数把每次的i作为参数传入新的函数作用域闭包捕获的是参数j自然就隔离了。这道题有一个容易踩的延伸坑如果把var改成const呢在普通的for循环里const声明循环变量会在第二次迭代执行i时抛错因为常量不允许重新赋值。只有for...of或for...in中的const才会为每次迭代创建新绑定。这个细节在考卷里出现得不多但答对很能体现功底。2.2 第二层级宏任务、微任务与async/await执行顺序如果第一层级只是开胃菜那第二层级直接进入事件循环机制。这套卷的进阶版输出题长这样console.log(start); setTimeout(function () { console.log(timeout); }, 0); Promise.resolve().then(function () { console.log(promise); }); console.log(end);输出顺序是start、end、promise、timeout。这里涉及事件循环里最核心的规则同步任务先执行微任务在当前宏任务执行完后、下一个宏任务开始前清空setTimeout的回调属于宏任务要排在微任务后面。用生活化的方式类比宏任务是排队的顾客微任务是顾客结账时顺手提出的小要求。收银员必须把当前顾客的所有小要求处理完才叫下一位顾客。所以即使setTimeout的延迟时间设成0它也要等当前宏任务和全部微任务执行完才有机会运行。进阶版本会把async/await也搅进来async function async1() { console.log(async1 start); await async2(); console.log(async1 end); } async function async2() { console.log(async2); } console.log(script start); async1(); setTimeout(() console.log(timeout), 0); new Promise(resolve { console.log(promise); resolve(); }).then(() console.log(then)); console.log(script end);正确的输出顺序是顺序输出说明1script start同步代码2async1 startasync1函数体同步执行3async2await右侧的函数调用同步执行4promisenew Promise的executor同步执行5script end同步代码收尾6async1 endawait之后的部分作为微任务继续7thenPromise的then回调微任务8timeoutsetTimeout回调宏任务这里最容易出错的是async1 end和then的顺序。await async2()的意思是先同步执行async2()然后把async1 end这段后续逻辑挂到微任务队列而代码里new Promise的then也是微任务。这里有个容易忽略的细节async2()作为await右侧的表达式它内部的console.log(async2)是同步执行的但await之后的代码会被推迟。微任务队列里async1 end先被放入所以它比then先输出。2.3 输出题答题时的推导方法与常见丢分点这类题在卷面上是选择题或填空题但答题时千万不要凭感觉选。我的习惯是在草稿纸上画一条时间线把同步代码、微任务队列、宏任务队列分别列出来每执行一步就划掉一步。这样虽然慢但能保证万无一失。常见丢分点主要是三个第一忽略new Promise的executor是同步执行的很多人以为Promise相关代码全是异步导致promise的输出位置错第二混淆微任务和宏任务的优先级以为setTimeout先于Promise.then执行第三没有注意到await右侧的表达式是同步求值的只记住了await后面是异步的这句口诀结果把async2的输出位置放错。备考建议是把这几道经典变形题动手跑一遍Node环境或者浏览器控制台都行看输出结果和预期是否一致。跑通之后再做变形例如把Promise.resolve().then换成多个嵌套的then把setTimeout的延迟时间改成不同值多错几次自然就理解事件循环了。3. 手写代码题防抖、节流、深拷贝这么写才稳手写代码题是这套卷的重头戏分数占比很高。用友这类企业软件的前端开发日常工作中最常用到的不是排序算法而是防抖节流、深拷贝、数组去重、事件代理这些基本功。手动实现一遍比背十道概念题都有用。3.1 为什么这套卷必考手写题企业级页面里搜索框输入要防抖滚动加载要节流表单编辑前要把原对象深拷贝一份用来做撤销这些需求几乎是每天都要写的代码。笔试考手写题其实是在模拟真实工作场景考察候选人有没有真正写过、理解过这些功能而不只是知道名词。代码题还有一个隐藏优势能看出代码风格。阅卷人会看你变量命名是否清晰、边界条件有没有考虑、代码有没有多余的重复逻辑。这些细节在校招候选人里差别很大写过项目的人和只刷过题的人代码一眼就能分辨。3.2 防抖与节流的完整实现与场景辨析防抖的实现思路是事件触发后不立即执行而是等待一段空闲时间如果期间再次触发就重置计时器。核心代码如下function debounce(fn, delay 300, immediate false) { let timer null; return function (...args) { const callNow immediate !timer; clearTimeout(timer); timer setTimeout(() { timer null; if (!immediate) { fn.apply(this, args); } }, delay); if (callNow) { fn.apply(this, args); } }; }有几个细节必须注意第一fn.apply(this, args)不能省this要指向实际调用者event对象等参数要完整透传第二定时器回调里要把timer置为null否则immediate模式下第二次触发时callNow判断会出错第三immediate参数控制是否立即执行一次搜索框请求场景经常需要这个能力。节流实现思路是限制函数在固定时间间隔内最多执行一次。基于时间戳的实现如下function throttle(fn, interval 300) { let last 0; return function (...args) { const now Date.now(); if (now - last interval) { last now; fn.apply(this, args); } }; }防抖和节流的配套题通常是问各自适用场景。防抖适合连续操作结束后执行一次的场景比如输入框搜索、窗口大小调整节流适合固定频率执行的场景比如滚动加载、按钮点击防连点。答题时如果能把这两个场景区分清楚再结合页面里的具体例子分数会比只写代码高不少。3.3 深拷贝的实现和阅卷人藏在后面的考点深拷贝是手写题里的分水岭。只写JSON.parse(JSON.stringify(obj))能得一半分但拿不到满分。原因是JSON序列化方案有一堆硬伤undefined、函数、Symbol会被直接丢弃Date会被转成字符串循环引用会直接抛错。企业级业务对象里这些情况太常见了所以出题人真正期待的是一个处理了边界情况的完整实现。参考实现如下function deepClone(target, map new WeakMap()) { if (target null || typeof target ! object) { return target; } if (target instanceof Date) { return new Date(target); } if (target instanceof RegExp) { return new RegExp(target.source, target.flags); } if (map.has(target)) { return map.get(target); } const clone Array.isArray(target) ? [] : {}; map.set(target, clone); Reflect.ownKeys(target).forEach(key { clone[key] deepClone(target[key], map); }); return clone; }这份代码里的每一个判断都对应一个考点。用WeakMap解决循环引用问题是因为WeakMap中的键是弱引用不会造成内存泄漏用Reflect.ownKeys遍历键是因为它能拿到Symbol键和不可枚举的属性Object.keys拿不到先判断Date和RegExp是因为这两个对象有内部槽普通遍历会得到空对象。阅卷人看这道题通常先扫一眼有没有做循环引用处理再确认特殊对象是否处理最后看遍历方式。能写出Reflect.ownKeys这个API的人说明对ES6特性掌握得比较系统这一项在评价里很加分。如果觉得上面版本太复杂至少也要写出处理循环引用和数组的基本版本比纯JSON方案高一个档次。4. 浏览器渲染链路一道简答题值不值得展开成一篇小论文这套卷的简答题部分最典型的一道是从在浏览器地址栏输入URL到页面展示中间发生了什么。这是一个经典面试题但很多人答得只覆盖了网络请求部分渲染部分三句话就带过了分数自然不高。4.1 标准流程该怎么答完整的流程可以分成网络链路和渲染链路两段。网络链路是DNS解析域名得到IP地址建立TCP连接并完成三次握手如果是HTTPS还要额外进行TLS握手然后发送HTTP请求服务器返回响应报文。渲染链路是浏览器拿到HTML文本后边解析边构建DOM树解析过程中遇到CSS就构建CSSOM树遇到JavaScript会阻塞解析执行完脚本才继续之后把DOM和CSSOM合并成渲染树计算布局最后绘制到屏幕上。答题时最容易犯的毛病是答得像背书点到解析HTML、渲染页面就结束。想拿高分至少要体现出三个层次网络请求和渲染是先后衔接的两个阶段渲染树不等于DOM树它只包含可见元素JavaScript会阻塞HTML解析所以有了async和defer两种加载方式。4.2 渲染性能优化从理论到用友复杂表格场景这套卷在简答题后面通常会跟一个小问结合具体业务场景说说你会做哪些渲染优化。这里的业务场景不会说得太细但结合用友的产品特征答题时可以往复杂数据表格方向靠。假设页面上有一个几千行数据的表格每一行都有按钮交互直接全部渲染会导致页面卡顿。优化思路有几个方向分页或者虚拟滚动只渲染可视区域的行数据更新时避免频繁操作真实DOM用DocumentFragment批量操作对不变的数据做缓存避免重复渲染。这类场景化答题比泛泛而谈减少重排重绘要具体得多也更能打动阅卷人。重排和重绘的区分也是高频考点。改变元素的width、height、left、top等布局属性会触发重排重排必然导致重绘只改变color、background等外观属性则只会触发重绘。优化手段包括用transform代替top/left做动画、读写分离避免频繁强制同步布局、用class批量修改样式而不是逐条修改style属性。4.3 缓存与网络层面的加分回答如果想在这个题里继续加分可以补上网络层面的优化静态资源用CDN分发通过强缓存和协商缓存减少重复请求HTTP/2多路复用降低连接开销资源文件压缩和按需加载减小首屏体积。这一串知识点并不难但能把网络和渲染串起来回答的人不多。回答这类题的策略是先把主干流程分阶段说清楚再挑两三个细节深入展开最后落到业务场景给优化方案。这样的答案框架完整、层次分明比想到哪写到哪的答案得分要高。5. Vue与工程化这套卷里占比被低估的选项题用友2019年前端团队的主力技术栈是Vue所以这套卷的客观题部分出现了不少Vue相关内容。虽然单题分值不高但数量多加起来占比不可忽视。备考时如果只顾着刷JS题而忽略框架题这部分很容易白丢分。5.1 v-if/v-show与computed/watch的答题模板v-if和v-show的区别是Vue题里出现频率最高的一道。先说结论v-if是真正的条件渲染值为false时元素不会被渲染到DOM中v-show则是无论如何都会渲染再用display: none隐藏。切换开销上v-if有更高的切换开销因为它涉及组件的销毁和重建v-show有更高的初始渲染开销因为所有元素都要先渲染一遍。使用场景的标准答法是频繁切换用v-show切换频率低或者条件在初始渲染后基本不会变的用v-if。答题时可以补充一点v-if在切换时会触发组件的beforeDestroy和destroyed生命周期这在真实业务中可能会导致事件监听器没有清理引发布内元素的事件泄漏所以高频切换场景优先选v-show。computed和watch的对比也经常出现在同一道题里。computed有缓存只有依赖的响应式数据变化时才会重新计算适合用于派生状态比如根据商品数量计算总价watch适合执行异步操作或开销较大的操作比如监听路由变化触发数据请求。答题时可以套用一句话能写在computed里的逻辑就不放在watch里watch只做它擅长的事。5.2 组件通信、生命周期与webpack的常见考法组件通信这道题用友的考法比较务实给一个父子组件场景问数据怎么传递。基础答案是props和$emit进阶答案要补充provide/inject、$refs、事件总线以及Vuex。给出场景分析更稳妥跨多层级组件用provide/inject复杂状态管理用Vuex简单状态没必要全局管理。生命周期题目主要考created和mounted的区别。created时数据已经初始化但DOM还没生成所以不能在这里操作DOMmounted之后组件被挂载到DOM上可以拿到真实DOM元素。数据请求放在created还是mounted都有道理但更标准的答法是初始化数据并需要尽快展示时放created依赖DOM完成的操作放mounted。工程化方面webpack的loader和plugin区别是高频题。loader用于对模块的源代码进行转换比如把ES6转成ES5、把SCSS转成CSSplugin解决的是loader无法实现的其他事比如打包优化、资源管理、环境变量注入。答题时用一个例子说明就够了例如babel-loader和HtmlWebpackPlugin分别负责什么。6. 复盘时间分配、答题踩坑和笔试后的准备技术点说完了聊聊实战层面的事。校招笔试不光是考知识储备还是一场时间管理考试。以这套卷子90分钟的时长来算合理的分配策略很重要。6.1 全程时间分配与题目优先级我的建议是选择题和填空题控制在20分钟内不要在任何一道题上纠结超过两分钟简答题控制在20到25分钟重点把框架写清楚手写代码题留出30分钟以上这是整套卷分值最集中、也最看得出水平的部分最后留15分钟检查重点回看输出题有没有粗心选错。碰到不会的选择题先标记跳过不要浪费时间。手写代码题如果第一遍写得不完整也先跳过做后面的最后再回头补边界情况。这套卷的计分方式里代码题的边界情况处理通常占了相当比例的分值宁可多花一点时间在补全deepClone的循环引用判断上也不要浪费在纠结某个CSS属性的取值上。6.2 那些让我丢分的低级错误复盘当年的答题过程有几个低级错误很典型。第一个是输出题没在草稿纸上推演直接凭印象选结果因为微任务和宏任务的顺序判断失误丢分。第二个是手写防抖时只写了核心逻辑忘记处理this指向和参数透传因为这种代码在本地环境跑不出来阅卷人一看就知道缺了这两项。第三个教训是简答题写得太教科书。回答输入URL到页面展示时如果只条目化地写DNS解析、TCP连接、HTTP请求、渲染没有把每一阶段的关键机制解释清楚给人感觉就是背过但没有真正理解。后来我调整了答法每一条都配一两句原理说明得分明显提高。6.3 笔试答案如何变成面试素材笔试结束后不要直接撒手不管。我在等面试通知的那几天把笔试里的输出题全部在Node环境跑了一遍核对自己的答案和实际输出是否一致把深拷贝实现里用到的每个API都查了一遍确保面试被追问时能讲清楚为什么这样设计。事实证明这一步非常值。用友的面试官在面试时会直接追问笔试中的手写代码题比如你刚才写了防抖如果我想让第一次点击立即执行你会怎么改或者你的深拷贝是怎么处理循环引用的。因为提前复盘过这些问题回答得很顺。最后的经验是笔试不只是笔试它是在为面试整理自己的知识档案。考完立刻把题目转成文档标出自己答错的、模糊的、完全不熟悉的点按优先级补齐。这套方法我后来在别的公司校招中也沿用了效果一直不错。能把这些题吃透后面面试遇到类似问题心里完全有底。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进