字节跳动前端一面校招:大模型缓存与CSS优先级
- 轮次
- 一面
- 结果
- 二面已挂
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 自我介绍 + 实习拷打
- 一些大模型有缓存机制,说说这些缓存的原理?
- 比如 DeepSeek 的上下文缓存,什么情况下算是缓存?
- Function Calling 的原理是什么?
- 在 Function Calling 中,大模型本身会执行这个方法吗?
- MCP 是什么?它和 Function Calling 有什么关系?
- 说说你对 Flex 布局的理解,常用的 Flex 属性有哪些?
- 如何实现一个 DIV 上下左右都居中?
- 说说 CSS 优先级的计算规则。
- 如果有多个 class 作用于同一个元素,哪一个样式会生效?比如它们都设置了
color,最终会使用哪个颜色? - 如果这些样式都在同一个 CSS 文件中,而且存在多个 class 组合、嵌套选择的情况,都作用于同一个 DIV,怎样判断哪个样式生效?
- 假设外层元素的 class 是
parent,内层元素的 class 是children,文本在内层元素中。前面写了.parent .children { color: red; },后面写了.children { color: blue; },最终哪个颜色生效? - 你了解 JavaScript 的原型和原型链吗?
- JavaScript 的 class 如何实现继承?
- 具体来说,怎样通过原型实现继承?
- 说说 JavaScript 的事件循环。
- 宏任务和微任务分别有哪些?
- Cookie 和 localStorage 有什么区别?
- Cookie 和 localStorage 分别适用于哪些场景?
- 为什么 Cookie 适合存储用户身份信息?
- 每次请求都会携带 Cookie,是吗?
- 请求携带 Cookie 有哪些限制?访问所有网站都会发送吗?什么情况下不会携带?
- 你平时使用 Vue 比较多,是吗?Vue 里面的 HTML 能直接渲染到页面上吗?
- 能具体说说,Vue 中的 HTML 是怎样渲染到页面上的吗?
- HTML 是怎样变成 DOM 树的?
- 它是直接生成 DOM 树的吗?
- Vue 的
template不是页面可以直接使用的 HTML,需要经过什么转换?它先被转换成什么? - 第一次转换得到的并不是浏览器的 DOM,那它先转成什么?是不是先转成 JavaScript?
- 浏览器收到的是 HTML,还是 JavaScript?
- 你了解声明式编程吗?
- 能结合具体代码举例,说明哪些属于声明式编程,哪些属于命令式编程吗?
- 手撕:发布订阅模式
《参考解析》
大模型上下文缓存的原理:本质是复用 Transformer 推理时的 KV Cache。自回归生成时,每个 token 的注意力需要历史所有 token 的 K/V 向量,如果每次都重算,复杂度随上下文长度平方增长。缓存的做法是把已经算好的每层 K/V 张量按 token 位置存下来,下次遇到相同前缀时直接命中,只对新增 token 做前向计算。工程上有两个层次:单次会话内的 KV Cache(同一次请求里逐 token 生成时复用),以及跨请求的前缀缓存(把系统提示词、固定文档、历史多轮对话这类公共前缀的 KV 常驻显存,多个请求共享)。判定「算不算缓存」的关键是前缀逐 token 完全一致——从第一个 token 开始严格匹配,一旦中间有一个 token 不同,后面的缓存全部失效,所以只改末尾的提问能命中,改了系统提示词开头就全废。DeepSeek 的上下文缓存就是按前缀匹配计费的:命中缓存的输入 token 价格远低于未命中部分,因此实践中要把稳定内容(system prompt、few-shot 示例、知识库片段)放在前面,把变化的用户输入放在最后。缓存淘汰通常按最近最少使用(LRU)和显存压力决定,也有按前缀树组织以便部分命中的实现。另外要区分「语义缓存」(把 query 向量化后找相似问题直接返回旧答案,命中靠相似度阈值)和「前缀缓存」,两者不是一回事,回答时点出来是加分项。
Function Calling 原理,模型会执行方法吗:不会。模型只负责「决定调用哪个函数、参数是什么」,输出的是结构化的调用意图(tool_calls,包含函数名和 JSON 参数),实际的函数执行完全由宿主程序(后端服务或 Agent 框架)完成。完整链路是:请求里带上工具的 JSON Schema 描述(名称、用途、参数类型与必填项)→ 模型判断是否需要调用工具,需要就把 finish_reason 置为 tool_calls 并返回参数 → 宿主解析参数、做校验和鉴权、真正执行本地函数(查库、调 API)→ 把执行结果作为一条 role: "tool" 的消息追加进对话历史,再次请求模型 → 模型基于工具结果生成最终自然语言回答。理解这一点很重要,因为它决定了安全边界:权限、参数白名单、超时和幂等都必须由宿主保证,不能指望模型自我约束;同时工具描述的措辞质量直接决定模型调用准确率。
MCP 与 Function Calling 的关系:MCP(Model Context Protocol)是 Anthropic 提出的开放协议,用统一的方式把「模型能用的外部能力」暴露出来。它和 Function Calling 不是替代关系而是分层关系:Function Calling 是模型 API 层面的能力,规定模型怎么表达调用意图;MCP 是集成层面的标准,规定工具/资源/提示词怎么被描述、发现和调用。MCP 定义了 client(宿主,比如 IDE 或 Agent 运行时)和 server(能力提供方,比如数据库、文件系统、内部平台)之间的 JSON-RPC 通道,server 侧暴露三类东西:tools(可执行动作)、resources(可读取的数据)、prompts(预置提示模板)。宿主启动时通过 tools/list 拉取所有 server 的工具清单,把它们转换成自己模型的 Function Calling 定义,然后按统一流程调用并回填结果。它解决的问题是 N×M 的适配爆炸:以前每个宿主都要为每个外部系统写一遍工具封装,现在外部系统只要实现一次 MCP server,所有支持 MCP 的宿主都能直接用。
Flex 布局与居中:Flex 是一维布局模型,作用在容器上的核心属性有 display: flex、flex-direction(主轴方向)、flex-wrap、justify-content(主轴对齐)、align-items(交叉轴对齐)、align-content(多行时的行间对齐)、gap;作用在子项上的有 flex-grow/flex-shrink/flex-basis(简写 flex,常用 flex: 1 表示 1 1 0%)、align-self、order。要能顺带说清两个高频坑:flex: 1 与 flex: auto 的区别在基准值是 0 还是内容宽度;以及子项内容超长导致溢出时,需要在子项上设 min-width: 0(或 overflow: hidden)才能让 flex-shrink 生效——这是横向滚动条最常见的原因。居中实现至少给三种:display: flex; justify-content: center; align-items: center;绝对定位 + inset: 0; margin: auto;绝对定位 + left: 50%; top: 50%; transform: translate(-50%, -50%)(不需要知道宽高)。再加一句 display: grid; place-items: center 是最简写法。
CSS 优先级计算:把它拆成三元组 (a, b, c) 比较,不是简单的十进制相加。a 是 ID 选择器数量,b 是 class、属性选择器、伪类数量,c 是元素类型和伪元素数量;通配符 *、组合器对优先级没有贡献;行内样式单独一档(高于任何选择器,低于 !important);!important 再高一档;继承来的样式优先级最低。比较时从左到右逐位比,前面大的直接胜出,所以 .a.b.c.d.e.f.g.h.i.j.k 也压不过一个 #id。经典追问 .parent .children { color: red } 与 .children { color: blue }:前者有两个 class,三元组是 (0,2,0),后者是 (0,1,0),所以红色生效——和后写的顺序无关,优先说了算。只有优先级完全相等时,才看源码顺序,后定义的胜出(还要考虑 @layer 层叠层和同为 !important 时的层序反转)。另外不同来源的顺序是:用户代理样式 < 用户样式 < 作者样式 < 作者 !important < 用户 !important;all: unset 和 Shadow DOM 里的样式隔离是另外两个常见追问方向。
原型与原型链、class 继承:每个函数都有 prototype 属性指向原型对象,每个对象都有内部 [[Prototype]](可通过 Object.getPrototypeOf 或 __proto__ 访问)。访问属性时先查自身,找不到就沿 __proto__ 往上找,直到 Object.prototype(其 __proto__ 为 null),这条链就是原型链。用原型实现继承的标准三步:Child.prototype = Object.create(Parent.prototype)、Child.prototype.constructor = Child、在子类构造函数里调用 Parent.call(this, ...) 继承实例属性。ES6 的 class 是这套机制的语法糖:extends 会自动设置原型链并修正 constructor,super() 等价于调用父类构造函数(必须先于 this 使用),super.method() 沿 [[HomeObject]] 找父类方法。两者有一个实质差异:class 声明不会被提升(存在暂时性死区)、内部代码默认严格模式、且必须用 new 调用。
事件循环与宏微任务:JS 是单线程的,靠事件循环协调。一次循环(tick)的顺序是:执行完当前同步代码 → 清空微任务队列(清空过程中新产生的微任务也在本轮一起做完)→ 取一个宏任务执行 → 再清空微任务 → 必要时进入渲染(样式计算、布局、绘制)。宏任务包括 setTimeout/setInterval、setImmediate(Node)、I/O 回调、UI 事件回调、MessageChannel;微任务包括 Promise.then/catch/finally、queueMicrotask、MutationObserver、Node 里的 process.nextTick(优先级比 Promise 还高)。常考的坑:await 之后的代码相当于包在 Promise.then 里,属于微任务;因此一串 Promise.resolve().then 会插在所有 setTimeout 之前执行完;微任务里无限递归(比如不断 Promise.resolve().then)会饿死渲染和宏任务。
Cookie 与 localStorage、携带规则:区别按维度列:容量(Cookie 约 4KB,localStorage 通常 5~10MB)、生命周期(Cookie 可设 Expires/Max-Age,localStorage 永久直到显式删除,sessionStorage 关标签页即清)、是否随请求自动发送(Cookie 会,localStorage 不会)、作用域(Cookie 有 domain/path,localStorage 按源同源隔离)、API(Cookie 要自己拼字符串,localStorage 是同步的键值 API)、以及安全属性(Cookie 支持 HttpOnly/Secure/SameSite,localStorage 完全可被 JS 读取,所以怕 XSS)。所以身份凭证用 Cookie:可以设 HttpOnly 防 XSS 窃取、Secure 只走 HTTPS、SameSite=Lax/Strict 防 CSRF,服务端无状态校验也天然适配。localStorage 适合存不敏感的前端状态:主题、草稿、列表缓存、埋点队列。
Cookie 的携带规则要答准:不是每次请求都带,也不是访问所有网站都带。浏览器按「域 + 路径 + 协议 + 端口」匹配,只有请求目标与 Cookie 的 domain/path 匹配且未过期才会带上;跨站请求默认受 SameSite 约束——SameSite=Lax 时只有顶级导航的 GET 才带,Strict 时跨站一律不带,None 必须同时设 Secure;跨域请求要带 Cookie 必须设置 withCredentials(fetch 的 credentials: 'include'),且服务端 Access-Control-Allow-Origin 不能是通配符、必须允许凭证;Secure 的 Cookie 在 HTTP 请求下不会发送;另外请求第三方域的图片、脚本等子资源时会带上该第三方域的 Cookie,这正是第三方 Cookie 被用于跨站追踪的原因,也是现在浏览器默认限制它的原因;document.cookie 也读不到 HttpOnly 的 Cookie。
Vue 模板到页面的完整链路:Vue 的 template 不是浏览器能直接渲染的 HTML,它要先经过编译。链路是:template 字符串 → 编译阶段用编译器解析成 AST(抽象语法树,纯 JS 对象,描述节点类型、属性、指令、插值)→ 做静态提升、补丁标记(PatchFlag)等优化 → 生成 render 函数(也就是 JS 代码,产物是 h()/createVNode() 调用)→ 运行时执行 render 函数得到 虚拟 DOM(VNode 树) → 与上一次的 VNode 树做 diff(双端比较或 Vue 3 的编译期优化 + 动态子节点对比)→ 只把差异部分映射成真实 DOM 操作(createElement/setAttribute/textContent)并挂载到页面。所以对「浏览器收到的是 HTML 还是 JavaScript」的回答是:以 SPA 为例,浏览器拿到的是几乎空壳的 HTML 加 JS bundle,DOM 是 JS 执行后动态构建的;只有在 SSR/SSG 场景下浏览器收到的 HTML 里才带着渲染好的 DOM 结构,客户端再 hydration 接管。同时要能讲清浏览器自己那条路:HTML 字节流 → 词法/语法分析(tokenizer → tree construction)边下载边解析 → 生成 DOM 树,遇 <script> 阻塞解析、遇 <link rel=stylesheet> 阻塞渲染,CSS 解析出 CSSOM 后与 DOM 合成渲染树 → 布局 → 绘制 → 合成。Vue 的 VNode 与浏览器 DOM 是两套结构,中间靠 patch 桥接,这就是「它不是直接生成 DOM 树」的准确含义。
声明式与命令式:命令式是「怎么做」——逐步给出操作步骤,自己管理状态和 DOM;声明式是「要什么」——描述目标状态,由框架决定怎么达到。例子:命令式 const el = document.createElement('div'); el.textContent = 'hi'; el.className = 'card'; document.body.appendChild(el);,声明式 <div class="card">hi</div>(或 React 的 createElement('div', {className:'card'}, 'hi'));命令式改文案要 el.textContent = 'bye',声明式只改数据、由 diff 更新。自定义数组过滤也一样:命令式 for 循环 push 到新数组,声明式 arr.filter(x => x > 0)。要在结尾给出取舍:声明式可读性和可维护性更好、把复杂度交给运行时,命令式对性能和副作用的控制更直接,所以动画、Canvas、WebGL 这类场景反而更适合命令式;SQL、K8s YAML、Terraform 也都是声明式的典型代表。
手撕发布订阅:核心是维护事件名到回调数组的映射,提供 on/once/off/emit,emit 时要复制一份数组再遍历(防止回调内部调用 off 导致遍历错位),并对单个回调的异常做隔离。
class EventBus {
constructor() { this.events = new Map(); }
on(type, fn) {
if (!this.events.has(type)) this.events.set(type, []);
this.events.get(type).push(fn);
return () => this.off(type, fn); // 返回取消订阅函数,便于清理
}
once(type, fn) {
const wrapper = (...args) => { this.off(type, wrapper); fn(...args); };
return this.on(type, wrapper);
}
off(type, fn) {
const list = this.events.get(type);
if (!list) return;
if (!fn) return void this.events.delete(type);
const i = list.indexOf(fn);
if (i > -1) list.splice(i, 1);
}
emit(type, ...args) {
const list = this.events.get(type);
if (!list) return;
[...list].forEach(fn => {
try { fn(...args); } catch (e) { console.error(e); }
});
}
}
写的时候主动说出来:为什么 emit 要浅拷贝(once 会在回调里改数组)、为什么 once 用包装函数而不是给回调挂标记、要不要支持通配符和 emit 返回值;如果要求支持最大监听数或异步串行执行,再补 Promise.all 版本。这一题面试官主要看边界意识,不是看 API 数量。