百度2020校招前端笔试卷复盘:JS与浏览器原理考点精析 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/bindthis指向第一个参数指定的对象。如果是箭头函数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-age31536000, 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、延迟加载非首屏图片loadinglazy、使用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”。我给大家整理一个速查列表根元素htmlfloat不为noneposition为absolute或fixeddisplay为inline-block、table-cell、table-caption、flow-rootoverflow不为visible如hidden、auto、scrollflex/grid容器的直接子元素BFC的核心作用是隔离内外布局比如解决margin塌陷、清除浮动、阻止元素被浮动元素覆盖。实际项目中最常用的是给容器加overflow: hidden或display: flow-root来创建BFC。后者是更现代的方案不会意外裁剪内容。层叠上下文也是必考。很多人以为z-index大就应该盖在上面但前提是它们处于同一个层叠上下文。如果父元素形成了层叠上下文子元素的z-index只能在父级内部比较。例如div styleposition: relative; z-index: 10; div styleposition: absolute; z-index: 999;A/div /div div styleposition: 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的实现。笔试时手写代码是最能反映真实水平的部分。第四阶段做项目复盘。把你自己做过的项目按照“项目背景、技术选型、核心难点、解决方案、优化效果”的框架整理成文档。大厂笔试中的开放题往往和你个人项目经历挂钩提前准备好案例答起来会从容很多。另外我始终觉得刷题不是目的理解系统才是。前端这个岗位有趣的地方在于它是离用户最近的技术栈任何性能问题、兼容性问题、安全问题都可能在线上暴露。所以笔试中那些“为什么”才是真正珍贵的东西。最后再说一个实用建议做笔试卷时先做会做的标记卡壳的最后集中攻克。这套卷子题量不小如果一道输出顺序题纠结太久后面的手写题可能时间不够。我的习惯是用五分钟扫一遍所有题目把分值高且有把握的题优先完成再回头啃硬骨头。时间管理本身也是校招笔试考察的一部分你要做到像处理线上事故一样冷静。