深度解析欢聚时代校招前端A卷:从JavaScript到浏览器原理
2026/9/5 6:54:53 网站建设 项目流程

每年校招季,各家公司笔试题目总会在学生群里被翻来覆去地讨论。欢聚时代2018年校招的web前端A卷,就是当时讨论热度很高的一份。我印象最深的一点是,整张卷子并不靠偏题怪题来刁难人,它的命题思路非常贴近一线开发场景——不考你背了多少API,而是看你在写页面、写JS的时候,有没有真正理解浏览器在替你做什么、JS引擎在怎么执行你的代码。

这份卷子适合两类人:一是正在准备前端校招的应届生,可以把题目当成知识盲区对照表;二是入行一两年、想系统查漏补缺的前端工程师。整张卷子分为选择题、简答题和手写编程题三大模块,覆盖范围基本落在四个方向:JavaScript语言特性、浏览器工作原理、HTML/CSS布局、网络协议与Web安全。下面我按这几个方向,把命题逻辑拆开,结合我当时备考和实际面试的经验来聊。

1. 卷子结构与考察方向拆解

1.1 整体命题逻辑:为什么考这些,不考那些

先给没参加过校招的朋友科普一下笔试设计的基本逻辑。校招笔试不是用来挑“最强天才”的,而是用来做第一轮粗筛的。面试官一天要面几十个人,笔试必须用尽量少的题目,把“基础不牢靠”和“基础扎实有潜力”这两类人快速分开。所以在有限时间内,命题人会优先选择覆盖面广、区分度高的知识点,而不是某个小众框架的上层用法。

欢聚时代这份A卷的题型分布,在当年很有代表性:

模块题型题量占比考察目标
JavaScript语言特性单选/多选/手写约40%语法理解、运行机制、编码能力
浏览器原理单选/简答约20%渲染过程、缓存、异步事件
HTML/CSS布局单选/手写约20%布局能力、盒模型、选择器
网络与安全单选/简答约10%HTTP协议、缓存、跨域与XSS
算法基础手写约10%数组/字符串处理、逻辑思维

从这个配比能看出来,JavaScript是绝对核心,这也是正常的。前端岗位的日常工作本质就是跟JS打交道,框架可以换,语言层面的功底骗不了人。浏览器原理占20%,因为它决定了你对线上问题的排查能力——为什么页面白屏、为什么滚动卡顿、为什么资源重复加载,这些都在浏览器原理的知识范畴里。

欢聚时代本身是做直播和音视频业务的,对实时交互体验要求很高。所以这份卷子里,异步编程、事件循环、性能优化相关的题占比明显高于一般公司。这也是命题逻辑里很有意思的一层:企业的业务形态会直接影响考点侧重。备考时候如果能提前了解目标公司的业务方向,大致能猜出它更重视哪一块。

1.2 难度定位:基础题的“细节陷阱”

A卷的整体难度属于当年的主流水平,没有特别刁钻的题,但细节题很密。什么叫细节题?就是看起来每个选项都认识,但考的是你平时会不会真正去思考的那些点。

举个例子,JS基础经常出现这种题:判断typeof nulltypeof function(){}typeof []分别是什么。如果你平时写代码没想过这类问题,光靠感觉答题很容易在“object”和“function”之间犹豫。再比如0.1 + 0.2 === 0.3的结果为什么是false,这背后是浮点数精度问题,属于计算机基础范畴,但很多前端同学直到工作两三年都没搞明白。

细节题的价值在于:它不是死记硬背能答好的,它反映的是候选人平时会不会深入思考。面试官真正想看的不是你记住了多少结论,而是你有没有建立“遇到问题追到原理层面”的习惯。

2. JavaScript核心考点:扎实功底是分水岭

2.1 闭包、作用域与this指向:必考且拉开差距

JavaScript这部分,几乎可以确定会出现闭包和作用域的题。最经典的代码题长这样:

