第十五天了。能坚持到这一步的人,基本已经告别了“背题式”准备,开始进入一种更危险的阶段:觉得自己什么都会,一开口又总觉得差点意思。我见过不少候选人,单拎某个知识点能聊十分钟,一旦把网络、浏览器、后端混在一起问,回答就开始变形。原因很简单,真实面试从来不会按章节出题,面试官问一个“页面加载慢”就能从 HTTP 摸到渲染、再从渲染摸到后端接口设计,中间没有任何过渡提示。
所以今天这批题,我不按单一技术栈出,而是把网络协议、浏览器机制、后端工程三类问题混在一起。你不是来记答案的,是来检验自己能不能在被打断、被追问、被反问的场景下,仍然把话说清楚。看完之后建议你先自己答一遍,再对照后面的拆解。
1. 第十五天,为什么我拿混合题来练你
前十四天如果都在做单一主题轰炸,第十五天就是模拟真实战场的日子。真实面试里没有人告诉你“下面考的是缓存”,你只能从问题里自己嗅出考点。这也是“拷打面试官”这个系列的核心逻辑:不是面试官单向考你,而是你也要有能力判断,对方问的到底是概念、是场景、还是系统设计。
混合题最大的价值,是逼你建立知识之间的连接。比如“强缓存和协商缓存”属于网络层,但问到最后一定会落到“前端发版为什么总是看到旧页面”,这时候就变成了浏览器和工程化问题。再比如“事件循环”属于 JavaScript 运行时,但问一问又可能拐到“为什么某些动画掉帧”,这就成了渲染性能问题。你如果只会孤立地背定义,一跨界就露馅。
我建议你拿到今天的题单后,先做一个小测试:给自己十五分钟,不看资料,把每个问题当成面试现场,用口述的方式回答。能说出“是什么”只算及格,能说出“在什么场景下用、不用会怎样、出了问题怎么排查”才是真正值钱的部分。接下来每道题我都按这个标准拆给你看。
1.1 今日题单结构:网络、浏览器、后端各占一份
今天一共拆九个高频问题,分三组。网络组解决的是“数据怎么传过来”,浏览器组解决的是“传过来之后怎么解释、怎么渲染”,后端组解决的是“服务端怎么稳定高效地把数据交出去”。这样拆完,你会发现一条完整链路:用户在浏览器输入网址,DNS 解析到 IP,TCP 连上服务器,HTTP 请求带回 HTML,浏览器解析渲染,期间遇到资源还要继续请求,而服务端这边则要考虑缓存、索引、幂等。每一个问题都是这条链路上最容易翻车的地方。
有人会问,为什么不放算法题放在今天?实话实说,算法题更适合单独花时间专项刷。今天这批题是知识密度高、容易被追问、且在很多面试轮次里都会被反复问到的“常青题”。这类题练好了,收益比闷头刷十道算法题更实在。
2. 网络与 HTTP:先解决这几个高频问法
网络部分的题,面试官有个共同习惯:先问一个简单概念,然后立刻给一个业务场景,看你能否把两个东西接上。比如下面这些。
2.1 HTTP 缓存到底是怎么生效的
这道题几乎是我见过的最高频面试题之一,但答得好的真不多。很多人能背出“强缓存、协商缓存”两个词,再问一句“ETag 和 Last-Modified 有什么区别”,就开始含糊了。
先给一个标准骨架。强缓存由Cache-Control控制,比如Cache-Control: max-age=3600,表示浏览器在接下来一小时内直接使用本地副本,不发请求。这里有个容易忽略的点:Cache-Control还能配合public、private、no-cache、no-store用。no-cache不是不缓存,而是每次使用前必须先向服务器验证,相当于强制走协商缓存;no-store才是真正不存。很多人把这两个概念混在一起,一听面试官说“我不想让浏览器缓存怎么办”,就脱口而出 no-cache,这是典型的失分点。
协商缓存则是另一个维度。浏览器带着If-None-Match或If-Modified-Since请求服务器,服务器比较资源是否变化,没变返回 304,变了返回 200 和新的内容。这里有张很直观的对照表:
| 类型 | 是否发请求 | 状态码 | 判断依据 | 典型场景 |
|---|---|---|---|---|
| 强缓存 | 不发请求 | 200(from disk/memory cache) | 是否在 max-age 有效期内 | 图片、CSS、JS 等静态资源 |
| 协商缓存 | 发请求 | 304 或 200 | ETag / Last-Modified 是否变化 | 需要快速感知变化的接口或页面 |
追问环节最常见的问题是:发版之后,为什么用户还是看到旧页面。答案不是缓存不好,而是资源 URL 没变化。所以工程上要做的是给文件名加哈希,比如app.abc123.js,文件内容变了哈希就变,浏览器把它当成一个新 URL,强缓存自然失效。这就是为什么现代构建工具都默认开启内容哈希命名的原因。你要是能把这一层答出来,面试官基本就不会继续在这个问题上纠缠你了。
2.2 从输入网址到显示页面,中间发生了什么
这题被问烂了,反而成了最容易看出水平的题。新手会背流水账:DNS 解析、TCP 连接、发送 HTTP、服务器返回、浏览器解析渲染。听起来没错,但细细追问,每一个环节都有坑。
先说 DNS。面试官可能会问:浏览器怎么知道该访问哪台服务器?你至少要说清楚,本地有 DNS 缓存、操作系统有 hosts 文件、再往上走是递归 DNS 服务器,而且这个过程本身也可能有缓存。有些面试官喜欢追问“DNS 用的是 TCP 还是 UDP”,答案是 UDP,但有些大响应会走 TCP。很多人听到这个问题就开始慌,其实不用紧张,把主链路说对比什么都重要。
然后是 TCP。三次握手不是只发生在浏览器和服务器之间,同一个页面里的多个资源请求,很可能复用了同一条连接,这就是 HTTP/1.1 的 keep-alive 和 HTTP/2 多路复用的意义。面试官如果追问“HTTP/2 有什么好处”,你提到“头部压缩、多路复用、服务端推送”基本就过关了。
再到服务器处理请求。这里不用展开后端代码,但要提到“可能经过反向代理、负载均衡、CDN 缓存”这一层,很多人把这一步完全漏掉,张口就是数据库,显得对系统整体架构没有概念。然后是浏览器拿到 HTML 之后,开始解析 DOM 树、CSSOM 树,合成渲染树,布局绘制。如果你还能补一句“JS 的解析和执行会阻塞 DOM 构建,所以 script 标签一般放在 body 底部或者用 defer”,这题基本稳了。
2.3 为什么我建议你学一下 TCP 三次握手
这不是为了让你背 SYN、ACK 那三个字段,而是为了让你理解“为什么网络通信要先建立连接”。面试官问三次握手,表面考协议,实际考的是你能否用通俗语言解释一个复杂问题。
三次握手的本质是,双方需要确认两件事:你的发送通道是通的,我的接收通道也是通的。第一次客户端发 SYN,告诉服务器“我要发起连接”;第二次服务器回 SYN+ACK,表示“收到你的请求,我也准备好了”;第三次客户端回 ACK,表示“我收到你的准备了,开始传数据”。有人问为什么不能只握两次手,最简单的回答是:如果只有两次,客户端可能发了一个已经过期的连接请求,服务器误以为这是新连接,白白浪费资源。第三次握手就是用来消除这种历史问题的。
回答的时候,别一上来就画状态图。先讲清楚目标,再说过程,最后补一句“如果网络很慢,握手的往返时间会成倍增加,这也是为什么高并发场景下要重视连接复用”。这个收尾会让面试官觉得你不仅有知识,还有场景意识。
3. 浏览器与 JavaScript:一天都在这里翻车
前端部分的题,最有意思的点在于:代码看起来简单,跑起来也可能简单,但一旦追问“为什么这个顺序输出”,很多人就翻车。今日三道题,正好覆盖运行时、交互事件、渲染流程三个层面。
3.1 Event Loop:把宏任务和微任务的真实执行顺序说清楚
这道题十个人有八个答过,但每次面试都能刷掉一半以上。原因很简单,能背结论的人多,能解释的人少。先给结论:执行完一个宏任务,会清空当前微任务队列;微任务队列清空之后,浏览器有机会进行渲染;然后才从宏任务队列里取下一个宏任务执行。微任务包括Promise.then、MutationObserver、process.nextTick;宏任务包括setTimeout、setInterval、I/O 事件、UI 渲染。
举个例子,下面这段代码,你先自己猜输出顺序:
console.log("start"); setTimeout(() => { console.log("timeout"); }, 0); Promise.resolve().then(() => { console.log("promise1"); }).then(() => { console.log("promise2"); }); console.log("end");答案是 start、end、promise1、promise2、timeout。很多人错在把 Promise 的回调当成同步任务,而实际上Promise.then是典型的微任务,会等当前宏任务执行完才运行。setTimeout 虽然延时是 0,但它属于宏任务,必须排队到下一轮。面试官如果继续追问“setTimeout 的延时真的准确吗”,答案是:不准确,因为如果微任务队列一直有任务,setTimeout 就只能一直等着,这也是为什么有时候你会感觉定时器“被延迟执行”了。
3.2 防抖和节流的区别与适用场景
这题最大的坑,不是让写代码,而是很多候选人写完代码之后,被问“它们到底解决了什么问题”又开始讲空话。一句话总结:防抖的核心是“只执行最后一次”,适合输入框搜索、窗口 resize 后计算布局;节流的核心理念是“固定频率内只执行一次”,适合滚动加载、按钮防重复点击。
给一个防抖函数范例,注意看代码里的细节:
function debounce(fn, delay = 300) { let timer = null; return function (...args) { const context = this; clearTimeout(timer); timer = setTimeout(() => { fn.apply(context, args); }, delay); }; }这里必须处理this和参数,否则在真正的 DOM 事件回调里会丢上下文。能补一句“返回的是闭包,每次调用都会重置定时器”,说明你理解闭包和定时器之间的关系。
节流另一种实现更简单:记录上一次执行时间,如果距离当前不足一个周期就直接 return。真正面试加分的是补充一句“防抖适合只关心最终结果的场景,节流适合关心过程进度的场景”。这个区分远比你背十行源码有说服力。
3.3 浏览器渲染中的重排和重绘
讲重排和重绘,喜欢用类比。重排就像你重新摆了一整屋子的家具,所有东西都要重新算位置;重绘只是给一面墙换了颜色,不影响其他家具的位置。所以重排的代价远比重绘高。
触发重排的典型操作包括:修改元素宽高、修改 padding/margin、修改字体大小、查询某些布局属性的时候,也会强制触发重排,比如读取offsetTop、offsetHeight、clientWidth。这是一个非常隐蔽的性能坑:如果在一段代码里反复读 offsetTop 又改样式,浏览器为了给你正确的值,只能每次同步计算布局,这被称为布局抖动。
面试官问“怎么优化”,你要答三个方向:第一,批量修改样式,尽量用修改 class 而不是逐条改 style;第二,把频繁操作的 DOM 元素用position: absolute或fixed移出文档流;第三,如果做动画,优先使用 transform 和 opacity 而不是修改 top、left,因为 transform 发生在合成层,不会触发布局和绘制。能答到最后一层,说明你真处理过性能问题。
4. 后端与分布式:会用和答得好是两回事
后端部分的题很吃经验。今天列的三道题,表面是基础知识,实际每个都对应一个线上事故场景。面试官不是想知道你背过多少,而是想知道,出问题时你会不会慌。
4.1 索引为什么能提速?哪些情况会失效
索引的问题我已经遇见过无数候选人卡在“失效”上面。先讲原理:一般使用 B+ 树结构,把索引列的值组织成有序结构,查询时可以快速定位,不需要一条条遍历全表。这个回答很清楚,但面试官更想听的是“失效场景”。
常见失效场景有这么几类。第一,对索引列做了计算或函数操作,比如WHERE YEAR(create_time) = 2024,这个写法会让索引无法使用,因为索引保存的是原始值,不是函数处理后的结果。第二,隐式类型转换,比如 varchar 列没加引号,MySQL 可能把字符串转成数字,索引失效。第三,前导模糊匹配,比如LIKE '%关键词',因为索引是从左往右匹配的,不知道开头是什么,自然用不上。第四,使用OR连接非索引列,也可能导致优化器放弃索引。
面试服务记得补一个实操案例:某个慢 SQL 接口本来要 8 秒,加了联合索引之后变成 30 毫秒。关键不是那 30 毫秒的数字,而是在加索引前,要先看查询条件、排序字段、分组字段,尽量把这三者覆盖到同一个联合索引里。如果面试官追问“联合索引的最左前缀原则”,你答“查询条件从最左列开始连续匹配才能命中索引”,基本到位了。
4.2 幂等性在支付和重试场景里是怎么落地的
幂等性这个名词听起来高深,其实解决的就是一个问题:同一个操作执行一次和执行多次,效果必须一致。最常见的业务背景是支付回调、消息重试、表单提交。比如用户下单后网络抖动,前端重试了一次,如果后端没有幂等保护,就会下出两笔订单。
落地方案至少有三种思路。第一,数据库唯一约束:给订单号或业务流水号建唯一索引,重复插入直接报冲突,这是最硬的手段。第二,状态机校验:订单状态只有从“待支付”到“已支付”是合法的,如果已经支付成功,再收到支付回调就直接忽略。第三,幂等表或 Redis 记录已处理的请求 ID,处理前先检查。
面试官爱追问“为什么不能只用 Redis 锁”,原因是 Redis 锁存在过期时间和异常丢失的问题,一旦锁失效,并发请求还是会打到数据库。所以最稳的方案往往是数据库兜底,Redis 只做提前拦截。你能说清“Redis 是性能优化,数据库唯一约束才是正确性保证”,这题就过关了。
4.3 为什么说缓存更新容易踩坑
缓存题很常见,但大多数人的回答只停留在“缓存穿透、击穿、雪崩”这三个名词上。下次试着换个角度:先讲清楚为什么更新缓存容易出问题。比如常见的“先更新数据库,再删除缓存”策略,好处是避免在并发情况下缓存里写入旧数据,坏处是如果删除缓存失败,下次请求还是会命中旧缓存。这也是为什么不少团队在删除缓存前会加一个短暂的时间,叫延迟双删。
再讲三种异常。穿透:查询一个不存在的数据,缓存和数据库都没有,导致大量请求直接翻到数据库。解决方法是缓存空值,或者用布隆过滤器做前置判断。击穿:某个热点 key 过期的那一瞬间,大量请求同时去数据库查询。解决方法是加互斥锁,或者让热点 key 的过期时间尽量分散。雪崩:大量 key 同时过期,数据库压力瞬间暴涨。解决方法是过期时间加随机值,避免集中失效。
回答最后要落到一句话:缓存只能降低数据库压力,不能保证绝对的强一致。能接受短暂不一致的业务才用缓存。这句话比背十个概念更能体现你对系统的理解。
5. 那些面试官自己也容易答岔的追问
我们一直说“拷打面试官”,但其实真正有效的拷打,不是刁难对方,而是在面试的互动环节里提出有质量的问题。很多候选人把面试结束前的“你还有什么问题吗”当成客套话,随便问一句“公司用什么技术栈”就结束了。太浪费了。
反问环节是你唯一能掌握主动权、用来判断这个团队是否值得去的机会。面试官问你“还有什么问题”,本质是想知道你对这次合作机会的兴趣程度和理解深度。如果你能问出和他刚才技术讨论相关的问题,说明你真的听进去了。
5.1 把问题还给面试官:三个不会踩雷的回问
第一个问题是:你们团队怎么评估一次上线是成功还是失败?这个问题能问出很多信息:是看指标,看崩溃率,看用户反馈,还是看领导满意度。面试官回答的速度和具体程度,直接反映团队工程化水平。
第二个问题:如果我来之后前三个月只能重点改进一个模块,你希望我优先改哪个?这个问题很锋利,它能问出团队当前最头疼的问题,也能判断面试官对你的预期是否实际。有些面试官会说“先了解业务,不着急改”,这没问题;如果面试官能脱口而出一个具体模块,说明团队对现状有清晰的认知。
第三个问题:当前系统里你觉得最不合理的依赖或者设计是什么?面试官如果认真回答这个问题,等于告诉你这个团队的技术痛点和可能存在的历史包袱,这对你判断 offer 选不选极有价值。当然,如果面试官打太极,也侧面说明团队可能对技术债务避而不谈,那么你进去之后可能会很累。
5.2 用结构化模板提问,避免临时想问题
不要临时想问题,而是在面试前就准备好一个提问模板。我的模板是:先问团队目标,再问个人职责,最后问技术架构。团队目标帮你判断部门定位,个人职责帮你判断进去了之后具体干什么,技术架构帮你判断这个团队有没有技术追求。三个问题下来,基本上四十分钟的面试里,你已经完成了双向评估。
这个思路放在“拷打面试官”系列里特别合适。因为你不再是那个被动坐着被审问的人,你是在收集信息、判断风险、决定是否要加入。能把这件事做明白的人,通常在谈判薪资时也更有底气。
6. 复盘:概念背得顺,分却不高,真相在这
今天最后一部分,我想聊聊一个很容易被忽视的现实:很多人题目都见过,答案也能说出来,但面试结果依然不理想。不是知识不够,而是答题方式出了问题。
第一个失分场景,是只背定义,不给场景。比如面试官问“什么是闭包”,你答“函数内部访问外部变量”,这没错,但如果补一句“闭包常用于封装私有变量,也用在防抖节流里保存 timer”,效果完全不同。面试官需要的不是教科书定义,而是你能证明你用过它。
第二个失分场景,是遇到不会的问题直接沉默。正确做法是先把你理解的部分说出来,哪怕不完整,再补一句“如果是这种情况,我可能先从日志入手排查”。面试官考察的很大一部分是你在不确定环境下的反应。答错不可怕,僵住才可怕。
第三个失分场景,是不知道收尾。回答完一个问题之后,加上一句“这就是我的理解,哪个地方需要我再补充吗”,既礼貌又能给对方追问空间。很多人把面试对话当成背书,答题完就干等下一题,互动感很差。
第十五天之后,建议你换一种练习方式:把手机录音打开,三分钟口述一道题,然后回听。你会发现大部分“我会”都是假象。口述时卡壳的地方,就是知识结构里的薄弱点。一次覆盖一个薄弱点,比反复背诵已经熟练的内容更有效。
根据我自己的经验,面试准备到了这个时候,拼的已经不是知识的量,而是表达的稳定度和对技术边界的感知能力。今天这十道题,你不求一次全对,但求每一道都能快速定位考点,理清逻辑,说得出场景。能做到这一步,你就有资格在面试快结束时,平静反问对方一句:“你们目前最希望这个岗位解决什么问题?”