我不是来跟你探讨人生意义的,咱们直接点:百度2020校招Web前端工程师笔试卷(第三批),这套题放在今天看,依然是前端校招笔试里非常有代表性的“试金石”。别管你现在是刚啃完《JavaScript高级程序设计》的应届生,还是工作了两三年想查漏补缺的开发者,这份试卷涉及的考点覆盖面、考察深度,都值得你花几个小时认真过一遍。
很多人一听到“校招笔试题”就以为是在考“背诵”,其实恰恰相反。百度这套卷子最刁钻的地方在于,它不考你“知道什么”,而是考你“怎么用”和“为什么这么用”。JS原型链、事件循环、跨域方案、手写Promise、HTTP缓存……这些八股文里都有的概念,被出题人包装成了一道道看似日常、实则暗藏杀机的实际场景题。这篇文章我会逐题拆解,把背后的原理、考官的出题意图、以及我当年踩过的坑全都翻出来讲透,让你不仅会做题,更懂题。
1. 试卷整体设计与出题思路拆解
1.1 百度的校招笔试在“考什么”
如果你翻看过百度历年校招真题,会发现一个规律:基础题占60%,进阶题占30%,区分度题占10%。第三批这份卷子也逃不出这个框架。但是“基础”这两个字,在百度这里从来不意味着简单,它意味着把最核心的JS概念、浏览器机制、网络协议嚼碎了、揉烂了,然后包装成你可能在工作中头一遭遇到的场景。
整套卷子分四大部分:计算机基础与网络、JavaScript核心、浏览器与前端工程化、手写代码题。前两部分考察你是否具备一个合格前端工程师的“地基”,后两部分则直接测试你的“实战手感”。
有些同学复习的时候喜欢抱着一堆“面试宝典”背题,什么“说说你对闭包的理解”“Vue和React的区别”——在这套卷子里,这种复习方式效果甚微。百度的出题人更倾向给你一段代码,问你输出什么;给你一个需求场景,问你如何设计实现。这背后考察的不是记忆力,而是你在真实开发环境里的问题定位能力和设计决策能力。
1.2 时间分配与做题策略
这套卷子题目量不算少,满分100分,考试时间我记得是90分钟。从第三批的回忆版来看,选择题和填空题大约占40分,简答题和编程题占60分。这意味着如果你在选择题上纠结太久,后面的程序题基本来不及写。
我当时做这套题的时候,策略是“先扫全卷,脑子里面先排优先级”。拿到卷子先花3分钟把所有题目浏览一遍,不做任何题,纯粹“摸底”。看到手写代码题的大致考点后,在草稿纸上迅速列一下可能的解题思路,因为人脑在切换不同类型题目时是有“启动成本”的,提前浏览一遍能让你在做选择题的时候,潜意识里也在构建编程题的答案框架。
提示:时间分配上,建议选择题+填空题控制在35~40分钟以内,简答题20分钟左右,剩下30~40分钟集中攻克手写代码题。代码题分值高、采分点明确,务必留足时间。
2. JavaScript核心知识点逐题破局
2.1 原型链与继承:不只是画张图
这份卷子里关于原型链的考察,几乎可以确定是“必答题”。但是百度很少直接问“什么是原型链”这种开放性概念题,它更常见的出题方式是给你一段多层继承的代码,让你判断某个属性访问的结果,或者是instanceof运算的结果。
很多人在学原型链时会陷入一个误区:认为prototype和__proto__的值是一样的。其实根本不是。prototype是构造函数独有的属性,__proto__是每个对象实例都有的属性,它指向构造该实例的构造函数的prototype。理解了这一点,你才能做对类似这样的变体题:function Foo() {}; const foo = new Foo(); foo.__proto__ === Foo.prototype,结果当然是true,但如果把foo换成Foo本身呢?Foo.__proto__ === Function.prototype也是true,因为Foo是Function的实例。
第三批试卷里有一道题我记得很清楚,它给了三个构造函数A、B、C,让它们之间形成了A.prototype = Object.create(B.prototype)这样的链式继承,然后问实例new A()的instanceof C的结果。这道题的核心逻辑是:instanceof运算符检查的是构造函数的prototype是否存在于实例的原型链上。只要C.prototype在A实例的原型链上,结果就是true,即使C和A在代码结构上没有任何直接关系。
我建议复习的时候别只看结论,自己动手在浏览器控制台里把每个对象的__proto__打印出来,走一遍链式查找的路径,比背十遍概念都有用。
2.2 事件循环与异步:输出顺序背后的闭环逻辑
事件循环这套题,基本是校招笔试里“失分重灾区”。JavaScript是单线程语言,异步代码靠事件循环机制来调度。这个考点在第三批卷子里,通常以“给出异步代码,写出输出顺序”的形式出现,而且一定会把setTimeout、Promise、async/await、process.nextTick(如果是Node环境)混在一起。
做对这类题的关键是“看清微任务和宏任务的边界”。每执行完一个宏任务,引擎会清空整个微任务队列,再取下一个宏任务。Promise.then和async/await后面的代码属于微任务;setTimeout、setInterval、I/O操作属于宏任务。
但是这里有一个特别“坑”的细节:async/await的await行,其实相当于一个Promise.resolve().then()的语法糖。await前面的代码是同步执行的,await后面的代码是微任务执行的。如果遇到await Promise.resolve(1)这种代码,很多新手会以为微任务要等下一轮才执行,其实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(); new Promise((resolve) => { console.log('promise1'); resolve(); }).then(() => { console.log('promise2'); }); console.log('script end');这道题的输出顺序,你如果现在还不能脱口而出,建议把事件循环的核心原理再复盘一遍。做题的时候,按照“同步代码优先 → 微任务队列清空 → 宏任务执行 → 微任务队列再次清空”的顺序,在草稿纸上画一个队列图,每一步往队列里放任务,再按顺序取出来。
2.3 作用域与闭包:从“现象”到“本质”
闭包这个概念,十个面试官九个会问,但百度的卷子里通常会把闭包和var/let的变量提升、块级作用域结合起来考。比如经典的for循环中使用var定义变量,然后通过setTimeout打印索引的题目,在第三批卷子里换了个包装出现。
很多同学知道“用let就能解决”,但问起为什么,却只能答出“因为let是块级作用域”。这个回答太浅了。正确的理解是:var声明的变量会被提升到函数作用域顶部,整个循环过程中,变量i始终是同一个变量;而let声明的变量,在每次迭代中都会创建一个新的词法环境绑定,也就是说每次循环的i都是一个独立变量,闭包捕获的是各自独立的绑定。
如果笔试里遇到“不用let,如何解决这个问题”这种连环追问,你可以用IIFE(立即执行函数)包裹,给每次循环创建一个独立作用域,然后在里面保存当前的i值。另一种方法是使用forEach、map这类高阶函数来创建独立上下文。能把这个过程讲清楚,说明你是真的懂闭包的本质,而不是背了一个答案。
3. 浏览器、网络与前端工程化实战解析
3.1 HTTP缓存机制:一套很吃经验的“送分题”
坦白讲,HTTP缓存这个知识点在校招笔试里其实是“相对友好”的,因为它规则清晰、答案固定。但百度的卷子绝不会直接问“强缓存和协商缓存的区别”,它会把场景复杂化:给你一个页面引用的三个静态资源,分别命中了不同的缓存状态,问你网络请求的状态码、资源来源,以及为什么会出现这种差异。
要拿稳这部分分,你脑子里的缓存体系必须清晰:
- 强缓存:浏览器在缓存有效期内直接使用本地资源,不发请求,对应的响应头是
Cache-Control: max-age=...和Expires。 - 协商缓存:资源有可能过期了,于是浏览器带上
If-Modified-Since或If-None-Match请求头,去问服务器资源有没有变。服务器返回304 Not Modified就继续用缓存,返回200就重新拉取并更新缓存。
关于Cache-Control的no-cache和no-store,有很多人混为一谈。no-cache的意思是“允许缓存,但每次使用前要跟服务器确认一下”,也就是强制走协商缓存;而no-store才是真正的“不缓存”。第三批卷子里出现过一道判断题,问“Cache-Control: no-cache表示不缓存”,如果你选了“对”,那这题就丢了。
除此之外,你可能还会遇到ETag和Last-Modified同时存在时的优先级问题。记住一个原则:ETag的优先级高于Last-Modified,因为前者是资源内容哈希,后者是时间戳,内容哈希更精准。
3.2 跨域方案:不止是CORS配置一下
跨域问题在笔试试卷里属于“必考且必深考”的知识点。百度的出题人喜欢让你结合场景去选择合理的跨域方案,而不是把CORS的单选配置背出来。比如:前端页面部署在www.example.com,后端接口在api.example.com,让你设计一个可行的方案,并说出为什么不用JSONP。
回答这类题的关键在于“对比维度”。你要能清晰说出几种主流方案的优缺点:
| 方案 | 适用场景 | 核心注意点 |
|---|---|---|
| CORS | 绝大多数前后端分离项目 | 需服务端配合设置响应头,携带Cookie需配置credentials |
| JSONP | 只支持GET请求的简单场景 | 动态创建script标签,安全性较差,易受XSS攻击 |
| 代理转发 | 开发环境/同域网关 | Webpack Dev Server / Nginx反向代理,不直接暴露后端地址 |
| postMessage | 跨窗口通信(iframe) | 需要处理消息来源校验,防止恶意攻击 |
把这张表的内容吃透,表达的时候再补充一些实际场景中的细节(比如CORS的预检请求OPTIONS的触发条件),基本就能覆盖大部分考点。
3.3 前端工程化与构建工具:答题思路从“会用它”到“懂它为什么”
2020年的前端校招笔试,Webpack还是绝对的霸主。第三批试卷里关于Webpack的考点,集中在loader和plugin的区别、打包优化的方向、如何做代码分割这几个方向。
很多同学复习Webpack时只知道“loader是用来处理文件的,plugin是用来做打包优化的”,这个说法其实不够准确。更严谨的理解是:loader是文件转换器,它操作的是单个文件内容,负责把非JS模块变成JS可理解的模块;plugin则是事件流监听器,它监听webpack构建生命周期中的特定事件,能介入整个打包过程的任意环节。比如HtmlWebpackPlugin在打包后帮你生成HTML文件,MiniCssExtractPlugin把CSS从JS代码中抽离成独立文件,这些都是loader做不到的。
回答Webpack优化的问题,我建议从“体积”和“速度”两个维度展开。体积优化包括:开启Tree Shaking(配合sideEffects配置)、压缩JS/CSS/HTML代码、按需加载(动态import)。速度优化包括:HappyPack或thread-loader多线程构建、DllPlugin预编译第三方库、cache-loader缓存中间产物。你可以顺便提一句Webpack 5内置的持久化缓存(cache: { type: 'filesystem' }),能明显提升二次构建速度,虽然当年百度校招主要考Webpack 4,但展示你对新版本的关注,是加分项。
4. 手写代码题的“得分密码”与实战演练
4.1 深拷贝:一个写不好直接拉胯的经典题
深拷贝是校招笔试手写题里的“常青树”。2020年的第三批卷子如果出了这道题,别高兴太早,它不会让你只处理普通对象和数组,一定会让你考虑循环引用、Symbol属性、函数、正则表达式等边界情况。
网上流传的“一行深拷贝”解法:JSON.parse(JSON.stringify(obj)),在笔试中千万别直接用,这只能处理JSON安全的数据结构,遇到undefined、函数、Symbol,属性会直接消失;遇到循环引用,直接报错。
一个合格的深拷贝函数,至少要满足以下几点:
- 使用弱引用Map(
WeakMap)来检测循环引用,避免内存溢出。 - 区分对象、数组、
Date、RegExp、Map、Set等内置类型。 - 使用
Reflect.ownKeys来遍历包括Symbol在内的所有键。
给你一个可以直接用于笔试现场的版本:
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); 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键,比Object.keys强很多。答题时在关键代码旁边简单注释一下设计思路,阅卷官能一眼看到你的知识面。
4.2 防抖与节流:从“背模板”到“讲场景”
防抖和节流在手写题中的出现频率,甚至比深拷贝还高。因为它在实际开发中的使用场景太广泛了:搜索框输入联想、滚动事件监听、窗口resize,几乎每天都要打交道。
但百度的卷子很少让你纯手写一个“标准答案”,它更倾向于给你一个动态变化的需求,让你进行改造。比如第三批的题可能是:“写一个带immediate参数的防抖函数,首次触发立即执行,后续在等待期内触发不会执行。”这比单纯的防抖又多了一个层次。
带immediate参数的防抖,核心逻辑是:记录首次触发的标志位。如果immediate为true且尚未触发过,立即调用函数;如果已经触发过,就重置定时器。
function debounce(func, wait, immediate = false) { let timer = null; let isInvoked = false; return function (...args) { const context = this; if (timer) clearTimeout(timer); if (immediate && !isInvoked) { func.apply(context, args); isInvoked = true; } else { timer = setTimeout(() => { func.apply(context, args); isInvoked = false; }, wait); } }; }这个版本考虑了this绑定问题,也考虑了参数传递。答题时建议用干净的ES6语法,不要用arguments耦合this,避免在箭头函数和普通函数的转换中把自己绕晕。
4.3 手写Promise:从“能跑”到“有设计感”
如果你的编程题部分出现了手写Promise,那基本上就是压轴题了。这一题答得好,前面即使有些小瑕疵,整体分也不会低。很多同学背过Promise/A+规范的代码,但在笔试现场最要命的不是写不出来,而是写出来的代码看起来对,运行却有问题。
百度的判题环境一般会在浏览器或Node里直接跑你的代码,所以如果你直接把Promise的状态栏写死,只处理了resolve没处理reject,或者把then的链式调用处理得不对,都会在隐藏测试用例里挂掉。
手写Promise的核心点有三个:
executor是同步执行的,状态只能从pending变为fulfilled或rejected。then方法里必须处理两个回调,且回调需要放入微任务队列(在旧环境里可用setTimeout模拟,但是引擎环境允许时用queueMicrotask)。then方法必须返回一个新的Promise,才能实现链式调用。
第三批卷子如果有这道题,建议你在基本实现之外,多做一步处理:在resolve或reject分支中,对x(即回调函数的返回值)进行“Promise解析流程处理”。也就是判断x是不是一个thenable对象,如果是,则递归处理它的then方法。这一小段逻辑,是区分“背代码选手”和“真懂Promise选手”的分水岭。
5. 高频易错题与实战排查技巧
5.1 笔试中容易“踩雷”的知识点
复习到这里,我给你盘一盘这套卷子里最容易错的地方,以及相关的故障排查思路。第一个是隐式类型转换。[] == ![]的结果是true,null == undefined的结果也是true,但null === undefined却是false,这种题全靠基本功。做题时,务必把引用类型toString()和valueOf()的调用优先级弄清楚:在对象转原始值时,默认先调用valueOf(),如果返回的不是原始值,再调用toString()。
第二个是CSS盒模型。很多同学在笔试里把box-sizing的属性值记反了。content-box是默认值,宽度只包含content;border-box的宽度则包含了content、padding和border。别小看这个知识点,2020年的一套前端笔试题里,它就藏在一道计算元素实际渲染宽度的选择题里,不少工作了两年的同学都算错了。
第三个是事件冒泡与事件委托。这不是什么新考点,但百度的出题人很喜欢出“动态添加的列表项如何绑定事件”的题目。答案就是事件委托,把监听器绑定在父级容器上,通过event.target判断触发源。如果笔试里让你用原生JS实现,别用onclick循环绑定,那样既浪费内存,又对动态元素无效。
5.2 遇到不会的题怎么办:保守策略
即使准备得再充分,考试时也一定会碰到拿不准的题。我的经验是:千万别空着。前端笔试题的采分点往往比较散,尤其是简答题,你写出一部分正确的理解,同样能捞到一定的分。
以“谈谈你对前端框架虚拟DOM的理解”为例,如果你完全不知道虚拟DOM的diff算法细节,也别慌。你至少可以写出:虚拟DOM是一个描述真实DOM结构的JS对象,操作它比直接操作真实DOM性能更高;框架通过对比新旧虚拟DOM的差异(diff),最小化真实DOM的变更范围。这已经覆盖了一半以上的采分点。如果你还能补上“在列表渲染时key的作用就是帮助diff算法精准复用节点”,那么这题的得分率就更高了。
对于代码题,即使不能完整写出答案,也建议写出你的思路草稿。比如手写防抖时,哪怕没写出immediate参数,你也可以注释上“如果需要在首次触发时立即执行,可以在这里加一个参数控制”。这能向阅卷官传达“你是有完整设计思路的,只是时间不够”,比交白卷强一百倍。
6. 基于这套试卷的复习路径建议
6.1 从现在到笔试,怎么安排复习节奏
如果你手头已经有这套卷子,打算开始刷题,我建议你不要做完对答案就完事,那样效果极差。我的做法是:按知识点模块刷题,每刷完一个模块,就去目录里找对应的章节把原理啃透,然后不看答案再重新做一遍错题。
具体的时间分配可以这样:前3天主攻JS基础(变量提升、作用域、闭包、原型链)和异步编程(事件循环、Promise);第4天到第5天主攻浏览器原理(渲染流程、缓存、跨域)和网络基础(HTTP协议状态码、TCP握手);第6天到第7天主攻框架相关(如果你简历上写的是Vue或React,至少要知道生命周期、数据双向绑定、组件通信等核心原理)和工程化(Webpack、Git)。
最后留两天做整体模拟。找一段完整时间,按考试时长掐表,在真实的OJ平台上做一遍整套卷子。不要分心看手机,模拟真实的考试环境,让自己提前适应做题节奏。
6.2 经典“原题”复盘:百度高频考点一览
为了让你复习更有针对性,我把百度近几年校招前端笔试题的高频考点整理成了表格,你可以对照着自查:
| 知识点模块 | 高频考点 | 考察深度 |
|---|---|---|
| JavaScript | 变量提升、闭包、原型链、事件循环 | 能分析代码输出,能解释原理 |
| 浏览器 | 渲染机制、缓存策略、跨域方案 | 能结合场景设计方案,能说出优化思路 |
| 网络 | HTTP状态码、TCP/UDP区别、HTTPS握手 | 能对比优缺点,能说明应用场景 |
| 框架 | 生命周期、虚拟DOM、数据流、组件通信 | 能解释设计动机,能写出具体实现代码 |
| 工程化 | Webpack打包流程、loader/plugin原理、性能优化 | 能说出优化策略,能定位构建问题 |
把表格里每一行的核心考点吃透,再去做第三批笔试卷,你心里就有底了。
6.3 写在最后:笔试只是起点
我最后再说几句实在话。做这套百度2020校招的笔试卷,你收获的绝不仅仅是一份“通过/不通过”的结果。这套题真正帮你打磨的是“前端工程师的思维底线”——那些你日常开发中可能忽略的底层机制,那些框架帮你藏起来的原理细节,都会在笔试题里被重新拎出来拷问你。
我当年作这套卷子的时候,其实也有好几道题没有答到点上。但正是那些不会做的题,逼着我后来花了大半个月的时间,把浏览器从输入URL到页面展示的整个链路、JS引擎从解析到执行的整个流程,彻彻底底地过了一遍。这种“被题目牵着鼻子走”的复习方式,反而让我在后来的面试中受益良多。
所以别怕做错,也别嫌题目老。2020年的卷子放到今天,核心的基础考点依然不过时。把每一道错题都当作一个“知识盲区探测器”,认真补全它,你的收获会远超一张“答得还不错”的卷面分。