for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, 100); }

问输出结果是什么。答案不是0、1、2、3、4,而是连续5个5。考点有两个:一是var没有块级作用域,循环里创建的i是同一个变量;二是setTimeout的回调是宏任务,要等同步代码执行完才运行,而那时循环已经结束,i已经变成5。

这道题的改法在当年是手写题的常客:改成let声明,或者用闭包传参:

for (var i = 0; i < 5; i++) { (function(j) { setTimeout(function() { console.log(j); }, 100); })(i); }

用IIFE把每次循环的i值作为参数固化到闭包里。后来ES6普及,let成了首选方案。但笔试如果只写let,建议把原理讲清楚,单纯换成let说明你只是知道结论,不一定理解背后的作用域机制。

this指向的题也几乎是必考的。常见考法:

var name = 'window'; var obj = { name: 'obj', getName: function() { return this.name; } }; var fn = obj.getName; console.log(fn()); console.log(obj.getName());

第一行输出'window',第二行输出'obj'。原因在于this的绑定规则:谁调用函数,this就指向谁。fn()是全局调用,this指向window;obj.getName()是对象方法调用,this指向obj。

当年还有一道扩充题,把所有情况串起来:普通函数调用、对象方法调用、call/apply/bind调用、new调用、箭头函数。每写一个调用方式,后面就跟一个输出结果。这类题考察的是你对this绑定优先级有没有完整的理解——new > bind > call/apply > 对象方法调用 > 默认绑定。箭头函数则特殊,它没有自己的this,往外层作用域找。

2.2 原型链与继承:手写题的决定性题目

原型和继承是2018年前后前端笔试的“压轴常客”。选择题考instanceof的判断结果,手写题大概率让你实现一个继承。

先看选择题经典:

function Parent() { this.name = 'parent'; } function Child() { this.age = 1; } Child.prototype = new Parent(); var child = new Child(); console.log(child instanceof Parent); console.log(child instanceof Child); console.log(child.constructor === Parent);

前两个输出都是true,因为child的原型链上既有Child.prototype,也能通过Child.prototype内部的[[Prototype]]找到Parent.prototype。第三个容易丢分:child.constructor此时指向的是Parent。原因在于Child.prototype被整个重新赋值为new Parent(),这个对象身上的constructor属性继承自Parent.prototype,指向Parent

手写继承题,当时最稳妥的回答是“组合继承”和“寄生组合继承”。我建议优先掌握寄生组合继承,它兼容了原型链继承和构造函数继承的优点,也是ES6 class继承在Babel转译后的实际实现方式:

function Parent(name) { this.name = name || 'parent'; this.colors = ['red', 'blue']; } Parent.prototype.sayHello = function() { console.log('Hello ' + this.name); }; function Child(name, age) { Parent.call(this, name); this.age = age; } Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; Child.prototype.sayAge = function() { console.log(this.age); };

这里核心是Object.create(Parent.prototype),它创建一个以父类原型为新对象原型的对象,再赋给Child.prototype。这样避免了直接new Parent()带来的多余属性,也保证了instanceof判断正确。最后一定要补Child.prototype.constructor = Child,修复constructor指向,否则后面判断对象构造函数会出问题。

2.3 事件循环与异步机制:直播业务公司的重点题

前面提到欢聚时代做直播,异步处理在业务里非常重要。所以这套卷子里,事件循环相关的输出题占比不低,而且难度逐年上升。

基础题是经典的宏任务/微任务输出顺序:

console.log('A'); setTimeout(function() { console.log('B'); }, 0); Promise.resolve().then(function() { console.log('C'); }); console.log('D');

输出顺序是A、D、C、B。原因是:同步代码先全部执行完,然后执行微任务队列(Promise.then回调),最后才是宏任务队列(setTimeout回调)。这个知识点在当年很多人会搞反,以为setTimeout的0毫秒是立即执行,实际上0毫秒只是表示“尽快加入宏任务队列”,并不能插队到微任务前面。

