2020年百度校招前端笔试题复盘:从第三批试卷看大厂到底想招什么人
2020年秋季招聘季,我参加了一次百度校招Web前端工程师的笔试,拿到的是第三批试卷。当时做完最大的感受就是:这场笔试和平时刷的LeetCode完全是两码事。它不考脑筋急转弯式的算法难题,也不问“你知道哪些前端框架”这种泛泛而谈的问题,而是把计算机基础、JavaScript语言特性、浏览器原理、网络协议、工程化实践全揉在一起,逼你在有限时间里展示自己的真实水平。
这份试卷对后来准备校招的人仍然有很强的参考价值。无论你现在是正在刷题的大三学生,还是刚转行想做Web前端的开发者,只要你想进大厂,想搞清楚“前端笔试到底考什么”,这篇复盘都可以当作一份路线图来用。我会结合当时考完后的回忆和后续反复查证的结果,把试卷结构、核心考点、手写代码题拿分点、简答题答题套路,一条一条拆开讲清楚。
1. 第三批笔试卷的整体命题逻辑:不是在考记忆,是在考“有没有建立体系”
1.1 试卷结构:题型占比和大多数人的预期不太一样
当年这套第三批试卷的题型分为四块:单选题、多选题、简答题、编程题,总分100分,考试时长90分钟。很多人惯性思维觉得“大厂笔试肯定全是算法题”,但百度前端岗的试卷恰恰不是这个风格。单选和多选占了将近40分,简答题20分左右,剩下40分左右是两道手写代码题。这个结构其实释放了一个很明确的信号:校招前端岗,更看重的基础知识广度、概念理解准确度、以及动手实现能力,而不是纯粹的逻辑题刷题量。
从考生回忆里综合来看,各知识板块的占比大约是这样的:
| 知识板块 | 大致分值占比 | 典型考点 |
|---|---|---|
| JavaScript语言核心 | 30%-35% | 闭包、原型链、this绑定、事件循环、类型转换 |
| 浏览器与网络基础 | 20%-25% | 渲染流程、缓存机制、HTTP状态码、跨域 |
| HTML/CSS基础 | 10%-15% | 盒模型、定位、选择器优先级、Flex/BFC |
| 框架与工程化 | 10%-15% | React/Vue生命周期、Webpack原理、组件通信 |
| 手写代码 | 30%-40% | Promise、深拷贝、防抖节流、事件总线等 |
这个比例并不是官方公布的数字,而是很多参加过考试的人复盘后的共同感受。它反映了一个规律:大厂前端笔试的底层逻辑是“原理优先”。框架版本会变、API会变,但JavaScript语言特性、浏览器工作原理、网络协议这些底层知识,二三十年内都不会有大的变化。
1.2 命题人想看到的,不是“会用”,而是“能解释为什么”
我印象很深的是,单选里有一道关于Promise执行顺序的题,代码大概是:
console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve().then(() => console.log(3)); console.log(4);输出顺序是1、4、3、2。如果只是背过“微任务先于宏任务”这句话,这道题能答对;但后面多选里继续追问“如果把setTimeout的第二个参数从0改成100,输出顺序会不会变化”,很多人就懵了。因为那已经不是在考定时器本身,而是在考“事件循环里各个任务队列的调度机制”。题目不会直接问“什么是微任务”,而是给你一段嵌套了三层Promise和两个setTimeout的代码,让你选输出,顺便在错误选项里埋一个“async函数里await后面的代码同步执行”的坑。
类似的题目还有:给出一个对象赋值表达式,问原型链上属性查找的顺序;给出一段使用var和let混合声明的循环代码,问点击后输出什么。这些题表面上是语法题,实际上是在检验你是否在脑子里建立了“JavaScript执行机制”的完整心智模型。如果你平时写代码全靠复制粘贴,没想过变量提升、作用域链、任务队列这些概念,第三批试卷的选择题部分会让你做得非常难受。
1.3 这份试卷真正过滤掉的是哪一种人
结合我自己和周围同学的反馈,第三批试卷的区分度其实很高。它最大的特点就是不考偏题、怪题,所有考点都写在常见的面试题清单里,但每道题都稍微换了一层皮。比如“闭包”这个考点,它不是让你说“闭包是什么”,而是给你一个计数器工厂函数,问多次调用后返回值是多少,同时让你指出其中存在的内存泄漏隐患。再比如“跨域”,它不直接问“怎么解决跨域”,而是给你四个不同场景的请求,让你选哪些会触发CORS预检请求。
这种出题方式筛掉的人,不是基础差的人,而是“学过但没总结”的人。一个知识点如果你只是刷题时见过一次,靠短期记忆记住了答案,换个场景就不认识了,那在第三批试卷里基本拿不到分。反过来,如果学习的时候有意识地从“是什么、怎么用、为什么这样设计、会踩什么坑”四个维度去整理知识点,这套试卷的选择题部分其实很容易拿高分。
2. 选择题里的高频考点拆解:这些细节最容易翻车
2.1 作用域、闭包和this绑定:永远绕不开的三兄弟
作用域和闭包几乎是百度历年校招笔试卷里100%出现的考点,第三批也不例外。当时有一道多选,代码大概长这样:
var name = 'global'; var obj = { name: 'obj', getName: function() { return function() { return this.name; } } }; console.log(obj.getName()());答案是'global',因为obj.getName()返回的是一个普通函数,这个函数执行时没有调用者,在非严格模式下this指向window。很多同学在这里栽了跟头,原因是把“函数定义时的上下文”和“this的绑定规则”混为一谈。闭包能捕获变量,但捕获的是词法作用域里的变量,而不是this。this的指向只和调用方式有关,和函数在哪定义、在哪返回都没有关系。
这种题背后考察的其实是两条核心线索:词法作用域链和this的四种绑定规则。准备这类题目的时候,我建议不要光刷题,而是亲手把下面几种情况写一遍,看看输出分别是什么:
- 普通函数调用:
fn(),严格模式下this是undefined - 方法调用:
obj.fn(),this指向obj - 显式绑定:
fn.call(obj)、fn.apply(obj)、fn.bind(obj) - new调用:
new Fn(),this指向新创建的对象 - 箭头函数:不绑定自己的this,沿用外层词法作用域的this
这五种情况搞清楚了,笔试里90%和this相关的题都不会再错。另外还要补充一个容易忽略的点:var声明的全局变量会挂到window上,而let、const声明的全局变量不会。所以上面那道题如果改成let name = 'global',输出就变成undefined了。这种细节不难,但考试时很容易忽略。
2.2 事件循环和异步机制:微任务、宏任务与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'); setTimeout(() => { console.log('setTimeout'); }, 0); async1(); console.log('script end');这道题的输出是:script start、async1 start、async2、script end、async1 end、setTimeout。核心就是记住:await后面的代码相当于被放进了微任务队列,要等当前宏任务执行完、微任务队列清空之后才会继续执行。也就是说,async1 end不会在async2()执行完立刻输出,而是要等当前同步代码跑完。
很多人记不清楚“await到底等的是什么东西”,这里有个更准确的理解方式:await会暂停当前async函数的执行,并把暂停点后面的部分封装成一个微任务。这个微任务要等两件事都完成才执行:当前调用栈清空、Promise的状态变为resolve。如果await后面跟的是一个已经是resolve状态的Promise,微任务依然要等当前宏任务结束才能执行,不会提前。
笔试里还喜欢把事件循环和“定时器延迟”结合考。setTimeout(fn, 0)并不是说fn一定会立刻执行,而是最快也要等当前宏任务和所有微任务执行完毕。如果前面有一个非常耗时的重计算或者死循环,定时器甚至可能无限期延后。理解了这一点,很多“输出顺序”题就能做对,不会落入“setTimeout一定先于Promise”的错误印象里。
2.3 HTTP缓存和状态码:选择题里最容易丢分的部分
HTTP缓存相关考点在第三批试卷里占了不小的比重。它不像JS可以在浏览器控制台里即时验证,很多同学面试前也没系统梳理过,所以到了笔试现场只能靠猜。其实这个知识点只要把几个关键字段捋顺,就能应对绝大多数题目。
首先要区分两种缓存:强缓存和协商缓存。强缓存阶段,浏览器直接读本地缓存,不发请求;协商缓存阶段,浏览器会向服务器发请求,由服务器判断资源有没有更新。控制强缓存的字段主要是Cache-Control(比如max-age=3600、no-cache、no-store)和Expires。控制协商缓存的字段主要是Last-Modified/If-Modified-Since和ETag/If-None-Match。
考试时有个高频坑:Cache-Control: no-cache到底是不是“不缓存”?很多人想当然地认为no-cache就是不缓存,实际上它表示“可以缓存,但每次使用前必须向服务器确认资源是否新鲜”。真正的不缓存是no-store。这个概念如果没搞清楚,选择题就会直接选错。
HTTP状态码也考了几道,包括301、302、304、403、404、500、502这些常见状态。第三批试卷里比较有迷惑性的一道题是:浏览器向服务器请求一个静态资源,服务器返回304,问这次请求耗费了几次HTTP往返。答案是1次:浏览器带上If-Modified-Since或If-None-Match发起请求,服务器校验后返回304,浏览器使用本地缓存。整个过程是一次请求,不是两次。
2.4 浏览器渲染机制和性能优化考点
浏览器渲染这块,第三批笔试既考选择题也考简答题。选择题一般给几个操作,问哪个会引起重排(reflow),哪个只会引起重绘(repaint)。比如修改width、height、display、font-size这些属性会重排,而修改color、background-color、visibility这些只重绘。
这类题目有个判断诀窍:只要属性变化影响元素的几何尺寸、位置,或者导致周围元素布局变化,就会重排;如果只是视觉样式的变化,不影响布局,就只重绘。更准确地说,重排几乎不可避免,因为元素的color变化虽然不改变布局,但在某些情况下浏览器为了确保渲染正确也会触发布局计算。笔试层面上,掌握最常见的几组即可。
还有一道让人印象深刻的题:在不断触发滚动事件的场景下,是选择防抖(debounce)还是节流(throttle)。正确答案是节流。因为防抖的逻辑是“停止触发后等待一段时间才执行”,如果用户一直滚动,那么回调可能永远不执行;节流则是“固定时间间隔内最多执行一次”,适合滚动、resize这类高频、持续触发但必须定期响应的场景。这种题考的已经不单纯是API背没背,而是对“用户行为模型”的理解。
3. 手写代码题:能跑通只是及格,边界和异常处理才是加分项
3.1 第一道编程题:手写深拷贝,考的不只是递归
第三批试卷的第一道编程题是“实现一个深拷贝函数”。看到题的时候,大多数人第一反应是:简单,递归遍历对象,复制属性。但真正写起来会发现这题要拿满分很难。满分标准至少包含下面几层:
- 基础递归实现
function deepClone(source) { if (source === null || typeof source !== 'object') { return source; } const target = Array.isArray(source) ? [] : {}; for (let key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = deepClone(source[key]); } } return target; }这版能处理普通对象和数组,但几个问题很明显:没有处理循环引用,遇到Date、RegExp、Map、Set这些特殊对象时行为不对,而且for...in会遍历到原型链上的可枚举属性。虽然我用hasOwnProperty过滤了,但真正打分时,评委更看重的是你有没有意识到这些问题。
当时我在这道题里加入了循环引用的检查,用了一个WeakMap来记录已经被拷贝过的对象:
function deepClone(source, map = new WeakMap()) { if (source === null || typeof source !== 'object') { return source; } if (map.has(source)) { return map.get(source); } const target = Array.isArray(source) ? [] : {}; map.set(source, target); const keys = Reflect.ownKeys(source); for (const key of keys) { const descriptor = Object.getOwnPropertyDescriptor(source, key); if (descriptor && 'value' in descriptor) { target[key] = deepClone(source[key], map); } else { Object.defineProperty(target, key, descriptor); } } return target; }为什么用WeakMap而不是普通Map?因为WeakMap的键是弱引用,不会阻止对象被垃圾回收,这样既解决循环引用问题,又不会造成内存泄漏。这个细节如果能在代码注释里写出来,评卷人一眼就能看出你理解得是否深入。
3.2 第二道编程题:手写Promise.all或EventEmitter
第三批的编程题二,考生回忆中比较多的版本是“实现一个简易的Promise.all”以及“实现一个EventEmitter(事件发布订阅)”。两道题二选一或者都出现的情况都有。以Promise.all为例,大多数人的第一版是这样:
Promise.myAll = function(promises) { return new Promise((resolve, reject) => { const results = []; let count = 0; for (let i = 0; i < promises.length; i++) { Promise.resolve(promises[i]).then(value => { results[i] = value; count++; if (count === promises.length) { resolve(results); } }, reject); } }); }这个版本基本能跑通正常场景,但有几个边界值得补充:如果传入的数组是空的,应该立即resolve一个空数组;如果某个promise已经是普通值,要用Promise.resolve包装;then的第二个参数是reject处理,但更常见的是用.catch统一捕获,避免某个promise的reject没有被正确处理导致整体卡死。
我当时的最终版本还处理了一个很多人忽略的细节:promises参数本身可能不是一个数组,而是一个可迭代对象,更严谨的写法是先用Array.from转成数组。另外,如果promises里某一项是thenable对象,Promise.resolve也会把它转换成Promise,这个步骤不能省。
这类手写题看起来简单,但对细节的考察非常密集。评卷人一般不会跑测试用例,而是直接看代码,重点看三样东西:有没有考虑边界情况、有没有错误处理、代码风格是否干净清晰。所以写的时候不要追求行数少,而是把正常路径、异常路径、空值、非数组输入都处理到位。
3.3 手写题里最隐蔽的坑:会把API背得很熟,但不理解参数语义
第三批试卷里还出现过一道看起来不太难,但错误率极高的小题:实现一个防抖函数。很多人直接写:
function debounce(fn, wait) { let timer = null; return function(...args) { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, wait); } }这道题的考点是:返回函数内部的this必须正确。如果直接用fn.apply(this, args),这里的this是返回函数执行时的调用者,这是对的。但有些人会把fn直接放在setTimeout里执行,写成:
timer = setTimeout(() => { fn(args); }, wait);这样既丢失了this,也把参数数组当成了单个参数传入。虽然面试官可能不会真的去运行代码,但一眼就能看出对JavaScript函数调用方式理解不透。这类题目我建议平时练习时一定要在浏览器里实际跑一遍,加几个不同的调用场景,输出结果验证对了才算真正掌握。
4. 简答题的答题套路:结论先行、原理铺路、实践收尾
4.1 “从输入URL到页面渲染完成,经历了什么?”
这道题是第三批试卷简答题里最经典的一道,几乎每家大厂前端笔试都爱出。它看起来简单,但拿高分不容易。常见的低分答案是贴一段网上看过的流程:DNS解析、TCP连接、HTTP请求、服务器响应、浏览器渲染。完。这种答案就算每一步都对,分数也高不了,因为没体现深度。
我当时的答题思路是分层展开:
请求阶段:用户输入URL,浏览器解析协议、域名、端口、路径。注意,现代浏览器会在输入URL前做搜索意图判断,如果输入的不是合法URL,会直接走搜索。合法URL则先查DNS缓存(浏览器缓存、系统缓存、路由器缓存、根域名服务器逐级查询),拿到IP后如果是HTTPS,还要做TLS握手。如果是HTTP/2,还会多路复用,多个请求共用一条TCP连接。
网络传输阶段:建立TCP连接时,三次握手的过程要写清楚——SYN、SYN-ACK、ACK。为什么是三次而不是两次?因为要防止历史重复连接请求突然到达服务器导致资源浪费。这个追问在简答题里也出现过,如果你只是背了“三次握手”四个字,追问就会露馅。
服务器处理与响应:服务器返回HTML、CSS、JS文件,响应头里的Content-Type、Cache-Control、ETag这些字段会影响浏览器后续的缓存行为,可以顺手提一句。
浏览器渲染阶段:收到HTML后,先解析HTML构建DOM树,同时解析CSS构建CSSOM树,两者合并成渲染树。这个过程中如果有<script>标签,默认会阻塞DOM解析,所以有defer和async两种优化属性。布局阶段计算每个节点的几何位置,绘制阶段生成Paint Layers,最后合成线程把图层合成输出到屏幕。
如果答题篇幅允许,我建议再补充一条:为什么CSS要放在<head>里,JS要放在</body>前,或者使用defer。这叫“理论联系实际”,不仅说明你懂原理,还说明你平时写页面时有意识地在运用这些原理。
4.2 “谈谈你对前端工程化的理解”
这道题在2020年这批试卷里出现频率很高,第三批也有。很多人在校期间只用过Vite或者Create React App,对工程化只有一个模糊概念,于是答案变成“Webpack可以打包文件、Babel可以转译ES6”,一句话带过。这种答案分数很低。
我建议把工程化拆成几个层次去答:
- 开发层:模块化开发,ES Module和CommonJS的区别;组件化设计,状态管理、路由、按需加载。
- 构建层:依赖分析、代码转译、代码分割、Tree Shaking、资源压缩。这里可以结合Webpack的Loader和Plugin机制来谈,Loader负责对模块源码进行转换,Plugin解决Loader解决不了的事情,比如打包优化、资源管理、环境变量注入。
- 质量与规范层:ESLint、Prettier、单元测试、CI/CD。前端工程化不只是解决“代码怎么组织”,更是解决“多人协作时怎么保证代码质量和可维护性”。
- 部署层:静态资源上传CDN、缓存策略、灰度发布、版本回滚。
这样一个框架答下来,既显示了知识广度,又展示了结构化思维。阅卷人看简答题,其实是在看你的思维模型清不清晰。同样一个知识点,罗列型答案和框架型答案,分数差距会很大。
4.3 “设计一个登录组件/弹窗组件,你会考虑哪些因素”
简答题还经常出这种组件设计题。它不是让你写代码,而是考察“当你要在真实项目里做一个组件时,会不会想全边界情况”。
我总结的答题维度是:功能边界、状态管理、无障碍、性能、可维护性。以弹窗组件为例:
- 功能和样式:支持标题、内容、底部按钮自定义;点击遮罩是否关闭、按Esc是否关闭、关闭时是否有回调。
- 状态管理:弹窗的打开/关闭状态由父组件控制还是组件自身控制;是否支持受控和非受控两种模式。
- 无障碍:弹窗打开时焦点是否移入面板、关闭时焦点是否归还给触发按钮、是否设置了
aria-modal。 - 性能:滚动锁定怎么做,
overflow: hidden和position: fixed各有什么副作用;弹窗内容是否懒挂载。 - 复用和扩展:能否通过API传入任意内容,而不是写死结构。
答题时不要把所有点全列出来没有重点,建议开头先用一句话定性,“一个合格的弹窗组件,不只是视觉功能,更需要处理好焦点管理、滚动锁定和状态同步”,然后再展开。这种答案会让阅卷人觉得你有真实项目经验,而不是临时背题。
5. 考完第三批之后,我对“前端学习路线”重新做的调整
5.1 2020年这张试卷反映出来的“人才标准”变化
以前总有人说,前端门槛低,会写页面就行。百度2020校招第三批这张试卷,直接把这种观点否掉了。它的题目横向覆盖了计算机网络、浏览器内核、JavaScript语言规范、现代工程化体系,纵向又要求手写代码有工程级质量。这背后的人才标准,已经从一个“能实现页面的人”,变成了“能理解整个Web运行链路的人”。
我举一个细节:有一道多选问的是“哪些操作会导致浏览器发起跨域预检请求”,正确答案包括:使用fetch且自定义了非简单请求头、Content-Type为application/json、使用了PUT/DELETE方法。而“使用form表单提交”和“使用<img>标签请求”不会触发。这种问题如果没做过实际的前后端分离项目,光靠背是容易记混的。所以笔试筛的其实是“真实项目经验”,不只是“刷题量”。
这也解释了为什么很多计算机基础扎实、但没怎么做过Web项目的同学,笔试成绩反而不如那些经常写小项目、踩过坑的“野路子”选手。原因是后者在实践里被迫理解了那些底层概念,而前者只是“知道”那些概念。
5.2 我给备考同学的一份“优先级清单”
如果你现在正在准备校招前端岗,想按第三批试卷透露出来的思路来备考,我建议把这几个方向按优先级排一排:
| 优先级 | 备考内容 | 原因 |
|---|---|---|
| P0 | JavaScript语言核心(作用域、this、原型链、异步、类型转换) | 选择题、手写题、简答题全面覆盖 |
| P0 | 浏览器渲染机制和性能优化 | 简答题高频考点,也是项目优化的理论依据 |
| P0 | HTTP协议、缓存机制、跨域 | 大厂必考,且容易形成区分度 |
| P1 | 手写常见工具函数(深拷贝、防抖节流、Promise系列、事件总线) | 笔试编程题直接考,需要达到默写水平 |
| P1 | 现代框架原理(React或Vue的响应式、生命周期、Diff) | 简答和面试会追问,笔试选择题占比不大 |
| P2 | 工程化工具的使用和原理(Webpack、Vite、Babel) | 简答题常考,但选择题涉及不多 |
| P2 | TypeScript | 2020年考得不多,但之后越来越重要 |
照着这个清单去分配时间,比漫无目的地刷题效率高很多。尤其是P0层级的内容,几乎每一场前端笔试都会遇到。
5.3 我的一个私藏方法:用“给自己讲一遍”代替单纯刷题
备考期间我一直在做一件事:每学完一个知识点,不急着做下一题,而是关掉浏览器,尝试把这个知识点用最直白的话讲给一个“完全不懂前端的人”听。比如讲事件循环,我会说:“JavaScript是单线程的,但浏览器背后还有定时器线程、网络线程。等你当前的代码跑完,浏览器就会把定时器回调、Promise回调按顺序取出来执行。Promise的回调永远排在定时器回调前面。”
这个方法听起来很笨,但效果奇好。因为在讲的过程中,发现自己讲不通的地方,就是理解最薄弱的地方。第三批试卷里那些“代码输出顺序题”,如果我能一句话把事件循环讲清楚,做题的时候基本不用一步步推,扫一眼就能凭直觉选出来。这种直觉不是天生的,是反复输出练出来的。
5.4 考完之后我才意识到:一份试卷的价值不在分数,在“查漏清单”
最后说说心态。当年考完第三批试卷,我大概能估到自己的正确率也就七成左右,但我没有陷入“完了,这波进不了面试”的焦虑,反而当天晚上就拿了张纸,把所有拿不准的题对应的知识点列了出来:事件循环里微任务嵌套、Cache-Control的no-cache语义、Object.create(null)与普通对象的区别、WeakMap的弱引用特性、Compositor线程在渲染管线里的位置。列了差不多20条。
这份清单后来成了我整个秋招复习的核心。我对着它一个一个查漏补缺,比任何网课和题库都更有针对性。所以如果你现在也在准备笔试,考完之后千万别只对一对答案就完事,更别把希望寄托在“多刷几套题”上。正确的做法是:每考完一套,就把“不确定的题”变成“知识点的漏洞清单”,再逐一击破。这才是校招笔试真正带给你的成长。
回头看,2020年百度第三批那张试卷,题目本身已经有些模糊了,但那种“每一道题都在提醒我,前端工程师不能只做一个页面拼接工”的感觉,我到现在还记得。它会让你重新审视自己学过的每一个知识点:是真的懂了,还是只是看着眼熟。想明白这一点,哪怕笔试没过,这一趟也值了。