考过百度校招笔试的同学应该都有印象,那份《百度2018校招Web前端工程师笔试卷(第二批)》在当年的求职圈里被传得很广。和很多公司直接甩一套LeetCode题不同,百度的这套卷子更偏向于考察基础功底的扎实程度,JS闭包、事件循环、渲染机制、手写代码这些老生常谈的东西,换着花样反复考。我当初刷这套题的时候,最大的感受就是:很多知识点平时觉得自己懂了,真到了笔试卷上要你写出来、算出来、推出来的时候,才发现自己其实是一知半解。
现在回头看,这套题里考的东西到今天依然是前端面试的高频核心。不管你是准备校招的应届生,还是想跳槽的初级前端,把这份卷子的考点彻底吃透,收益远不止应付一场笔试。这篇内容我会把卷子里涉及的典型题目拆开讲,补全每一道题背后的原理、推导过程和常见误区,顺手整理一些实际笔试过程中踩过的坑和应对思路,希望能给准备前端岗位面试的朋友一些真正能落地的参考。
1. 试卷整体风格与考察思路
1.1 题型结构与分值分布
百度的这套笔试卷,整体结构大致分为四个板块:基础选择题、简答/判断题、手写代码题和综合应用分析题。选择题部分覆盖面很广,从HTML标签语义、CSS盒模型、JavaScript数据类型,到浏览器缓存策略、HTTP状态码、事件冒泡捕获,几乎每个基础知识点都会扫一遍。
简答和手写代码题是拉开差距的关键。我记得卷子里有直接让你手写深拷贝的,也有让你实现一个防抖函数的,还有给一段代码让你分析输出的。这类题目没有太多取巧空间,平时有没有真正动手写过,一测便知。综合应用题通常会结合一个具体的业务场景,比如页面性能优化、组件设计思路,考的是你面对真实问题时的分析和落地能力。
从分值分布来看,JavaScript部分的权重明显高于HTML和CSS,大概占了一半左右。这也符合当时大厂前端岗位的考察倾向:CSS决定了你的下限,而JS水平决定了你的上限。网络上流传的2018年版本答案,各大论坛上面都有人整理过,但光看答案意义不大,关键是理解每一题背后的考察点。
1.2 考察重点与能力倾向
通览整套卷子,能明显感觉到百度出题人的思路——他们不追求偏题怪题,而是在常见知识点上挖深度。同样是考闭包,普通公司可能让你说说闭包的优缺点,百度会给你一段代码,让你逐行分析作用域链的变化,每一个变量在哪个阶段指向什么样的值,写错一步整题就废了。
再比如事件循环,选择题里考一次宏任务微任务的执行顺序,简答题里可能还要你解释为什么输出是这样的顺序,背后的调用栈和任务队列是怎么协作的。这种考法非常检验你是否真正理解JavaScript的运行机制,而不是背了几个输出顺序的结论。
另一个明显的倾向是重视代码实现能力,而不是光靠记忆。数组中让你不借助原生API去实现数组去重,不是让你调一次[...new Set(arr)]就完事,而是要用双指针、哈希表或者排序的方式推演整个过程。这类题对算法基本功有一定的要求,但和纯算法岗比,前端的算法题通常更贴近数组、字符串和对象操作,不会出现特别偏门的图论或高级数据结构。
2. 前端基础核心题拆解
2.1 HTML/CSS 高频考点还原
基础知识部分,HTML和CSS的题虽然占比不高,但有一个特点:喜欢考细节,而且这些细节平时写业务代码时往往不会太在意。
比如HTML语义化,卷子里会给你几个标签组合,比如<div>、<span>、<section>、<article>、<aside>、<nav>,让你选出哪些是语义化标签,并简述各自的适用场景。这里的坑点在于,很多人分不清<section>和<article>的区别。简单来说,<article>强调内容的独立性,单独拿出来也是一篇完整的文章;而<section>更强调内容的章节性,通常是整体的一部分。如果整个页面只有一个<section>没有标题结构,那就违背了它的语义初衷。
CSS部分,盒模型是必考题。我记得卷子里有一道很经典的题:一个div设置了width: 200px; padding: 20px; border: 5px solid #000; box-sizing: content-box;,问这个元素实际占用的宽度是多少。答案是200 + 20*2 + 5*2 = 250px。如果box-sizing换成了border-box,整体宽度就固定为200px,内容区会被压缩到150px。这题本身不复杂,但能考验你对标准盒模型和IE怪异盒模型的理解是否透彻。
还有一道印象很深的题:父元素和子元素的margin-top都设置为 20px,问父元素和子元素之间的距离。这就是经典的 margin 塌陷(margin collapsing)问题。结果为 20px,而不是 40px,因为相邻的垂直外边距会发生合并。解决方式有很多,给父元素加overflow: hidden、padding、border或者是display: inline-block都能打断这种合并。卷子考这个点的目的,就是看你在写项目时是否真的遇到过这类布局上的怪异行为。
2.2 浏览器渲染与事件机制
浏览器的渲染机制也是这套卷子的常客。有一道选择题很典型:在HTML文档中,当浏览器解析到<script>标签时会怎么处理?答案是会暂停DOM的解析,先下载并执行脚本文件,再继续后面的解析流程。这也是为什么业内一直强调把普通脚本放在</body>之前,或者给脚本加上defer属性。
这里需要补充的是defer和async的区别。题目经常会把这两个属性混在一起考:defer是脚本会并行下载,但会等到HTML文档全部解析完成后才按照出现顺序执行;而async是并行下载完就立刻执行,不保证顺序,执行时可能会中断文档的解析。如果理解不到位,很容易在这道送分题上丢分。
事件机制部分,考的是事件传播的三个阶段:捕获阶段、目标阶段、冒泡阶段。我记得有一道题问的是addEventListener第三个参数传true和false时,分别是在哪个阶段触发。当时很多人只记住了true是捕获、false是冒泡,但没有深入想过,事件从window开始往下走,一直到目标元素,再往上冒泡到window,整个链路里stopPropagation()和preventDefault()分别作用于什么。前者是阻止事件继续传播,后者是阻止浏览器默认行为,两者不能混用。比如在表单提交场景下,如果你只想阻止页面刷新但不想影响事件冒泡,那只能用preventDefault()。
事件委托也是常考的点。就是利用事件冒泡的特性,把子元素的事件处理函数绑定到父元素上,然后通过e.target判断实际触发的是哪个元素。当时卷子里有一道应用场景题:一个ul列表有上千个li,要给每个li绑定点击事件,问最优方案是什么。正确答案就是事件委托,而不是给每个li都addEventListener,因为那样会创建大量监听器,消耗内存。这个思路在现在的框架开发中依然很有价值,尤其是处理动态渲染列表时。
3. JavaScript 重难点实战解析
3.1 作用域、闭包与 this 指向
JavaScript这部分是整套卷子的重头戏,第一类高频题就是作用域和闭包。有一道非常典型的代码输出题,贴出来大家感受一下:
for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 100); }输出是五个5,而不是 0, 1, 2, 3, 4。原因在于var声明的变量没有块级作用域,循环结束后i的最终值就是 5,所有的setTimeout回调函数捕获的是同一个i变量,在100毫秒后执行时读取到的自然是 5。解法有很多:把var改成let,利用let的块级作用域每次循环都会创建一个新的绑定;或者用闭包包一层,把i作为参数传进去;也可以用bind或立即执行函数传参。
闭包这块,卷子还喜欢让你解释闭包的形成原理和内存问题。所谓闭包,就是当内部函数引用了外部函数的变量时,即使外部函数执行完毕,这些变量依然会被内部函数引用而不会销毁。这个特性非常强大,可以用来实现私有变量、函数柯里化、模块化等模式。但副作用也很明显:如果闭包长期持有大型对象,内存就无法释放,导致泄漏。所以写业务代码的时候,用完的闭包引用要记得置空。
this指向在卷子里几乎是必然出现的。经典的那道题:
var obj = { name: 'baidu', say: function() { console.log(this.name); } }; var fn = obj.say; fn();输出是undefined或全局对象上的name属性,而不会是'baidu'。原因很简单,this的指向取决于函数被调用时的上下文,fn()是普通函数调用,在非严格模式下this指向全局对象,严格模式则是undefined。真正想保持obj上下文,得用obj.say()这种形式调用,或者用fn.call(obj)、fn.bind(obj)。
3.2 异步编程与 Event Loop
异步编程在百度这套卷子里占据的篇幅比我想象中还要多。除了基础的回调函数,重点考了 Promise 和 Event Loop 的执行顺序。
有一道经典的输出顺序题:
console.log('start'); setTimeout(() => { console.log('timeout'); }, 0); Promise.resolve().then(() => { console.log('promise'); }); console.log('end');正确的输出顺序是start、end、promise、timeout。这道题考察的是宏任务和微任务的区别。setTimeout回调属于宏任务,会进入宏任务队列;Promise 的.then回调属于微任务,会进入微任务队列。每个宏任务执行完之后,会把当前微任务队列里的所有任务全部执行完,才会去取下一个宏任务。所以虽然setTimeout写在前面,但它的优先级低于微任务。
我当时备考时总结了一个记忆方法:同步代码永远先执行,微任务优先于宏任务,一个宏任务之后清空所有微任务队列。这套规则搞清楚了,大部分输出顺序题都能对。
Promise 本身的设计也是考点。卷子会问Promise.all和Promise.race的区别,以及什么场景下用哪个。Promise.all是所有 Promise 都 resolve 后才进入.then,任何一个 reject 就会进入.catch,适合处理多个无依赖的接口请求并合并结果的场景。Promise.race则是哪个先出结果就算哪个,适合做超时控制,比如请求超过3秒就提示失败。
这套卷子的时间节点在2018年,async/await已经算比较新的语法,但已经进入了考察范围。有一道简答题让写一段async/await处理并发请求的代码。这里有个容易踩的坑:如果写的是await a(); await b();,那就是串行执行,两次请求的时间是叠加的。如果想让它们并发执行,应该先同时调用函数拿到 Promise,再统一await:
const p1 = fetch('/api/a'); const p2 = fetch('/api/b'); const [r1, r2] = await Promise.all([p1, p2]);很多人在这一题上失分,不是因为不会用async/await,而是没有意识到串行和并发的性能差异。这种题考的很实际,工作中接口并发优化也是性能优化的高频手段。
3.3 手写代码题:防抖节流、深拷贝、数组去重
手写代码是笔试卷里最让人紧张的部分,也是拉开差距的核心。这套卷子真正动手写的题不算特别多,但每一道都很有代表性。
第一个是防抖(debounce)。场景是:用户在输入框中输入内容,需要等用户停止输入500毫秒后才发送搜索请求。实现思路是每次触发时清除之前的定时器,再重新设一个新的定时器:
function debounce(fn, wait) { let timer = null; return function(...args) { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, wait); }; }这里有几个细节要注意:返回的函数必须保持this指向,否则在作为对象方法使用时会出现问题;...args要透传,保证参数不丢;定时器变量要放在闭包里,保证多次调用之间共享同一个timer。
节流(throttle)的思路和防抖不同,它限定了函数在固定时间内最多执行一次,适合监听滚动事件时使用。一个常见的实现是:
function throttle(fn, interval) { let last = 0; return function(...args) { const now = Date.now(); if (now - last >= interval) { last = now; fn.apply(this, args); } }; }第二个典型题是深拷贝。当时标准答案通常是递归判断类型然后逐层复制:
function deepClone(obj, map = new WeakMap()) { if (obj === null || typeof obj !== 'object') return obj; if (map.has(obj)) return map.get(obj); const result = Array.isArray(obj) ? [] : {}; map.set(obj, result); for (let key in obj) { if (obj.hasOwnProperty(key)) { result[key] = deepClone(obj[key], map); } } return result; }用WeakMap处理循环引用是加分项,因为普通递归遇到a.self = a这种情况会陷入死循环。当时大部分考生能写出类型判断加递归的版本,但只有少数人想到了循环引用的处理。
第三个是数组去重。最简单的写法是[...new Set(arr)],但笔试通常要求自己实现。可以用哈希表、双重循环、排序后去重等方式。我记得评卷时看重的是你能否在O(n)时间复杂度内完成,而不仅仅是能不能出正确结果。用Map或对象作为哈希表就是标准解法,这个思路在需要统计频率的算法题中也会反复用到。
4. 网络与性能优化类真题
4.1 HTTP 缓存机制
网络部分重点围绕HTTP缓存展开,尤其是强缓存和协商缓存的区别。这是一道高频简答题:浏览器请求一个资源,第一次请求后服务器返回的响应头里带有Cache-Control和Last-Modified,请简述第二次请求时浏览器的行为。
答案要分几步说清楚:第二次请求时,浏览器先判断Cache-Control里的max-age是否过期。在有效期内,直接从本地缓存读取,完全不和服务器通信,这就是强缓存。一旦过期,浏览器会带上If-Modified-Since或If-None-Match重新请求服务器,由服务器判断资源是否真的发生变化。如果没变,返回304状态码,继续使用缓存;如果变了,返回200和新的资源,这就是协商缓存。
这里面有几个常见误区。Cache-Control的no-cache并不是完全不缓存,而是每次都需经过服务器校验;no-store才是真正不做任何缓存。ETag是资源内容哈希类的标识,优先级高于Last-Modified,因为修改时间只能精确到秒,某些修改可能在同秒内完成,而 ETag 能捕捉到内容真正的变化。
性能优化题通常会结合具体场景,比如:首屏加载很慢,你会怎么排查和优化?这类题不需要拿标准的性能优化清单背一遍,关键是分主次作答。我当时的回答思路是从网络请求、资源体积、渲染路径三个层面展开:减少不必要的请求、合理利用缓存和CDN、压缩JS/CSS资源、图片懒加载、拆分首屏关键的CSS、减少阻塞渲染的脚本等。面试官更在意的是你有没有形成一套自己的排查体系,而不是记了多少个优化技巧。
4.2 跨域方案与实战选择
跨域也是笔试卷上的常客。我记得有一道题给出几种跨域方案,让分析各自的优劣和适用场景。JSONP、CORS、postMessage、代理转发,这些都是当时的常规选项。
JSONP 的原理是利用<script>标签不受同源策略限制的特点,动态插入一个 script 标签指向目标接口,接口返回一段调用了回调函数的JS代码。优点是兼容性好,缺点是只能支持GET请求,而且会引入额外的回调函数命名管理问题。CORS 则是现代浏览器的标准跨域方案,服务器在响应头里增加Access-Control-Allow-Origin等字段,支持各种HTTP方法。两种方案在项目中都有应用场景,比如早期一些开放API会用JSONP,而正规的接口平台基本都走CORS。
当时有一道场景题是:前端页面部署在a.baidu.com,接口部署在b.baidu.com,应该如何解决跨域问题。最佳实践是看后端能否配合。如果可以改后端响应头,优先用CORS;如果后端不方便改,开发环境下用 devServer 的 proxy 代理转发,生产环境则在 Nginx 层做反向代理。这些内容在今天的工程化开发中已经很成熟,但在当年能完整答出来的人并不多。
5. 算法与数据结构题目思路
5.1 常见算法题型
校招笔试题里算法题的风格偏应用型,不会直接让你写红黑树或者图的最短路径,而是把算法嵌入到字符串、数组处理中。这套卷子出现的题目大致分三类:
第一类是字符串处理题,比如给定一个字符串,找出最长不重复子串的长度。这是典型的滑动窗口问题,用双指针维护一个窗口,窗口内不包含重复字符,每次移动右指针扩展窗口,遇到重复字符就移动左指针收缩,同时记录窗口的最大长度。
第二类是对称与回文相关问题。比如给你一个字符串,判断是不是回文串,或者找出字符串中的最长回文子串。回文判断本身不难,但中心扩展法和动态规划两种解法的时间复杂度差异很大,笔试时如果能在时间限制内写出来,通常已经比大多数人强了。
第三类是数组操作题,包括数组交并差集、移动零到数组末尾、合并两个有序数组等。这些题的核心思想普遍是双指针和原地操作。移动零那道题,要求把0全部移到末尾,同时保持非零元素的相对顺序,用快慢指针一次遍历就能完成,时间复杂度O(n)、空间复杂度O(1),这就是标准解法。
5.2 解题模板与复杂度分析
备考刷算法题时,不建议海量刷题,而是先把常考题型归纳成模板。我当时总结了两类高频模板:
双指针模板适合有序数组的合并、查找、去重等场景。初始状态一个指针指向头部,一个指向尾部,根据计算结果决定移动哪个指针,直到两个指针相遇。
滑动窗口模板适合子串和子数组问题。固定左边界,移动右边界扩展窗口,不满足条件时收缩左边界。看到“最长”“最短”“连续子数组”“子串”这些关键词,优先考虑滑动窗口。
复杂度分析是笔试答题时容易被忽略的点。很多题目,即使你给出了正确解法,如果复杂度不达标,得分也会打折。比如最长不重复子串,暴力解是三重循环,复杂度O(n^3);优化成滑动窗口后是O(n)。写答案时建议把时间复杂度和空间复杂度一并标出来,既展示思路清晰,也是加分项。
6. 应试策略与备考建议
6.1 时间分配与答题技巧
整场笔试的时间通常比较紧凑,题型又多,如果按顺序死磕某一道难题,很容易导致后面的大题来不及写。我见过不少同学栽在时间分配上,前面的选择题反复纠结,后面手写代码题潦草几笔就交卷了。
我个人的建议是:先花两三分钟快速浏览全卷,把题目按难易程度分个层。容易的题先拿分,困难的题做个标记,最后有时间再回来想。选择题和判断题控制在总时间的四分之一以内,不要恋战。不会的选择题凭第一感觉选一个,然后立刻走人。
手写代码题建议先写核心思路,用注释简要描述边界条件和关键逻辑,再写代码。因为笔试评卷时,即使代码有些小错误,思路清晰、关键步骤注释到位的答案通常也能拿到大部分分数。留白才是最可惜的。
6.2 常见失分点与避坑清单
整理几个实际考场上反复出现的失分点,大家备考时对照自查。
第一是作用域和this不熟,代码输出题全靠懵。这类题没有技巧,只能多写多练,把var/let/const的区别、三种函数调用方式下this的指向全部理一遍,用node跑测试代码验证自己的判断。
第二是手写代码时忽略边界条件。比如深拷贝没处理null、数组和循环引用;防抖函数没处理this指向;数组去重没考虑空数组和稀疏数组。这些都是评卷时的高频扣分点。写完后习惯性地问自己:输入是空值会怎样?输入是异常值会怎样?
第三是只写代码不写思路。笔试和面试不一样,面试你可以口头表达,但笔试只能通过你写下来的内容来判断。代码之上,清晰注释和思路描述就是你的“口语”。尤其是复杂逻辑,务必在关键行旁边标注意图。
第四是粗心导致的低级错误。比如把===写成=,函数名和调用名不一致,数组索引越界。考试紧张时尤其容易犯这种错误。写完代码后,至少花一分钟从头到尾读一遍自己的代码再交卷。
这套百度2018校招Web前端工程师笔试卷(第二批),对当时的前端求职者来说是一座很实用的标杆,它把前端基础、JS功底、网络知识和通用算法能力完整地串了一遍。即使你现在不是准备校招,这份考点清单也可以拿来当作自测标准:能不看资料写对几道手写题,能快速分析出一段代码的输出顺序,能在源码级别解释出浏览器的缓存机制,如果这些都能做到,说明你的前端基础经得起考验。备考过程中我最大的体会是:不要迷信刷题数量,每一道真题做完之后,多问自己一层“为什么”,把背后的原理吃透,比多做三套卷子都管用。