进阶版会叠加async/await:

async function test() { console.log('start'); await Promise.resolve(); console.log('end'); } test(); console.log('sync');

这里的输出需要理解await的语义。await右侧的表达式会立即执行,遇到await时test函数暂停,后续代码作为微任务排队。所以输出是:start、sync、end。

遇到这类题,我建议在草稿纸上画一下任务队列:先写同步任务,再画一个微任务队列和一个宏任务队列,遇到Promise.then和await后面代码就往微任务队列塞,遇到setTimeout就往宏任务队列塞。按这个流程走,基本不会错。而且这个思路本身也是实际排查线上问题时需要的能力——比如为什么某个接口回调比另一个后执行,为什么图表渲染在滚动事件之后才更新,本质都是任务队列的顺序问题。

3. 浏览器原理与网络:一线开发的基本功

3.1 从输入URL到页面渲染:简答题的经典框架

这类简答题几乎每个前端笔试都会出现,A卷也不例外。题目一般长这样:“描述在浏览器地址栏输入网址并回车,到页面完整显示,中间经历了哪些步骤?”

完整的回答框架是:

  1. URL解析:浏览器判断输入是合法URL还是搜索关键词,补全协议和路径。
  2. DNS解析:把域名解析成IP地址,依次查找浏览器缓存、系统缓存、路由器缓存、根DNS服务器。
  3. 建立TCP连接:三次握手,客户端发送SYN、服务器返回SYN+ACK、客户端回复ACK。
  4. 发起HTTP请求:构造请求行、请求头、请求体,通过TCP发送。
  5. 服务器处理并返回:后端处理请求,返回响应报文和响应体。
  6. 浏览器解析渲染:解析HTML构建DOM树、解析CSS构建CSSOM树、两者合成渲染树、计算布局、绘制并合成图层。
  7. 断开连接:短连接四次挥手,或者保持长连接复用。

我当时复习这道题时,专门整理了一段口诀:“解析域名、建立连接、发送请求、解析响应、构建渲染、执行脚本”。回答时要注意两点:一是有逻辑层次,从网络层面到渲染层面逐步展开;二是要提到关键细节,比如渲染过程中遇到<script>标签会阻塞DOM解析,所以脚本一般放body底部或加defer/async。这个细节在笔试里很容易被忽略,但恰恰是面试官判断你“真的理解”还是“背过答案”的分界线。

3.2 回流重绘与性能优化:面试加分项

渲染原理延伸出来的高频题就是回流(reflow/layout)和重绘(repaint)。笔试选择题会这么考:以下哪些操作会触发回流?选项一般包括:修改元素宽度、修改颜色、读取offsetWidth、删除DOM节点、改变窗口大小、添加类名。

这里有个容易踩坑的点:很多人以为“读取”不会触发回流,但offsetWidthclientWidthgetComputedStyle这类读取操作会强制浏览器同步完成布局计算,然后才返回值。原因是浏览器为了性能会合并多个样式修改,延迟一次统一回流,但当你读取布局属性时,它没办法给个“过期”的值,只能先算一遍。

关于回流的优化手段,我在实际项目中验证有效的有几条:

  • 把需要多次修改的DOM操作合并,用document.createDocumentFragment()或者一次性改class
  • transform代替top/left做动画,因为transform走合成器,不触发回流重绘。
  • 对需要高频触发的事件(如scroll、resize)做防抖或节流。
  • 需要读取布局属性时,尽量一次性读取缓存,不要反复读。
  • will-changecontain属性减少渲染范围。

这些优化思路在笔试里可以作为简答题的补充答案,在面试中则是实打实的加分项。尤其是“transform为什么比top高性能”这个追问,能答出“合成器在GPU上处理,不经过布局和绘制”就算过关。

3.3 HTTP缓存机制与状态码:细节决定成败

