浩鲸科技前端校招笔试B卷解析:聚焦JS底层与浏览器机制
先声明一下,这篇东西是我以一个参加过浩鲸科技校招前端笔试的求职者视角,结合前后几届同学的面经和自己在实际开发中的验证,把整套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-age3600是相对时间,而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,证明客户端的发送能力没问题;第二次握手服务器回复 SYNACK,证明服务器的接收和发送能力都没问题;第三次握手客户端回复 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 indexOfconst 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 加速:给图片加loadinglazy,把静态资源放到 CDN开启 HTTP 缓存:静态资源设置长缓存 文件名加 hash 戳,避免缓存失效问题代码分割:用import()动态导入,把路由页面拆成独立 chunk,按需加载Gzip 压缩:服务端开启 gzip,文本类资源体积可以压缩 60% 到 80%资源预加载:link relpreload预加载首屏关键资源渲染执行层面:减少重排重绘:合并 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 基础是否扎实、异步模型是否清晰、浏览器原理是否理解、代码实现是否有细节意识。把这些基本功打磨好,不管去哪里笔试面试,都是稳的。