前言
大家好,我是木斯佳。
相信很多人都感受到了,在AI浪潮的席卷之下,前端领域的门槛在变高,纯粹的"增删改查"岗位正在肉眼可见地减少。曾经热闹非凡的面经分享,如今也沉寂了许多。但我们都知道,市场的潮水退去,留下的才是真正在踏实准备、努力沉淀的人。学习的需求,从未消失,只是变得更加务实和深入。
这个专栏的初衷很简单:拒绝过时的、流水线式的PDF引流贴,专注于收集和整理当下最新、最真实的前端面试资料。我会在每一份面经和八股文的基础上,尝试从面试官的角度去拆解问题背后的逻辑,而不仅仅是提供一份静态的背诵答案。无论你是校招还是社招,目标是中大厂还是新兴团队,只要是真实发生、有价值的面试经历,我都会在这个专栏里为你沉淀下来。

温馨提示:市面上的面经鱼龙混杂,甄别真伪、把握时效,是我们对抗内卷最有效的武器。
面经原文内容(二面算法 + 三面节选)
📍面试公司:腾讯
🕐面试时间:二面2月11日,三面3月4日
💻面试岗位:前端暑期提前批
❓面试问题(节选):
二面(算法部分)
- LRU缓存
- 大数相加,自己去写一些测试用例并验证
- 思考题:现在有假设一栋楼有100层,你有两个玻璃球,有些楼层扔下去球会碎,有些不会碎,你需要利用这两个球,找到那个临界楼层,最优的解法是什么
三面(节选)
-
有没有考虑过计费或者说成本,或者说对于服务端的压力,比如说CDN的(针对实习亮点)
-
对于国际化开发和国内开发的区别,有没有什么心得感受
-
多语言工具用的是什么,原理是什么
-
怎么判断用户当前应该使用的是什么语言
-
对于跨端架构的几种方案,如何进行选择(h5,native等)
-
现在在跨端架构中如果使用webview加载离线包的方案,如果在端内点击一个下载按钮,整个调用链路和过程是怎么样的
-
端侧的方法是如何注入到web中的
-
langchain.js框架解决了什么事情
来源:牛客网 喜喜玺玺
📝 腾讯前端暑期提前批·二面算法与三面节选深度解析
🎯 面试整体画像
| 维度 | 特征 |
|---|---|
| 面试风格 | 二面:算法实战型 + 思维拓展型 三面:成本意识型 + 跨端深入型 + AI工程型 |
| 难度评级 | ⭐⭐⭐⭐(四到五星,覆盖面广且有深度) |
| 考察重心 | 数据结构、大数处理、逻辑思维、成本意识、国际化、跨端通信、AI框架 |
木木有话说:这场面试比较有价值,我分成了上下两篇去讲,二三面有一半是根据个人项目去提问的,这里我没有整理,大家可以去原文看看题型进行准备。虽然环境不行,但是坚持总有回报,感谢原UP的分享,也恭喜他成功获得offer。
🔍 逐题深度解析
二面·算法1:LRU缓存
问题:实现LRU缓存
javascript
// LRU = Least Recently Used,最近最少使用
// 1. 使用Map实现(利用Map的插入顺序)
class LRUCache {
constructor(capacity) {
this.capacity = capacity
this.cache = new Map()
}
get(key) {
if (!this.cache.has(key)) return -1
// 更新访问顺序:先删除再添加
const value = this.cache.get(key)
this.cache.delete(key)
this.cache.set(key, value)
return value
}
put(key, value) {
if (this.cache.has(key)) {
// 已存在,删除旧的
this.cache.delete(key)
} else if (this.cache.size >= this.capacity) {
// 缓存已满,删除最久未使用的(Map的第一个)
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey)
}
// 添加新项
this.cache.set(key, value)
}
}
// 2. 使用双向链表 + Map(面试手写推荐)
class ListNode {
constructor(key, value) {
this.key = key
this.value = value
this.prev = null
this.next = null
}
}
class LRUCache {
constructor(capacity) {
this.capacity = capacity
this.cache = new Map() // key -> node
this.head = new ListNode()
this.tail = new ListNode()
this.head.next = this.tail
this.tail.prev = this.head
}
// 移动到头部(最近使用)
moveToHead(node) {
this.removeNode(node)
this.addToHead(node)
}
addToHead(node) {
node.prev = this.head
node.next = this.head.next
this.head.next.prev = node
this.head.next = node
}
removeNode(node) {
node.prev.next = node.next
node.next.prev = node.prev
}
removeTail() {
const node = this.tail.prev
this.removeNode(node)
return node
}
get(key) {
if (!this.cache.has(key)) return -1
const node = this.cache.get(key)
this.moveToHead(node)
return node.value
}
put(key, value) {
if (this.cache.has(key)) {
const node = this.cache.get(key)
node.value = value
this.moveToHead(node)
} else {
const node = new ListNode(key, value)
this.cache.set(key, node)
this.addToHead(node)
if (this.cache.size > this.capacity) {
const tail = this.removeTail()
this.cache.delete(tail.key)
}
}
}
}
// 3. 测试
const cache = new LRUCache(2)
cache.put(1, 1)
cache.put(2, 2)
console.log(cache.get(1)) // 1
cache.put(3, 3) // 删除key 2
console.log(cache.get(2)) // -1
二面·算法2:大数相加
问题:大数相加,自己写测试用例并验证
javascript
// 大数相加:处理超过Number.MAX_SAFE_INTEGER的整数
// 1. 字符串相加(从后往前)
function addStrings(num1, num2) {
let i = num1.length - 1
let j = num2.length - 1
let carry = 0
let result = []
while (i >= 0 || j >= 0 || carry > 0) {
const digit1 = i >= 0 ? Number(num1[i]) : 0
const digit2 = j >= 0 ? Number(num2[j]) : 0
const sum = digit1 + digit2 + carry
result.push(sum % 10)
carry = Math.floor(sum / 10)
i--
j--
}
return result.reverse().join('')
}
// 2. 测试用例
function testAddStrings() {
const cases = [
{ num1: '123', num2: '456', expected: '579' },
{ num1: '999', num2: '1', expected: '1000' },
{ num1: '0', num2: '0', expected: '0' },
{ num1: '12345678901234567890', num2: '98765432109876543210', expected: '111111111011111111100' },
{ num1: '9', num2: '9', expected: '18' },
{ num1: '100000000000000000000', num2: '1', expected: '100000000000000000001' }
]
cases.forEach(({ num1, num2, expected }, index) => {
const result = addStrings(num1, num2)
const passed = result === expected
console.log(`测试用例 ${index + 1}: ${passed ? '✅' : '❌'}`)
console.log(` 输入: ${num1} + ${num2}`)
console.log(` 期望: ${expected}`)
console.log(` 实际: ${result}`)
console.log('---')
})
}
testAddStrings()
// 3. 扩展:处理小数
function addBigNumbers(num1, num2) {
// 分离整数和小数部分
const [int1, frac1 = ''] = num1.split('.')
const [int2, frac2 = ''] = num2.split('.')
// 补齐小数位
const maxFracLen = Math.max(frac1.length, frac2.length)
const paddedFrac1 = frac1.padEnd(maxFracLen, '0')
const paddedFrac2 = frac2.padEnd(maxFracLen, '0')
// 整数和小数分别相加
const intSum = addStrings(int1 || '0', int2 || '0')
const fracSum = addStrings(paddedFrac1, paddedFrac2)
// 处理小数进位
let carry = 0
let fracResult = fracSum
if (fracSum.length > maxFracLen) {
carry = 1
fracResult = fracSum.slice(1)
}
// 整数部分加上进位
const finalInt = addStrings(intSum, carry.toString())
return fracResult ? `${finalInt}.${fracResult}` : finalInt
}
二面·思考题:两个玻璃球找临界楼层
问题:100层楼,两个玻璃球,找临界楼层(球会碎的楼层)
javascript
// 问题分析
// 有100层楼,从某个楼层F开始往下扔球会碎,F以下不会碎
// 有两个相同的球,找到F的最小尝试次数
// 1. 错误解法:二分法
// 第一个球在50层扔,如果碎了,第二个球要从1层开始逐层试(最多50次)
// 最坏情况:50次
// 2. 最优解法:等间隔法
// 思路:让第一次尝试的间隔逐渐减小,保证最坏情况下的尝试次数最小
// 设第一次尝试的间隔为x,如果碎了,第二个球需要试x-1次
// 如果没碎,下一次尝试间隔x-1,以此类推
// 总次数 = x + (x-1) + (x-2) + ... + 1 >= 100
// 解方程:x(x+1)/2 >= 100 => x >= 14
// 3. 具体策略
// - 第一个球在14层扔
// - 如果碎了,第二个球从1层开始试到13层(最多13次)
// - 如果没碎,下一个在27层(14+13)扔
// - 如果碎了,第二个球从15层试到26层(最多12次)
// - 以此类推:14, 27, 39, 50, 60, 69, 77, 84, 90, 95, 99, 100
// 4. 最坏情况
// 最坏需要14次
// 5. 代码实现
function findCriticalFloor(totalFloors = 100) {
// 计算初始间隔
let x = Math.ceil((Math.sqrt(8 * totalFloors + 1) - 1) / 2)
let step = x
let prevFloor = 0
let attempts = 0
console.log(`尝试策略:间隔递减法,初始间隔 ${x}`)
// 第一个球
while (step > 0) {
const floor = prevFloor + step
attempts++
console.log(`第${attempts}次尝试:在${floor}层扔第一个球`)
// 模拟:假设临界楼层是某个值
// 这里只是演示算法,实际不会知道是否碎了
prevFloor = floor
step--
}
return { maxAttempts: x, strategy: '间隔递减法' }
}
findCriticalFloor()
三面·问题3:成本与服务端压力
问题:有没有考虑过计费或者说成本,或者说对于服务端的压力,比如说CDN的
javascript
// 1. 成本考虑维度
// 1.1 CDN成本
// - 流量费用:按GB计费
// - 请求次数:按万次计费
// - 边缘节点存储费用
// 1.2 优化策略
// - 缓存命中率优化
const cacheHitRate = (cacheHits / totalRequests) * 100
// 目标:提升到95%以上
// - 压缩减少流量
// Gzip/Brotli压缩,减少70%体积
// - 图片格式优化
// WebP比JPEG小30%,AVIF更小
// - 资源合并减少请求
// 合并CSS/JS,减少请求数
// 2. 服务端压力
// 2.1 指标监控
// - QPS(每秒查询数)
// - 响应时间
// - CPU/内存使用率
// - 数据库连接数
// 2.2 优化方案
// - 缓存策略
// - 数据库索引优化
// - 异步处理
// - 服务端渲染缓存
// 3. 具体案例
// 优化前:CDN月费用 5000元,源站QPS峰值 2000
// 优化后:CDN月费用 3000元(-40%),源站QPS峰值 800(-60%)
三面·问题4-6:国际化开发
问题:国际化开发区别、多语言工具、语言判断
javascript
// 1. 国际化 vs 国内开发区别
// 1.1 技术层面
// - 多语言支持:i18n
// - 双向文本:RTL布局
// - 日期/时间格式:UTC转换
// - 货币格式:汇率处理
// - 字体支持:不同字符集
// 1.2 业务层面
// - 合规要求:GDPR等
// - 支付方式:多样化
// - 本地化运营
// 2. 多语言工具
// 2.1 i18next(最常用)
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
i18n.use(initReactI18next).init({
resources: {
en: { translation: { hello: 'Hello' } },
zh: { translation: { hello: '你好' } }
},
lng: 'zh',
fallbackLng: 'en'
})
// 2.2 原理
// - 根据key加载对应语言的JSON文件
// - 运行时替换占位符
// - 支持复数、变量插值
// 3. 判断用户语言
// 3.1 浏览器语言
const browserLang = navigator.language || navigator.userLanguage
// 'zh-CN', 'en-US'
// 3.2 优先顺序
function getUserLanguage() {
// 1. 用户设置
const savedLang = localStorage.getItem('language')
if (savedLang) return savedLang
// 2. 浏览器语言
const browserLang = navigator.language.split('-')[0]
if (['zh', 'en', 'ja'].includes(browserLang)) {
return browserLang
}
// 3. 默认
return 'en'
}
// 3.3 服务端判断
// 通过Accept-Language头
app.get('/', (req, res) => {
const acceptLang = req.headers['accept-language']
// 'zh-CN,zh;q=0.9,en;q=0.8'
const preferredLang = parseAcceptLanguage(acceptLang)
})
三面·问题8-10:跨端架构
问题:跨端方案选择、离线包下载链路、端方法注入
javascript
// 1. 跨端方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|------|------|------|----------|
| H5 | 开发快、更新灵活 | 性能一般、依赖网络 | 营销页、轻应用 |
| 离线包H5 | 速度快、离线可用 | 包大小限制 | 核心功能H5 |
| React Native | 性能接近原生 | 调试复杂 | 复杂交互应用 |
| Flutter | 性能好、跨端一致 | 包体积大 | 高性能要求 |
| 小程序 | 生态好 | 厂商限制 | 微信生态 |
| 原生 | 性能最好 | 成本高 | 核心体验 |
// 2. 离线包下载链路
// 点击下载按钮后的完整流程
2.1 端内点击下载按钮
2.2 JS调用端API(通过JSBridge)
2.3 端侧发起下载请求
2.4 下载管理器创建任务
2.5 请求CDN获取离线包
2.6 校验包完整性
2.7 解压到本地
2.8 更新离线包索引
2.9 通知JS下载完成
2.10 跳转到本地页面
// 3. 端方法注入(JSBridge实现)
// 3.1 安卓注入
// 原生代码
webView.addJavascriptInterface(new Object() {
@JavascriptInterface
public void download(String url) {
// 执行下载
}
}, "NativeBridge")
// Web端调用
window.NativeBridge.download('https://example.com/file.zip')
// 3.2 iOS注入
// WKWebView配置
WKUserContentController *controller = [[WKUserContentController alloc] init];
[controller addScriptMessageHandler:self name:@"nativeBridge"];
// Web端调用
window.webkit.messageHandlers.nativeBridge.postMessage({
action: 'download',
url: 'https://example.com/file.zip'
})
// 3.3 JSBridge封装
class JSBridge {
constructor() {
this.callbacks = {}
this.callId = 0
this.init()
}
init() {
// 注册全局回调
window._handleNativeCallback = (callId, data) => {
const callback = this.callbacks[callId]
if (callback) {
callback(data)
delete this.callbacks[callId]
}
}
}
callNative(action, params, callback) {
const callId = this.callId++
this.callbacks[callId] = callback
const message = { callId, action, params }
if (window.NativeBridge) {
// 安卓
window.NativeBridge.postMessage(JSON.stringify(message))
} else if (window.webkit) {
// iOS
window.webkit.messageHandlers.nativeBridge.postMessage(message)
}
}
}
// 使用
const bridge = new JSBridge()
bridge.callNative('download', { url: 'file.zip' }, (result) => {
console.log('下载完成', result)
})
三面·问题14:langchain.js
问题:langchain.js框架解决了什么事情
javascript
// 1. LangChain是什么?
// 一个用于开发LLM应用的框架,提供模块化组件
// 2. 核心能力
// 2.1 链式调用
import { Chain } from 'langchain/chains'
import { OpenAI } from 'langchain/llms'
const model = new OpenAI({ temperature: 0 })
const chain = new Chain({
llm: model,
prompt: '请翻译成英文: {text}'
})
const result = await chain.call({ text: '你好' })
// 2.2 检索增强生成(RAG)
import { RetrievalQAChain } from 'langchain/chains'
import { HNSWLib } from 'langchain/vectorstores'
import { OpenAIEmbeddings } from 'langchain/embeddings'
// 创建向量存储
const vectorStore = await HNSWLib.fromDocuments(
documents,
new OpenAIEmbeddings()
)
// 创建RAG链
const chain = RetrievalQAChain.fromLLM(
model,
vectorStore.asRetriever()
)
// 2.3 工具调用
import { initializeAgentExecutor } from 'langchain/agents'
import { Calculator } from 'langchain/tools'
import { SerpAPI } from 'langchain/tools'
const tools = [new Calculator(), new SerpAPI()]
const executor = await initializeAgentExecutor(
tools,
model,
'zero-shot-react-description'
)
// 3. 解决的问题
// 3.1 简化LLM集成
// 不用自己处理API调用、token计数、重试等
// 3.2 提供标准接口
// 统一不同模型的调用方式
// 3.3 支持复杂流程
// 链式调用、Agent、RAG开箱即用
// 3.4 前端专用版本
import { ChatOpenAI } from 'langchain/chat_models'
import { ConversationChain } from 'langchain/chains'
// 在浏览器中运行
const chain = new ConversationChain({
llm: new ChatOpenAI({
openAIApiKey: process.env.OPENAI_API_KEY
})
})
📚 知识点速查表
| 知识点 | 核心要点 |
|---|---|
| LRU缓存 | Map实现、双向链表、O(1)操作 |
| 大数相加 | 字符串处理、进位、小数处理 |
| 玻璃球问题 | 间隔递减法、最坏情况最小化 |
| 成本优化 | CDN费用、缓存命中率、压缩 |
| 国际化 | i18n、RTL、语言判断、多语言工具 |
| 跨端方案 | H5、离线包、RN、Flutter对比 |
| JSBridge | 方法注入、调用链路、回调处理 |
| LangChain | 链式调用、RAG、Agent、简化LLM集成 |
📌 最后一句:
腾讯这场二面和三面,从算法思维到成本意识,从国际化到跨端通信,再到AI工程化框架,考察的是工程师的综合素养。能答好这些,说明你不仅有代码能力,还有业务视野和技术深度。