网络协议部分,A卷侧重缓存机制。核心考点是强缓存和协商缓存的完整链路。

强缓存有两个头字段:Expires(HTTP/1.0)和Cache-Control(HTTP/1.1)。Cache-Control: max-age=3600表示从请求时刻起缓存1小时。而Expires是绝对时间,如果客户端时间和服务器时间不一致,缓存就会失效,所以现代项目基本以Cache-Control为主。

协商缓存也有两个头字段:Last-Modified/If-Modified-Since按文件修改时间判断,ETag/If-None-Match按文件内容指纹判断。两者区别在于:修改时间只能精确到秒,同一秒内多次修改就判断不出来;ETag是内容哈希,更精确但计算成本更高。服务器返回304表示“你可以用本地缓存”,这次请求不返回响应体。

缓存题还有一种变体:无缓存刷新(Ctrl+F5)和普通刷新(F5)有什么区别?普通刷新会带上If-Modified-SinceIf-None-Match,走协商缓存;强制刷新则会绕过强缓存和协商缓存,直接向服务器发新请求。

状态码的考点集中在:200成功、301永久重定向、302临时重定向、304缓存命中、403禁止访问、404找不到资源、500服务器内部错误、502网关错误。还有个容易混淆的:301和302的区别,一个是永久迁移(搜索引擎会更新链接权重),一个是临时跳转。

4. CSS/HTML应用题:布局细节最容易翻车

4.1 经典布局的实现与对比

CSS部分,A卷还是老套路:盒模型、居中布局、两栏/三栏布局、flex布局。这些题目本身不难,但很多人因为平时写代码依赖框架,自己手写的时候反而不利索。

盒模型的考点是:标准盒模型和IE盒模型的区别。标准盒模型的width只包含内容区,paddingborder是额外加上的;IE盒模型的width包含内容、paddingborderbox-sizing: border-box就是把元素设置为IE盒模型,这在移动端布局里非常实用,因为子元素设置百分比宽度后再加padding不会超出父容器。

垂直水平居中方案是手写题常客。当年我给出的标准答案至少要有四种:

/* 方案一:flex */ .parent { display: flex; justify-content: center; align-items: center; } /* 方案二:绝对定位 + transform */ .parent { position: relative; } .child { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } /* 方案三:margin: auto 配合绝对定位 */ .child { position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; } /* 方案四:table-cell */ .parent { display: table-cell; text-align: center; vertical-align: middle; }

笔试要求写两到三种,面试则希望你能说出每种方案的局限性和适用场景。比如flex方案要求父容器高度确定,绝对定位+transform要求父容器是定位父级,table-cell适合旧浏览器兼容。能讲出这些,才算真正掌握了。

三栏布局是CSS布局题里的“定番题”。圣杯布局和双飞翼布局的思路都是左右两栏定宽、中间自适应,区别在于圣杯用padding腾位置,双飞翼用中间栏内部的margin腾位置。我当时练习时做了个判断:双飞翼的逻辑更直接,不容易写错,但两者原理都要会说。2023年以后flex和grid已经全面普及,这类传统方案在笔试中的出现频率有所下降,但一旦出现,如果答不上来会比较尴尬。

4.2 选择器优先级与BFC:细节里的高分题

选择器优先级计算是选择题高发区。记一个权重模型:内联样式1000,id选择器100,类/属性/伪类10,元素/伪元素1。比较时按位相加,大的优先。!important优先级最高,但慎用,因为它会破坏级联规则,后期维护很难覆盖。

BFC(块级格式化上下文)是这一部分的另一个考点。选择题一般问你哪个属性会触发BFC,或者BFC能解决什么问题。

触发BFC的条件有:根元素、float不为none、position为absolute或fixed、display为inline-block或table-cell、overflow不为visible、display: flow-root

