1. 百度2020校招Web前端笔试卷(第二批)整体复盘
先聊点实在的。百度校招的前端笔试题,在圈子里一直以“范围广、基础深、紧跟工程化实践”著称。2020年第二批这套卷子,我印象很深,因为它没有跟风去考一堆花哨的框架API,而是把大量精力放在JS语言本身的特性、浏览器工作原理、网络协议边界,以及工程化中真正会踩的坑上。换句话说,这套卷子的筛选逻辑很明确:刷过题的人能拿一部分分,但真正理解前端运行机制的人才能拿高分。
我当时做完之后最大的感受是:这不仅仅是一场笔试,更像是一次低成本的前端知识体检。如果你的原型链、事件循环、HTTP缓存、CSS布局这些概念掌握得不够牢靠,在限定时间内会非常吃力。这篇文章我会把整套卷子的核心考点拆开,结合我自己的做题经历和后来带人时总结的经验,把每个知识点背后的“为什么”讲清楚,并且补上一些适合实战的解题思路。不管你是正在准备校招的应届生,还是想查漏补缺的初级工程师,这篇文章都能帮你在类似考核里少走弯路。
先给一个整体画像。整套卷子大概分为四类:JS语言基础与进阶特性、浏览器与网络相关、CSS布局与样式细节、算法与工程化应用。不同模块的难度梯度比较明显,前两部分是分水岭,后两部分则直接考察你的动手能力和场景抽象能力。
2. 试卷背后到底在考什么:知识模块与命题逻辑
2.1 模块占比与考察意图
从题目分布来看,2020年第二批的卷子有几个明显的倾向:
- JS语言特性占比最大,约占四成,尤其偏爱闭包、原型链、this指向、异步编程。
- 浏览器与网络考点约占三成,重点是事件循环、缓存机制、渲染流程、跨域方案。
- CSS布局与细节约占两成,主要考察Flex/Grid的熟练度、BFC、层叠上下文。
- 算法与工程化约占一成,通常是一道中等难度的编程题加一道场景设计题。
这个分布放在今天依然有很强的参考意义。不是百度一家这样考,而是大厂校招基本都认这套逻辑:前端工程师的核心竞争力不是“会写页面”,而是对运行时环境有足够深的理解。所以你在复习时,不要只盯着某一家题库刷,而是要把每一类考点形成自己的知识网络。
2.2 命题逻辑分析:为什么这么考
先说一个反常识的观察:校招笔试题很少会直接考“Vue源码”或“React 16新特性”这种过于具体的框架问题。原因很简单,校招面向的是还没有正式工作经验的候选人,如果题目太偏向某个框架,就会把那些基础扎实但恰好没用过该框架的同学误杀。所以百度这种级别的公司更倾向考察“框架之下的东西”,也就是JS本身和浏览器平台。这恰好是很多自学前端的人最薄弱的部分——大家太早学框架,反而忽略了语言和平台。
第二个逻辑是“边界感”。卷子里很多题不是直接问“输出什么”,而是给你一段看似简单的代码,让你判断在不同情况下行为是否一致。这类题考的是你对语言规范的理解边界,比如块级作用域、暂时性死区、隐式类型转换。你不仅要记住结论,还要能解释推导过程。我在后面会结合具体题型展开。
第三个逻辑是“场景还原”。算法题不会只让你写一个排序,而是会套一个“前端页面需要渲染大量数据”之类的实际场景。这里考的不是算法竞赛能力,而是你在资源受限环境下如何选择数据结构和优化方案。这一点在后面的实操模块里我会详细演示。
3. 核心细节解析与实操要点:JS高频考点拆解
3.1 原型链与构造函数:不要只背图,要会推演
原型链几乎是必考内容,2020年这份卷子同样没放过。常见的出题方式有两种:一是直接给出继承关系,让你写出某个属性访问的路径;二是结合new、Object.create、class语法,考察不同方式创建的对象的原型差异。
先看一个经典题目(根据记忆还原):
function Person(name) { this.name = name; } Person.prototype.sayName = function() { console.log(this.name); }; function Student(name, age) { Person.call(this, name); this.age = age; } Student.prototype = Object.create(Person.prototype); Student.prototype.constructor = Student; let stu = new Student("张三", 20); stu.sayName();这道题不算难,但里面藏了三个关键点。首先,Person.call(this, name)的作用是什么?它是在子构造函数内部调用父构造函数,把父构造函数的实例属性复制到子实例上。如果不做这一步,stu.name就是undefined。其次,Object.create(Person.prototype)生成一个以Person.prototype为原型的对象,把它赋给Student.prototype,这样Student的实例就能沿着原型链找到sayName方法。最后,Student.prototype.constructor = Student这行很多初学者会漏掉,它修正了constructor指向,否则通过stu.constructor拿到的会是Person。这一步虽然在这道题里没有直接考察,但在实际框架代码里经常能看到,属于规范写法。
再看一个坑比较深的点:如果直接用Student.prototype = Person.prototype会怎样?这会让两个构造函数的原型指向同一个对象。如果之后在Student.prototype上添加新方法,Person.prototype也会跟着变,污染父级。这是写类继承时最容易犯的错。
实操建议:遇到原型链题,先在草稿纸上画出三层关系——实例、构造函数、原型对象。所有属性查找都沿着这条链走,遇到Object.prototype都没有,就返回undefined。理解了查找机制,大多数题都能迎刃而解。
3.2 this指向:四种绑定规则,一套判断流程
this指向是前端笔试的“钉子户”,几乎每一批试卷都会出。考法一般是给一段代码,让你输出this的指向,或者判断apply/call/bind调用后的结果。
this指向的判断,我总结了一套固定的流程:
- 看函数被调用时的形式。如果是
obj.method()这种形式,this指向obj。 - 如果是普通函数调用
fn(),严格模式下this是undefined,非严格模式下是window/globalThis。 - 如果使用了
new关键字调用,this指向新创建的实例。 - 如果使用了call/apply/bind,this指向第一个参数指定的对象。
- 如果是箭头函数,this不绑定,而是继承外层词法作用域的this。
举一个容易出错的例子:
var name = "global"; let obj = { name: "obj", fn: function() { console.log(this.name); }, arrowFn: () => { console.log(this.name); } }; obj.fn(); // obj obj.arrowFn(); // global let fn = obj.fn; fn(); // global(非严格模式下)箭头函数arrowFn在定义时就捕获了外层作用域的this。外层是全局对象,所以即使通过obj调用,this依然不指向obj。这个特性在React类组件中曾经很常用,用来绑定事件处理函数,避免this丢失。现在虽然函数组件和hooks成了主流,但理解箭头函数的this词法绑定依然很重要。
再提醒一个细节:bind返回的是新函数,它的this是永久的,即使再调用call也无法改变。这一点在笔试中偶尔会以判断题形式出现。
3.3 闭包与作用域链:从内存角度理解
闭包不只是一道笔试题,更是前端开发中的核心工具。2020年这套卷子对闭包考察很重视,至少出现了三道相关的题,包括循环中var/let的经典陷阱、防抖节流的实现原理,以及私有变量的模拟。
先说经典陷阱:
for (var i = 0; i < 5; i++) { setTimeout(() => { console.log(i); }, 100); }这段代码输出什么?答案是5个5。原因是var声明的i是函数作用域,循环结束后i已经变成了5,定时器回调执行时访问的是同一个i。如果把var改成let,情况就变了,输出0到4。因为let声明的是块级作用域,每次循环都会创建一个新的绑定。这个知识点很基础,但很多人在笔试现场会因为紧张而写错。
再深入一层:如果不得不使用var,有什么办法让输出变成0到4?可以用立即执行函数IIFE来创建独立作用域:
for (var i = 0; i < 5; i++) { (function(j) { setTimeout(() => console.log(j), 100); })(i); }这个做法的本质是给每个定时器回调提供了一个持有正确j值的闭包。在ES6之前,这就是标准解法。理解了这一点,你对闭包的理解就不仅仅停留在“函数里返回函数”了,而是理解了闭包的核心价值:保存外部变量的引用。
顺便提一句,闭包带来的内存泄漏问题也是笔试常问的延伸点。当一个闭包长期存在于全局变量中,而它引用的对象又很大时,垃圾回收器无法释放这些内存。解决方法是及时解除引用,比如把闭包赋值为null。我在实际项目里见过因为闭包忘记清理导致页面卡死的案例,排查了大半天,最后定位到是一个长列表组件里保存了每个item的DOM引用。
3.4 异步编程:事件循环与宏任务/微任务顺序
事件循环是衡量前端水平的重要标准之一,2020年这批卷子里直接出了一道代码输出题。题目大概是这样的(还原核心逻辑):
console.log("start"); setTimeout(() => { console.log("setTimeout"); }, 0); Promise.resolve().then(() => { console.log("promise1"); }).then(() => { console.log("promise2"); }); console.log("end");正确的输出顺序是:start、end、promise1、promise2、setTimeout。理解这个顺序的关键在于事件循环中的两类任务:宏任务和微任务。每个宏任务执行完毕后,会清空当前微任务队列,然后才进入下一个宏任务。setTimeout回调属于宏任务,而Promise的then回调属于微任务。虽然setTimeout延迟设置为0,但它依然要等到当前宏任务执行完、微任务队列清空后才执行。
我在带教时发现,很多人会背“微任务优先于宏任务”,但遇到嵌套时会懵。比如,在Promise的then里再创建一个setTimeout,又在setTimeout里创建一个Promise,执行顺序该怎么分析?我建议用队列来模拟:遇到宏任务放进宏任务队列,遇到微任务放进微任务队列,每执行完一个宏任务,就把微任务队列清空一次。只要严格按这个规则推演,所有顺序题都能解出来。
还有一点容易被忽略:async/await只是Promise的语法糖。await后面的代码会作为微任务执行。比如:
async function test() { console.log("a"); await null; console.log("b"); } console.log("c"); test(); console.log("d");输出是c、a、d、b。因为调用test函数时,a是同步打印的,await null会让后面的代码挂起,等当前宏任务结束后再执行。这个问题很多工作了一两年的人都会栽跟头。
4. 浏览器与网络:缓存、渲染与跨域实战要诀
4.1 HTTP缓存机制:强缓存与协商缓存如何配合
网络模块是前端笔试中的常客,缓存问题更是高频中的高频。百度这套卷子出了这样一道题:一个静态资源URL,首次请求返回200,第二次刷新返回304,第三次刷新返回200 from memory cache,请解释这个过程。
要回答好这道题,需要把浏览器缓存模型画出来。缓存分为强缓存和协商缓存两类。强缓存命中时,HTTP状态码为200,资源从缓存中读取,不会发起网络请求。强缓存通过Cache-Control或Expires控制。当强缓存未命中或过期时,会走协商缓存流程,带上If-Modified-Since或If-None-Match发给服务器验证,服务器返回304或200。
回到题目场景。首次请求肯定需要走网络,服务器返回200,同时带有缓存策略头。第二次刷新时,如果强缓存有效,其实应该直接命中强缓存,不会发请求。但题中说的是304,说明强缓存已经失效(可能是Cache-Control设置了no-cache或max-age较短),于是发起了协商缓存,服务器校验文件未修改,返回304,浏览器继续用旧缓存。第三次刷新出现memory cache,说明如果是通过地址栏回车或页面刷新以外的操作,某些资源可能直接从内存缓存读取。实际操作中,Chrome的缓存策略还受浏览器内存、磁盘容量、用户行为等影响,不能只看状态码机械判断。
给实际开发者的建议是:对不常变的静态资源(如带hash的JS/CSS),使用Cache-Control: max-age=31536000, immutable,因为文件名变了就相当于新资源。对HTML文件,使用no-cache确保每次请求都校验更新。这个组合是大型项目最常见的缓存策略。
4.2 渲染流程:关键渲染路径与性能优化
笔试卷里有一道问答题,要求简述从输入URL到页面展示的完整过程。这题看着像送分题,实际上面试官最想听到的细节包括:DNS解析、TCP连接、TLS握手、HTTP请求、响应处理、HTML解析、CSS解析、JavaScript执行、重排重绘、合成线程提交。很多人只写到“下载HTML、渲染页面”就停了,丢分很可惜。
我建议把完整链路分成五个阶段回答,每个阶段再补充一两个细节。
- 网络阶段:DNS解析(可能有缓存)、TCP三次握手、如果HTTPS则TLS握手、发起HTTP请求、接收响应。
- 解析阶段:浏览器收到HTML后,边解析边构建DOM树;遇到CSS,构建CSSOM;遇到JS则阻塞解析(除非加async/defer)。
- 渲染阶段:DOM和CSSOM合并为渲染树,计算布局,绘制。
- 合成阶段:把页面拆成多个图层,由合成线程合成并显示。
- 加载完成事件:全部资源加载完成后触发load事件。
其中最容易丢分的是“render tree与DOM的区别”。渲染树只包含可见元素,display:none的元素不会出现在渲染树中,但 visibility:hidden 的元素会,因为它占位且不可见。这个细节在笔试和面试中都可以作为加分点。
再补充一个性能优化视角:关键渲染路径优化,就是尽量缩短从HTML到屏幕像素的时间。具体手段包括:减少阻塞渲染的CSS和JS、延迟加载非首屏图片(loading="lazy")、使用CDN、启用HTTP/2多路复用等。2020年试卷里虽然没有直接考“如何优化LCP”,但考了渲染过程后,紧接着考理解“DOMContentLoaded与load事件的区别”。前者是HTML解析完成后触发,后者是所有资源(图片、样式等)加载完后触发。实战中,监听DOMContentLoaded更适合做首屏逻辑初始化。
4.3 跨域方案:从原理到选型
跨域是Web前端绕不开的话题。2020年这套卷子出了一道方案选择题:在iframe和父页面之间通信,要求跨域,应该怎么实现?备选有postMessage、document.domain、JSONP等。
先说基础概念。同源策略要求协议、域名、端口完全一致才能互相操作DOM和发起Ajax。解决跨域有很多方案,但要分场景:
- JSONP:利用script标签不受同源限制,只能GET请求。适合简单的接口,但不够灵活。
- CORS:标准方案,服务端设置Access-Control-Allow-Origin。支持各种HTTP方法,也是现代项目最推荐的。
- postMessage:适合iframe和 window.open 打开的页面之间的通信。这也是题目里最合适的方案。
- document.domain:只能用于同一个主域下的子域跨域,比如 a.baidu.com 和 b.baidu.com。现在已经不太推荐,因为实用性有限。
- nginx反向代理:主要用于开发环境和部署环境,把跨域问题在服务器层面解决,前端无感知。
回到题目,如果页面嵌入了不同域的iframe,需要双向通信,postMessage是最合理的选择。它的用法很简单:
// 父页面发送消息 iframe.contentWindow.postMessage("hello", "https://target.com"); // iframe页面接收消息 window.addEventListener("message", (e) => { console.log(e.data, e.origin); });这里要提醒两点:一是接收消息时一定要校验e.origin,防止来自恶意页面的消息;二是发送消息的目标源要填写精确,不能随意传“*”。很多安全漏洞都来自postMessage的信任边界没有做好。笔试中如果能写出这两点,面试官会认为你有安全意识。
5. 实操过程与核心环节实现:手写一个数据请求函数
笔试中有一道题让我印象很深:手写一个带超时控制的fetch请求。这道题看起来简单,却一箭三雕,考察了Promise的用法、AbortController的使用和异常处理能力。
题目原意是:实现一个getData(url, timeout)函数,在timeout毫秒内未返回结果,则主动取消请求并抛出错误。
传统的XHR方案可以调用xhr.abort()。但fetch的取消需要借助AbortController,如果在2020年你还不熟悉这个API,很有可能写不出来。标准写法如下:
function getData(url, timeout = 5000) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); return fetch(url, { signal: controller.signal }).then(response => { clearTimeout(timer); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }).catch(error => { clearTimeout(timer); if (error.name === "AbortError") { throw new Error(`Request timeout after ${timeout}ms`); } throw error; }); }这个实现有几个亮点:一是用AbortController连接了定时器和fetch,二是通过检查error.name区分“超时取消”和“真正的网络错误”,三是无论如何都清理定时器,避免内存泄漏。这些细节在笔试时都能体现你的工程素养。
但只有这个版本还不够。实际项目中,我们往往还需要支持重试、取消旧请求、以及避免竞态。比如在搜索框场景里,用户快速输入时,上一次请求的响应比后一次更快返回,导致页面显示旧结果。这个问题不能只靠setTimeout解决,需要用请求序号或AbortController取消上一次请求。我当时在笔试附加题里补充了一种用“最新请求标识”防竞态的思路:
let currentRequestId = 0; function search(keyword) { const requestId = ++currentRequestId; fetch(`/api/search?q=${keyword}`) .then(res => res.json()) .then(data => { if (requestId === currentRequestId) { renderList(data); } }); }这个方案在真实项目里非常可靠,而且不依赖现代API,兼容性好。笔试现场写出来,是明显的加分项。
算法题部分,2020年这批的难度适中,我记得有一道是“给定两个数组,求它们的交集(结果需要去重)”。这道题可以先用Set做过滤,也可以用双指针。我当时选择的是先排序再用双指针:
function intersection(nums1, nums2) { nums1.sort((a, b) => a - b); nums2.sort((a, b) => a - b); const result = []; let i = 0, j = 0; while (i < nums1.length && j < nums2.length) { if (nums1[i] < nums2[j]) { i++; } else if (nums1[i] > nums2[j]) { j++; } else { if (result[result.length - 1] !== nums1[i]) { result.push(nums1[i]); } i++; j++; } } return result; }注意去重逻辑:因为数组排序后相同的元素是连续的,所以比较result最后一个元素即可。如果直接用Set配合filter,实现更简单,但排序加双指针展示了更扎实的算法功底。两种方案都能过,但后一种更能体现你在数据结构选择上的思考。
6. 常见问题与排查技巧实录:那些笔试中的丢分陷阱
6.1 输出顺序题:表面考输出,实际考任务队列
笔试里最容易拉开差距的就是那几道“打印顺序”的题。我总结了几个高频陷阱,几乎每个校招季都能看到同学在上面栽跟头:
陷阱一:先同步,再微任务,再宏任务的顺序被忽略。很多人只会盯住Promise,忘了先输出同步代码。
陷阱二:async函数内部的异步时机。前面举例过,await后面的代码是微任务,但有人会误以为await会立即同步执行。
陷阱三:微任务中嵌套宏任务,会重新进入事件循环。比如:
Promise.resolve().then(() => { console.log(1); setTimeout(() => console.log(2), 0); }); setTimeout(() => console.log(3), 0);输出顺序是1、3、2。因为在微任务第一次清空时,setTimeout2加入宏任务队列,排在已有的宏任务3之后。如果对事件循环理解不透,很容易写成1、2、3。
做题技巧:用纸笔模拟两个队列,坚决不靠直觉。每遇到一个异步API,就判断它是宏任务还是微任务,放进对应队列。执行完当前宏任务后,把微任务队列清空,再取下一个宏任务。这个方法虽然慢,但准确率极高。
6.2 CSS布局题:BFC和层叠上下文为何总能出题
CSS在百度这套卷子里的比重不算大,但每道都是经典。有一道题是问“哪些属性会创建BFC”。我给大家整理一个速查列表:
- 根元素(html)
- float不为none
- position为absolute或fixed
- display为inline-block、table-cell、table-caption、flow-root
- overflow不为visible(如hidden、auto、scroll)
- flex/grid容器的直接子元素
BFC的核心作用是隔离内外布局,比如解决margin塌陷、清除浮动、阻止元素被浮动元素覆盖。实际项目中,最常用的是给容器加overflow: hidden或display: flow-root来创建BFC。后者是更现代的方案,不会意外裁剪内容。
层叠上下文也是必考。很多人以为z-index大就应该盖在上面,但前提是它们处于同一个层叠上下文。如果父元素形成了层叠上下文,子元素的z-index只能在父级内部比较。例如:
<div style="position: relative; z-index: 10;"> <div style="position: absolute; z-index: 999;">A</div> </div> <div style="position: relative; z-index: 5;">B</div>即使A的z-index是999,它依然受父级z-index:10的限制,整体高于B。如果把父级z-index改成1,那么A再大也会被B盖住,因为整个父级上下文都在B之下。这个知识点在做弹窗、浮层、表格固定列时经常会踩坑。
6.3 手写题:防抖节流如何写才能拿满分
手写防抖节流已经是标配了,2020年这批卷子也有一道。但很多人在笔试时写出的版本只能算“能跑”,离满分还有距离。我推荐一套稳健写法。
防抖(debounce)的含义是:触发事件后,如果再次触发,则重新计时。适用于输入框搜索、窗口resize等场景。
function debounce(fn, delay = 300) { let timer = null; return function(...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; }注意这里用了function而不是箭头函数,因为要确保this指向调用者。同时用fn.apply(this, args)把参数传进去。很多新手漏掉this处理,导致内部函数拿不到正确的上下文。
节流(throttle)的含义是:在一定时间内只执行一次。适用于滚动事件、按钮防重复点击。
function throttle(fn, interval = 300) { let lastTime = 0; return function(...args) { const now = Date.now(); if (now - lastTime >= interval) { lastTime = now; fn.apply(this, args); } }; }如果想要头尾都执行,可以使用定时器版本。不过在笔试中,时间戳版本清晰易懂,能满足绝大多数场景。如果能顺手解释两者区别和适用场景,基本就能拿满分了。
6.4 工程化场景题:如何设计一个前端组件库
2020年这套卷子还出了一道开放题,让你设计一个可复用的前端组件库的打包与发布方案。这种题没有唯一答案,但可以反映出你对工程化的理解程度。我的回答框架是:
- 目录结构:采用 monorepo 还是单包多目录?推荐按组件拆分,每个组件包含源码、样式、测试和文档。
- 构建工具:使用 Rollup 还是 webpack?组件库更适合 Rollup,因为产物更干净,可以输出ES Module和CommonJS两种格式。webpack更常用于应用打包。
- 样式处理:使用CSS变量实现主题定制,或者采用CSS-in-JS方案。如果组件库面向外部开发者,CSS变量更轻量。
- 按需加载:提供
lib目录下每个组件的独立入口,配合sideEffects字段让打包工具正确tree-shaking。 - 版本管理:遵循Semver语义化版本,使用lerna或changesets统一管理发布流程。
- 文档与演示:使用Storybook或自研文档站,保证每个组件都有可交互的demo。
如果有余力,还可以补充“如何确保样式不冲突”的话题,比如使用BEM命名规范,或给每个组件添加统一前缀。这些都是在真实开源项目中会遇到的问题,提前想清楚,笔试时自然能写出一份有条理的方案。
7. 备考方法建议:从一套笔试卷延伸到一套知识体系
如果你认真研究过2020年百度校招第二批试卷,你会发现它其实是一个很好的复习提纲。与其到处找新题刷,不如把这份卷子里的考点逐个展开,形成自己的知识树。我自己在带新人时,会推荐如下备考路径:
第一阶段:打好JS基础。把《JavaScript高级程序设计》里关于函数、作用域、闭包、异步、原型链的章节读透。一定要动手写例子,不要只看文案。
第二阶段:理解浏览器原理。推荐阅读《Web性能权威指南》和MDN上的关键渲染路径、事件循环、缓存策略相关文档。学会用Chrome DevTools的Network和Performance面板验证理论。
第三阶段:刷题输出。每天手写两个函数,比如防抖、节流、深拷贝、数组去重、Promise.all、new的实现。笔试时,手写代码是最能反映真实水平的部分。
第四阶段:做项目复盘。把你自己做过的项目按照“项目背景、技术选型、核心难点、解决方案、优化效果”的框架整理成文档。大厂笔试中的开放题往往和你个人项目经历挂钩,提前准备好案例,答起来会从容很多。
另外,我始终觉得,刷题不是目的,理解系统才是。前端这个岗位有趣的地方在于,它是离用户最近的技术栈,任何性能问题、兼容性问题、安全问题都可能在线上暴露。所以,笔试中那些“为什么”才是真正珍贵的东西。
最后再说一个实用建议:做笔试卷时,先做会做的,标记卡壳的,最后集中攻克。这套卷子题量不小,如果一道输出顺序题纠结太久,后面的手写题可能时间不够。我的习惯是,用五分钟扫一遍所有题目,把分值高且有把握的题优先完成,再回头啃硬骨头。时间管理本身也是校招笔试考察的一部分,你要做到像处理线上事故一样冷静。