先声明一下,这篇东西是我以一个参加过浩鲸科技校招前端笔试的求职者视角,结合前后几届同学的面经和自己在实际开发中的验证,把整套B卷的考点、答案逻辑和做题思路完整梳理了一遍。不是什么官方标准答案,但每道题背后的原理和踩坑点,都是实打实能用在面试和日常开发里的东西。如果你正在准备校招,或者工作两三年想回头补一补基础,这篇值得花半小时慢慢看。
1. 试卷整体结构与考点分布分析
先聊整体。浩鲸科技2020届前端B卷,总体题量不大,但覆盖面很典型,基本就是校招前端笔试题的标配组合:选择题(涵盖JS基础、浏览器机制、网络协议)、简答题(概念解释+场景设计)、手写代码题(算法 + 原生API实现)、外加一道前端工程化相关的开放题。
这套题真实反映了当时(以及现在依然是)校招前端考核的底层逻辑:不考框架API背诵,考的是语言本身和浏览器底层原理。Vue和React在当年的B卷里占比不高,更多出现在项目问答环节,笔试部分基本集中在原生JavaScript、HTTP、浏览器渲染、事件循环、作用域、闭包、原型链、异步编程这些硬核基础。
为什么这么设计?我后来工作久了才想明白——浩鲸这种体量的公司,校招进来的前端大概率要直接接触电信级B端系统,这些系统有大量数据展示、复杂表单、长列表渲染、图表交互的场景,而且历史包袱重,可能还在用老版本的框架甚至原生JS维护页面。所以笔试不考花架子,考的是你能不能在没有框架兜底的情况下把一个页面写明白,能不能理解异步时序,能不能定位性能瓶颈。
从考点权重来看,我根据回忆和同届交流整理了一个大致分布:
| 考点模块 | 题量占比 | 难度级别 | 核心考察点 |
|---|---|---|---|
| JS语言基础(原型/闭包/作用域) | 30% | 中等 | this指向、变量提升、原型链查找 |
| 异步编程与事件循环 | 20% | 中等偏高 | Promise时序、宏任务/微任务 |
| 浏览器与网络 | 20% | 中等 | 渲染流程、HTTP缓存、TCP握手 |
| 手写代码与算法 | 20% | 中等 | 数组去重、深拷贝、防抖节流 |
| 前端工程化与开放题 | 10% | 中等 | Webpack原理、性能优化方案 |
这个分布其实很良心,没有偏题怪题,每一类都能在牛客网和LeetCode上找到对应练习。如果你现在才开始准备,按这个权重去分配复习时间,性价比是最高的。
2. JS核心机制逐题拆解:原型、闭包与作用域
B卷的选择题和简答题里,JS语言基础占了将近三分之一,这是最值得细抠的部分。下面我把高频出现的几类考点逐一说透,包括题目背后真正想考察的思维模式。
2.1 原型链查找:不要只背结论,要会画图
选择题里几乎必有一道原型链相关的题,典型问法是:function Foo() {}然后问你Foo.__proto__、Foo.prototype.__proto__、Object.__proto__分别指向什么。这种题死记硬背很容易混,教大家一个我当年自己总结的笨办法——画查找链。
任何对象都有一个__proto__指向构造它那个函数的prototype,函数本身也是对象,所以函数也有__proto__。Foo是Function的实例,所以Foo.__proto__ === Function.prototype;Function.prototype是一个对象,它由Object构造,所以Function.prototype.__proto__ === Object.prototype;而Object.prototype是原型链的终点,它的__proto__是null。
这里有一个高频易错点:函数的prototype属性本身也是一个对象,这个对象的__proto__指向Object.prototype,而不是指向Function.prototype。也就是说Foo.prototype.__proto__ === Object.prototype而不是Foo.prototype.__proto__ === Function.prototype。我当年就是栽在这里,后来画了一遍完整的查找链才彻底理顺。
更进阶的考法是把new的过程混进来。new Foo()做了三件事:创建一个空对象、把这个对象的__proto__指向Foo.prototype、执行Foo函数体并把this绑定到这个新对象上。如果构造函数显式返回一个对象,那么new表达式的结果就是这个对象;如果返回的是基本类型或者没有 return,那么结果还是第一步创建的那个对象。这个知识点在简答题里也很常考,答题时建议把三步骤写全,再补一句"构造函数 return 对象时行为不同"的边界情况,能明显加分。
2.2 闭包与作用域链:从内存角度理解
闭包也是B卷的常客,常见的出题形式有两种:一种是让读代码输出结果,一种是用闭包实现某个功能。比如经典的循环打印问题:
for (var i = 0; i < 5; i++) { setTimeout(() => { console.log(i) }, 1000) }输出结果是 5 个 5,原因是var声明的i属于全局作用域,循环结束后i已经是 5,而回调函数是在循环结束后才执行的。用let替代var就能输出 0 到 4,因为let是块级作用域,每次循环都会创建一个独立的绑定。
但光是知道这个结论还不够,面试官更想听的是,你能否用闭包来解释解决方法:
for (var i = 0; i < 5; i++) { (function (j) { setTimeout(() => { console.log(j) }, 1000) })(i) }这个 IIFE 每次循环都会创建新的函数作用域,参数j被固定在了当前循环的i值上,回调函数引用的j就是闭包捕获的那个值,不会被后续循环覆盖。从内存视角看,闭包让j所在的执行上下文在循环结束后依然存活,因为回调函数持有对它的引用。
简答题里闭包通常结合应用场景来问,比如防抖、节流、单例模式、柯里化。答题时抓住一个核心:闭包是函数与其词法作用域的组合,它能让内部函数访问外部函数的变量,即使外部函数已经执行完毕。然后补一个实际场景说明"为什么需要这样做",而不是背定义。
2.3 this 指向:四条规则排序
this指向问题是B卷选择题里区分度最高的一类。题目一般会给出一个嵌套对象和函数调用的组合,让你判断this的值。
我总结的记忆方法是四条规则按优先级排序:
new绑定:用new调用函数时,this指向新创建的对象- 显式绑定:
call/apply/bind指定的this,优先级高于隐式绑定 - 隐式绑定:函数作为对象的方法调用时,
this指向该对象 - 默认绑定:独立函数调用时,
this指向undefined(严格模式)或全局对象(非严格模式)
箭头函数不在这四条规则之内,它的this是定义时外层词法作用域的this,且不能被call/apply/bind改变。B卷里很喜欢考"箭头函数 + setTimeout"的组合,记住箭头函数不绑定自己的this就能轻松拿分。
还有一个高频变体:const obj = { a: 1, fn: () => { console.log(this.a) } }。这里this不是obj,而是全局对象,因为箭头函数定义时所在的作用域是全局作用域,obj这个对象字面量不会产生函数作用域。很多人在这里翻车。
3. 事件循环与异步编程:校招笔试的分水岭
异步编程这块是B卷里最容易拉开分差的模块,也是面试时问得最深的。我这套题里至少有一道 Promise 组合输出的选择题,一道事件循环顺序的简答题。如果你能把这类题的输出顺序完整说出理由,基本上半只脚已经踏进二面了。
3.1 宏任务与微任务:从浏览器底层看执行顺序
要理清事件循环,先记住这两个概念:
- 宏任务(macrotask):
setTimeout、setInterval、I/O 操作、UI 渲染、requestAnimationFrame - 微任务(microtask):
Promise.then/catch/finally、queueMicrotask、MutationObserver
执行顺序是:执行一个宏任务 → 执行所有微任务 → 执行下一个宏任务。注意关键点:微任务是在两个宏任务之间全部清空的,而不是一个一个间隔执行。
经典考题:
console.log('1') setTimeout(() => { console.log('2') }, 0) Promise.resolve().then(() => { console.log('3') }) console.log('4')输出顺序是 1、4、3、2。原因是同步代码先执行完毕,微任务队列里的Promise.then在进入下一个宏任务之前被清空,所以3先于2输出。
B卷的进阶版会把多个 Promise 嵌套起来,比如Promise.resolve().then(() => { console.log(1) }).then(() => { console.log(2) })和setTimeout的组合。这种题的关键是:每次.then都会返回一个新的 Promise,并且新的.then回调会注册到微任务队列的尾部。如果前一个.then又返回了一个 Promise,那么后续的.then要等这个返回的 Promise resolve 之后才会进入微任务队列,这会导致执行顺序出现"看起来不按代码顺序"的现象。做这类题的时候,我的习惯是在草稿纸上画出任务队列的变化过程,先标同步,再标微任务,最后标宏任务,一步步推,基本不会错。
3.2 Promise 实现原理与手写技巧
B卷的手写题里可能出现"手写一个简易 Promise"这类题,也可能是让补全Promise.all或Promise.race的实现。如果时间紧张,优先掌握Promise.all和Promise.race,因为代码量短,逻辑清晰,性价比最高。
Promise.all的核心逻辑是:接收一个可迭代对象,返回一个新的 Promise;所有输入 Promise 都成功时,返回一个包含所有结果的数组;任何一个失败,则立即 reject。
function promiseAll(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) } }, (reason) => { reject(reason) } ) } }) }注意几个细节:用count计数而不是直接判断results.length === promises.length,是因为数组的稀疏情况会导致 length 判断出错;赋值用results[i]而不是results.push(),是为了保证结果顺序与输入一致;每个元素都包一层Promise.resolve()是为了兼容普通值输入。
我当年笔试这道题时没写Promise.resolve()这层兼容,直接被扣了分。后来面试官告诉我,如果入参是普通数字,Promise.resolve(promises[i])会把普通值转成已兑现的 Promise,而直接调用.then会报错。这个细节不到 10 行代码,但能拉开差距。
3.3 从事件循环延伸到浏览器渲染时机
B卷还有一个比较新的问法:把事件循环和渲染时机结合起来,问setTimeout回调里修改 DOM 会不会导致性能问题,或者requestAnimationFrame和setTimeout有什么区别。
标准回答是:浏览器渲染的时机是在宏任务执行完之后、下一个宏任务开始之前,具体是渲染前会先清空微任务队列。requestAnimationFrame的回调会在下一次重绘之前执行,所以用它做动画能保证和屏幕刷新率同步,不会掉帧。而setTimeout的最小延迟虽然是 4ms(嵌套调用时),但实际执行时间还会受到任务队列排队情况的影响,所以不能用来做精确计时。
这道题能答到"微任务在渲染前清空,所以修改 DOM 应该尽量放在微任务里,避免不必要的重排"这个层面,面试官基本就会认可你确实理解浏览器的调度机制。
4. 浏览器渲染机制与网络协议:从输入 URL 到页面显示
网络和浏览器这块,B卷的简答题经典问法是:从浏览器地址栏输入 URL 到页面展示,中间发生了什么。这道题几乎每个前端面试都会问,但大多数人的回答停留在"DNS 解析 → TCP 连接 → HTTP 请求 → 服务器响应 → 浏览器渲染"这一层。要拿高分,需要把每一步拆细,并且补充关键细节。
4.1 输入 URL 到页面展示的完整链路
我的回答框架是分两段:网络链路和渲染链路。
网络链路部分:
- 浏览器解析 URL,识别协议、域名、端口、路径
- DNS 查询:先查浏览器缓存,再查操作系统缓存,然后是本地 hosts 文件,都没命中就发起递归 DNS 查询
- 拿到 IP 后,如果协议是 HTTPS,先进行 TLS 握手(协商加密算法、交换证书、生成会话密钥)
- 建立 TCP 连接,三次握手
- 发送 HTTP 请求:请求行、请求头、请求体
- 服务器处理并返回响应:状态码、响应头、响应体
- 浏览器收到响应后,根据状态码和响应头决定是否走缓存、是否重定向
渲染链路部分:
- HTML 解析:构建 DOM 树
- CSS 解析:构建 CSSOM 树
- 合并 DOM 和 CSSOM,构建渲染树(render tree),注意只包含可见节点
- 布局(layout):计算每个节点的几何位置
- 绘制(paint):把节点绘制到屏幕上
- 合成(composite):如果涉及图层,由合成器合成最终画面
这里有一个高频加分项:JavaScript 的执行会阻塞 HTML 解析。当解析器遇到<script>标签时,会暂停 DOM 构建,先下载并执行 JS。所以实践中会把脚本放在 body 底部,或者使用defer和async属性。defer是解析完成后按顺序执行,async是下载完立即执行,不保证顺序。
B卷如果问得再深一点,会问"CSS 会不会阻塞渲染"。答案是 CSSOM 构建完成后才开始渲染树构建,所以 CSS 也会阻塞渲染,但不阻塞 HTML 解析(只是如果 CSS 没加载完,渲染树构建不了,页面白屏)。实践中把 CSS 放在 head 里尽早加载,就是为了让首屏尽快出现。
4.2 HTTP 缓存:强缓存与协商缓存的判断流程
HTTP 缓存是 B 卷选择题和简答题都爱考的点,而且在实际开发中每天都会碰到。核心是两个概念:强缓存和协商缓存。
强缓存阶段,浏览器直接命中本地缓存,不发请求,对应的响应头是Expires和Cache-Control。注意Cache-Control的优先级高于Expires,并且Cache-Control: max-age=3600是相对时间,而Expires是绝对时间,受客户端本地时钟影响,所以现在推荐用Cache-Control。
协商缓存发生在强缓存未命中的情况下。浏览器带上If-Modified-Since或If-None-Match请求头,服务器根据资源的Last-Modified和ETag判断资源是否变化。若未变化,返回 304 Not Modified,浏览器使用本地缓存;若变化了,返回 200 和新资源。
常见选择题陷阱:某资源设置了Cache-Control: no-store,问能不能使用协商缓存。答案是不能,no-store表示完全不缓存,连协商缓存都不走。而no-cache虽然名字带 no,实际意思是"可以缓存,但每次使用前必须向服务器验证",走的是协商缓存流程。
我当时笔试还遇到一道题问ETag和Last-Modified的区别。标准答法:ETag 是资源的唯一标识,可以解决Last-Modified精度不够(秒级)的问题,也能应对文件内容没变但修改时间变了的情况,所以 ETag 更精确;但 ETag 的计算有开销,通常是 hash 或者对文件属性做组合。
4.3 TCP 三次握手与四次挥手:不要只背数字
网络协议的题在 B 卷里占的比重不大,但如果出了,通常是选择题形式,问三次握手的目的是什么,或者四次挥手中 TIME_WAIT 状态出现在哪一端。
三次握手的核心目的是确认双方的收发能力都正常。第一次握手客户端发送 SYN,证明客户端的发送能力没问题;第二次握手服务器回复 SYN+ACK,证明服务器的接收和发送能力都没问题;第三次握手客户端回复 ACK,证明客户端的接收能力也没问题。三次之后双方确认彼此可以正常收发数据。
四次挥手和三次握手的区别是,由于 TCP 是全双工的,每一方的连接关闭都需要单独确认。客户端发送 FIN 表示"我没有数据要发了",服务器回复 ACK 表示"我知道了",然后服务器发送 FIN 表示"我也没有数据要发了",客户端回复 ACK。TIME_WAIT 出现在主动关闭连接的那一端(通常是客户端),需要等待 2MSL(time 2 Maximum Segment Lifetime)才能彻底关闭,目的是保证最后的 ACK 能到达对方,同时让旧连接的报文在网络中消散,避免影响新连接。
如果你能在面试时把这些"为什么"讲清楚,而不是只背"三次、四次",面试官对你的网络基础评价会高一个档次。
5. 手写代码题:高频题型的标准解法与易错点
B卷的手写代码题一般有 2 到 3 道,难度不超 LeetCode Easy 到 Medium,但很看重细节。下面这几道是我考的卷子里出现过的和同届同学反馈高频出现的,每道我都给出标准解法和易错点。
5.1 数组去重:从基础到进阶
数组去重是最常见的手写题,但我强烈建议不要只写new Set(arr)一个答案,而是至少准备三种解法,并说清各自的适用范围。
解法一:Set一行版
const unique = [...new Set(arr)]优点:代码简洁,性能好。缺点:只能处理基本类型,对象去重时会按引用比较(不同引用永远不相等)。
解法二:filter + indexOf
const unique = arr.filter((item, index) => arr.indexOf(item) === index)思路是保留"第一次出现"的元素。indexOf找到的是第一个匹配项的索引,如果当前索引和它相等,说明这是第一次出现。缺点是indexOf对NaN无效,因为indexOf内部用严格相等判断,而NaN !== NaN。
解法三:reduce + 哈希表
const unique = arr.reduce((acc, cur) => { if (!acc.includes(cur)) { acc.push(cur) } return acc }, [])如果需要对象按某个属性去重,可以把这个思路扩展成:
const uniqueByKey = arr.reduce((acc, cur) => { if (!acc.some((item) => item.id === cur.id)) { acc.push(cur) } return acc }, [])对NaN、undefined、null等特殊值的处理,建议在答题时主动提一句"如果需要兼容 NaN,可以用includes或者Map来解决",面试官会觉得你考虑问题很周到。
5.2 深拷贝:手写递归和边界处理
深拷贝是另一道常客题。最容易过的版本是用JSON.parse(JSON.stringify(obj)),但必须主动指出它的局限性:
- 不能处理
undefined、Symbol、Function,这些会被忽略或转成null - 不能处理循环引用,会直接报错
- 不能处理
Date、RegExp等特殊对象,会变成字符串或空对象
手写一个能处理基础边界情况的深拷贝,是 B 卷常见的加分项。
function deepClone(target, map = new Map()) { if (target === null || typeof target !== 'object') { return target } if (map.has(target)) { return map.get(target) } const result = Array.isArray(target) ? [] : {} map.set(target, result) if (target instanceof Date) { return new Date(target) } if (target instanceof RegExp) { return new RegExp(target.source, target.flags) } for (let key in target) { if (Object.prototype.hasOwnProperty.call(target, key)) { result[key] = deepClone(target[key], map) } } return result }几个易错点要单独提一下:
typeof target !== 'object'的判断不能漏,否则null会被误判为对象直接死循环。用Map来记录已克隆的对象,是为了解决循环引用,比如obj.self = obj这种结构,没有 Map 记录的话递归会无限栈溢出。Date和RegExp要单独处理,否则克隆出来的类型不对,instanceof检查会失败。用for...in时一定要配合hasOwnProperty,否则会把原型链上的可枚举属性也拷贝进去。
5.3 防抖与节流:概念、实现、场景三连问
防抖和节流在 B 卷里可能以手写题出现,也可能以简答题出现,核心是考察你对高频事件触发场景的理解。
防抖(debounce)的含义是:事件触发后等待一段时间再执行,如果期间再次触发,则重新计时。典型场景是搜索框输入联想,用户停止输入后才发请求。
function debounce(fn, delay = 300) { let timer = null return function (...args) { if (timer) { clearTimeout(timer) } timer = setTimeout(() => { fn.apply(this, args) }, delay) } }注意这里必须用fn.apply(this, args),而不是直接fn(args),因为需要保留原函数执行时的this指向。如果直接调用fn,里面的this会变成undefined(严格模式)或全局对象,导致事件处理函数拿不到当前元素。
节流(throttle)的含义是:事件触发后,在一段时间内只执行一次。典型场景是滚动事件、窗口 resize 事件。
function throttle(fn, interval = 300) { let last = 0 return function (...args) { const now = Date.now() if (now - last >= interval) { last = now fn.apply(this, args) } } }防抖和节流的区别是面试必问题:防抖是"只执行最后一次",节流是"定期执行,控制频率"。做一道选择题时,题目给一个场景问用防抖还是节流,你只需要抓住这个本质就能答对。
5.4 数组扁平化与手写 reduce
数组扁平化也是一道高频手写题,常见问法:[1, [2, [3, [4]]]]怎么转成[1, 2, 3, 4]。
最简洁的答案是:
const flat = (arr) => arr.flat(Infinity)但面试官通常希望看到手写递归版本:
function flatten(arr) { return arr.reduce((acc, cur) => { return acc.concat(Array.isArray(cur) ? flatten(cur) : cur) }, []) }递归版本的关键在于concat的妙用:如果当前元素是数组,就递归展开后再拼接;如果不是数组,直接拼进去。用push的方式写也可以,但会遇到"递归返回值应该 push 还是 concat"的细节问题,整体代码会更啰嗦。
如果题目要求"实现一个flat函数,支持指定深度",那么加一个depth参数:
function flat(arr, depth = 1) { return depth > 0 ? arr.reduce((acc, cur) => { return acc.concat(Array.isArray(cur) ? flat(cur, depth - 1) : cur) }, []) : arr.slice() }depth等于 0 时返回浅拷贝,这里用.slice()是为了不修改原数组。这个边界条件容易被忽略,但写上了会让代码更完整。
6. 前端工程化与开放题:从 Webpack 到性能优化
B卷最后一部分通常是开放题,考的是你对前端工程化和性能优化的整体认知。这类题没有标准答案,但回答要有层次、有依据。
6.1 Webpack 核心概念:一句话说清每个 part 的作用
Webpack 的题一般会问:entry、output、loader、plugin分别是什么,有什么区别。
entry是打包入口,Webpack 从入口文件开始,解析依赖关系,构建依赖图。output是输出配置,指定打包后文件的路径和文件名。loader是转换器,把非 JS 文件(如 CSS、图片、less、scss)转成 Webpack 能处理的模块。plugin是扩展插件,可以做打包优化、资源管理、环境变量注入等 loader 做不到的事。
关键的区分点是:loader 本质上是函数,处理单文件的转换;plugin 可以监听 Webpack 生命周期,在打包的各个阶段做事情,能力更大。举例说,babel-loader是把 ES6+ 语法转成 ES5,而HtmlWebpackPlugin是自动生成 HTML 并注入打包后的 JS 文件路径。
如果题目追问"为什么需要 loader",可以答:Webpack 本身只认识 JS 和 JSON 模块,但真实项目中还有 CSS、图片、字体、TypeScript、Vue 单文件等资源,loader 的作用就是把这些资源翻译成 Webpack 能识别的模块。
6.2 性能优化:渲染层面和网络层面分开答
性能优化是开放题的高频问法,题目通常会这样出:"项目首屏加载慢,你会从哪些方面优化?"
我建议把答案分成两个维度:资源加载层面和渲染执行层面。
资源加载层面:
- 图片懒加载和 CDN 加速:给图片加
loading="lazy",把静态资源放到 CDN - 开启 HTTP 缓存:静态资源设置长缓存 + 文件名加 hash 戳,避免缓存失效问题
- 代码分割:用
import()动态导入,把路由页面拆成独立 chunk,按需加载 - Gzip 压缩:服务端开启 gzip,文本类资源体积可以压缩 60% 到 80%
- 资源预加载:
<link rel="preload">预加载首屏关键资源
渲染执行层面:
- 减少重排重绘:合并 DOM 操作,用
DocumentFragment或者批量更新 - 避免长任务阻塞主线程:大的计算用 Web Worker 处理
- 减少不必要的请求:接口合并,或者用缓存管理工具(如 SWR、React Query)做数据缓存
- 首屏关键路径优化:内联关键 CSS,异步加载非关键 JS
回答时不要只是罗列,最好结合一个具体案例。我当时举的例子是:一个列表页每次滚动都会重新渲染整页数据,我把渲染方式改成了虚拟滚动,只渲染可视区域的数据,页面掉了 50% 的滚动卡顿。这类实战案例比单纯背诵优化点更有说服力。
6.3 项目相关的开放题:怎么讲清楚一个项目
B卷最后一道题往往会结合你在简历上写的项目来问,比如:"介绍一个你印象最深的项目,你在其中负责什么,遇到的最大难点是什么,怎么解决的?"
这类题考察的重点不只是技术,还有你对自己项目的思考深度。回答的时候用 STAR 法则组织:背景(Situation)、任务(Task)、行动(Action)、结果(Result)。
比较好的回答结构是:
- 项目背景:一句话说清楚项目是干什么的,面对什么用户
- 我的职责:具体负责哪几个模块
- 技术选型:为什么用这个框架、这个状态管理方案,有没有对比过其他方案
- 最大难点:选一个真实的问题,讲清现象 → 排查 → 定位 → 解决的完整链条
- 结果与反思:解决了什么问题,有没有可以做得更好的地方
注意,不要把项目讲成流水账。面试官想看到的是你发现问题、分析问题、解决问题的能力。比如你可以说:"当时项目列表页在低端机上卡顿,我用 Chrome Performance 面板录制了性能数据,发现主线程被长任务阻塞了 300ms,原因是渲染了每一条数据的所有字段。我改成虚拟滚动后,首屏时间从 1.8s 降到了 600ms。"
这种回答有数据、有工具、有结果,比"我做了个项目,用 Vue 写了几个页面"强太多。
7. 完整模拟答题与踩坑实录
最后一部分,我把整套 B 卷按"时间分配 + 答题节奏 + 踩坑清单"讲一遍,这部分是我最想分享的实操经验。
7.1 笔试时间分配建议
B卷的时间通常是 60 到 90 分钟。我建议按下面的节奏分配:
| 题型 | 建议时间 | 策略 |
|---|---|---|
| 选择题(10-15题) | 15分钟 | 不会的先跳过,不要在单题卡超过1分钟 |
| 简答题(3-5题) | 20分钟 | 每题答题控制在5分钟以内,写要点比堆字数重要 |
| 手写代码(2-3题) | 25分钟 | 优先做自己最有把握的,保证 AC 至少一题 |
| 开放题(1-2题) | 10分钟 | 按"背景-方案-结果"结构写,不需要写太细 |
| 检查 | 5分钟 | 重点检查选择题有没有看错选项,手写题有没有漏掉边界条件 |
这里有个很重要的心态问题:笔试不是要拿满分,而是要保证正确率优先。如果你手写题卡住了,果断放弃,先去做后面的开放题。因为开放题只要写了就能拿分,而手写题如果写不出完整代码,浪费半小时也还是 0 分。
我在实际笔试时遇到过一次这种情况:手写一道深拷贝的题,写到一半发现忘了处理循环引用,纠结了十分钟,结果后面一道简答题没时间写完整。后来学乖了,遇到卡壳直接跳到下一题,等最后有剩余时间再回来补。
7.2 我踩过的坑:五个高频丢分点
这里集中整理一下 B 卷答题时的常见丢分点,这些坑我当年踩过,后来帮学弟学妹改笔试题也反复看到:
丢分点一:this指向题没有考虑严格模式。题目如果没有明确说非严格模式,默认严格模式,独立函数调用的this是undefined而不是全局对象。比如function foo() { console.log(this) } foo()在严格模式下输出undefined。
丢分点二:数组去重没有考虑对象和特殊值。只写new Set(arr)虽然简洁,但如果题目数据里有NaN,或者明确要求"对象按id去重",需要给出扩展方案。答题时主动说明当前方法的局限性,能体现思维完整性。
丢分点三:手写防抖时忘了this绑定和参数透传。直接写setTimeout(fn, delay)而不是setTimeout(() => fn.apply(this, args), delay),导致函数在真正的调用环境下this丢失。这是非常常见的一个细节。
丢分点四:HTTP 缓存题混淆no-store和no-cache。题目问"哪个头表示完全不缓存",答案是no-store。no-cache是"可以缓存但要每次都去服务器验证",和"不缓存"是两码事。
丢分点五:手写题没有考虑边界条件。比如数组扁平化的空数组、深拷贝的null、Promise.all 的空数组。空数组传给Promise.all应该 resolve 一个空数组,Promise.race空数组会永远 pending,这几个边界条件讲清楚能明显加分。
7.3 一套上手可用的刷题清单
如果你看到这篇文章时还有两周以上的准备时间,推荐按下面的清单刷题,基本可以覆盖 B 卷百分之八十的考点:
JS 基础:
- LeetCode 上 tag 为 Array、String 的 Easy 题刷 20 道
- 手写:
debounce、throttle、deepClone、flatten、curry、new、call/apply/bind - 理解:
原型链、闭包、作用域、this 指向、事件循环
浏览器与网络:
- 理解:
输入 URL 到页面展示完整过程 - 理解:
HTTP 缓存机制(强缓存/协商缓存) - 理解:
TCP 三次握手四次挥手 - 理解:
DOM 树的构建和渲染阻塞
框架与工程化:
- 理解:
Vue 响应式原理(2.x 和 3.x 对比) - 理解:
虚拟 DOM 和 diff 算法 - 理解:
Webpack 核心概念(entry/output/loader/plugin)
刷题不是越多越好,而是要每个考点都能形成"定义 + 原理 + 场景 + 边界"的完整认知链。比如你理解了防抖,还要知道"搜索框输入"和"滚动事件"分别用防抖和节流的原因是什么,这样才能应对笔试和面试的双重考验。
7.4 最后再分享一个答题技巧
笔试是在电脑上答题,不像面试可以实时交流。所以答题时,我强烈建议你在手写代码题旁边用注释写明你的思路,或者在简答题里把解题步骤先列出来再展开。
原因有两点:第一,改卷人很可能不是面试官本人,而是 HR 或者技术团队的其他成员,他们需要根据你的答案推断你的水平,有思路注释的代码明显更容易被判断为"基础扎实";第二,如果你代码没写完整,思路注释能帮你拿到一部分过程分,总比空着强。
比如深拷贝那道题,你可以在代码开头写一句注释:
// 思路:递归遍历对象属性,使用 Map 记录已克隆对象以处理循环引用 // 边界:null、Date、RegExp、数组、函数需单独处理这不需要花多少时间,但传递出的信息量是:你不仅会写代码,而且写代码之前有结构化思考的习惯。这套思维模式,恰恰是校招笔试最想筛出来的东西。
说到底,浩鲸这套 B 卷没什么神题怪题,考的就是前端工程师最核心的底层能力:JS 基础是否扎实、异步模型是否清晰、浏览器原理是否理解、代码实现是否有细节意识。把这些基本功打磨好,不管去哪里笔试面试,都是稳的。