BFC解决的核心问题有三个:

  • 清除浮动:子元素浮动导致父容器高度塌陷时,给父容器创建BFC,它能包含浮动子元素。
  • 防止margin合并:垂直方向上相邻元素的margin会合并,把其中一个元素包进BFC可以阻止合并。
  • 阻止元素被浮动元素覆盖:BFC区域不会与浮动元素重叠。

我当时手写过一段清除浮动的兼容写法,.clearfix::after { content: ''; display: block; clear: both; },这个方案比给父容器加overflow: hidden更安全,因为后者在某些场景下会裁掉下拉菜单之类的溢出内容。笔试简答题里能写出这种细节,是加分项。

5. 手写编程题:解题过程与边界处理

5.1 防抖与节流:必考手写题,要注意边界

手写题第一道高频题就是防抖(debounce)和节流(throttle)。题目有时候只让你写“实现防抖函数”,有时候更鸡贼,要求“实现防抖函数,并说明它和节流的区别”。

防抖的核心逻辑是:事件触发后设定一个等待时间,等待时间内再次触发则重新计时,只有停止触发后才执行一次。

function debounce(fn, delay) { let timer = null; return function(...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); timer = null; }, delay); }; }

我当时写这段时容易漏两个点:一是this的绑定问题,回调里直接用fn()会导致this丢失,必须用fn.apply(this, args)把this传进去;二是clearTimeout之后timer要置null,防止连续调用次数的累积污染。

节流的核心逻辑是:固定时间间隔内只执行一次。

function throttle(fn, interval) { let last = 0; return function(...args) { let now = Date.now(); if (now - last >= interval) { last = now; fn.apply(this, args); } }; }

还有一种用定时器实现的版本,区别在于末尾是否多执行一次。笔试时如果能写出“第一次触发立即执行,结束后再补充执行一次”的版本,并解释两种时间的差异,基本就是满分答案。

5.2 数组去重与扁平化:一题多解展示功底

数组去重是另一道高频手写题。直接用Set是最简洁的:

const unique = (arr) => [...new Set(arr)];

但只写一行可能让面试官觉得你只会API,所以建议同时写出filter加indexOf的方案:

const unique = (arr) => arr.filter((item, index) => arr.indexOf(item) === index);

这道题想拿高分,关键在于主动说清楚不同方案的适用场景。比如Set方案无法区分1'1'?其实不是,Set使用SameValueZero比较,1'1'是不同的值,NaN会被正确去重,这是它比indexOf方案强的地方。但如果数组元素是对象,Set做的是引用比较,无法按对象内容去重,这时候需要自己写reduceMap组合方案。

数组扁平化也是一道经典题。ES6有Array.prototype.flat(depth),但手写实现要考察递归能力:

function flatten(arr) { return arr.reduce((prev, cur) => { return prev.concat(Array.isArray(cur) ? flatten(cur) : cur); }, []); }

注意:递归方案对嵌套特别深的数组可能导致调用栈溢出。如果面试追问,可以用栈或迭代来优化,把深度控制在O(n)空间。我在写这段时,会把concat和展开运算符[...prev, ...]的性能差异也提一下,这个细节能体现你对代码性能的敏感度。

5.3 手写发布订阅:Event Bus的常规操作

发布订阅模式是这类笔试里比较有区分度的手写题。题目通常说:实现一个简单的EventEmitter,支持ononceoffemit四个方法。

class EventEmitter { constructor() { this.events = new Map(); } on(name, fn) { if (!this.events.has(name)) { this.events.set(name, []); } this.events.get(name).push(fn); } once(name, fn) { const wrapper = (...args) => { fn.apply(this, args); this.off(name, wrapper); }; this.on(name, wrapper); } off(name, fn) { if (!this.events.has(name)) return; if (!fn) { this.events.delete(name); return; } const fns = this.events.get(name); const index = fns.indexOf(fn); if (index > -1) fns.splice(index, 1); } emit(name, ...args) { const fns = this.events.get(name); if (fns && fns.length) { fns.slice().forEach(fn => fn.apply(this, args)); } } }

这题有几个容易丢分的细节:once注册的回调执行后要立即解绑,所以外面包了一层wrapperoff时必须能用wrapper从列表中删除;emit时遍历回调数组要用slice()浅拷贝一份,防止回调内部调用off导致正在遍历的数组被修改;off如果不传具体函数,应该清空该事件的所有回调。这些边界情况在笔试里不一定要求全部都写出来,但能在白纸上写完整,说明你的工程意识是有的。

6. 笔试常见丢分点与复习路线建议

6.1 我观察到的三大丢分原因

先说丢分点。我身边参加过这份笔试的同学,回来说得最多的不是“题不会”,而是“会但是没写全”。这与校招笔试的评分方式密切相关:手写题往往是按点给分的,少了边界处理、少了原理说明,扣分都很正常。

第一个丢分点是手写题没有处理边界条件。比如实现防抖函数时不处理this绑定,实现EventEmitter时不考虑once回调的自动解绑,数组去重时忽略NaN的去重需求。这些边界情况在真实项目中很重要,笔试评分时也是区分“背过模板”和“真正理解”的关键判据。

第二个丢分点是选择题里模棱两可的选项过于纠结,导致时间分配失衡。一张卷子选择题大概30道,建议每道不超过1分半,不会的先跳过,把时间留给手写题。手写题分值高、区分度大,写不全基本就等于丢了这部分分数。

第三个丢分点是简答题只写条目不解释。问“浏览器渲染过程”,如果只写“构建DOM树、构建CSSOM树、合成渲染树”三行,分数一定不高。阅卷人想看的是你对每一步的理解,比如构建DOM树遇到<script>怎么处理、CSSOM树会不会阻塞渲染、合成树和渲染树的区别。解释得越细,越能证明你真的做过页面性能排查。

6.2 针对性的复习路线:从笔试到面试的延续

备考前端校招,我的建议是分三条线并行走。

第一条线是语言基础。把JavaScript的核心机制过一遍,重点不是背API,而是理解原理:作用域链、闭包、原型链、this绑定规则、async/await和Promise的执行顺序。推荐的方式是每个知识点都自己写一个小例子,在控制台验证输出,再尝试解释原因。

第二条线是浏览器和网络。推荐动手做一个“输入URL到页面渲染”的完整实验,在网络面板里观察强缓存和协商缓存的请求头变化,打开Performance面板看回流重绘和长任务。这些实践能帮你把抽象概念和真实现象对应起来。

第三条线是刷手写题。防抖节流、数组去重、数组扁平化、实现Promise、深拷贝、EventEmitter,这些是前端笔试的常青题目。我的建议是每个题至少手写三遍:第一遍看答案后默写,第二遍闭卷独立写,第三遍尝试优化和扩展。

还有一条面试层面的建议:笔试题目通常会在面试中被追问。答完一道手写题,面试官很可能会问“这段代码有什么问题”“如果并发调用会怎么样”“内存方面有没有考虑”。所以复习时不要只满足于写出来,要有意识地思考代码的边界和优化空间。

写在最后

回看欢聚时代这份2018年的web前端A卷,我觉得它最大的参考价值不是题目本身,而是它对基础知识的重视程度。很多人在校招季容易陷入一种焦虑,拼命追最新框架、最新特性,却被一张考察闭包和事件循环的卷子教做人。事实是,技术栈会变,框架会过时,但JavaScript语言的底层机制、浏览器的工作原理、HTTP协议的交互逻辑,这些才是前端工程师真正的护城河。

如果你正在准备校招,建议把这份卷子当作一面镜子,逐个考点自查,哪一块不熟悉就补哪一块。我在实际备考中感受到,最有效的学习方式就是把每一个考点讲给别人听,讲不清楚的地方就是你的盲区。希望这份拆解能帮你少走一些我当时走过的弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询