字节跳动前端代码题汇总(2025-2026)
目录
一、高频手写代码题
1. 带并发限制的 Promise 调度器(必考 ⭐⭐⭐⭐⭐)
class Scheduler {
constructor(limit) {
this.limit = limit;
this.running = 0;
this.queue = [];
}
add(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject });
this.run();
});
}
run() {
while (this.running < this.limit && this.queue.length) {
const { task, resolve, reject } = this.queue.shift();
this.running++;
task()
.then(resolve, reject)
.finally(() => {
this.running--;
this.run();
});
}
}
}
// 测试
const scheduler = new Scheduler(2);
const task = (id, delay) => () =>
new Promise(resolve => {
console.log(`任务${id}开始`);
setTimeout(() => {
console.log(`任务${id}完成`);
resolve(id);
}, delay);
});
scheduler.add(task(1, 1000));
scheduler.add(task(2, 500));
scheduler.add(task(3, 300));
scheduler.add(task(4, 800));
// 输出: 1开始 2开始 → 2完成 3开始 → 3完成 4开始 → 1完成 4完成
2. 带重试和超时的 fetch
/**
*
* @param {string} url
* @param {{ retrytimes: number; timeout: number; }} options
* @returns
*/
async function myFetch(url, options) {
const { retrytimes = 1, timeout = 5000, ...args } = options;
let retryCount = 0;
return new Promise((resolve, reject) => {
const run = () => {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
fetch(url, { signal: controller.signal, ...args })
.then((res) => {
clearTimeout(timer);
resolve(res);
})
.catch((err) => {
clearTimeout(timer);
if (retryCount < retrytimes) {
retryCount++;
console.log(
`第 ${retryCount} 次重试,${retryCount * 1000}ms 后执行`,
);
setTimeout(() => run(), retryCount * 1000);
} else {
reject(err);
}
});
};
run();
});
}
3. 深拷贝(含循环引用)
function deepClone(obj, map = new WeakMap()) {
if (typeof obj !== 'object' || obj === null) return obj;
if (map.has(obj)) return map.get(obj);
const target = Array.isArray(obj) ? [] : {};
map.set(obj, target);
for (const key of Object.keys(obj)) {
target[key] = deepClone(obj[key], map);
}
return target;
}
关键点:为什么用 WeakMap?弱引用不会阻止 GC 回收,防止内存泄漏。
4. 防抖(debounce)
两种防抖对应两种触发时机:
Trailing(尾部触发,immediate = false)
- 事件停止触发后,等待 delay 才执行一次。
- 只要还在连续触发,就一直往后推,永远不执行,直到安静下来。
- 特点:执行的是"最后一次"的结果,有延迟感。
- 场景:搜索框输入联想(等用户打完字再发请求)、窗口 resize 结束后再重新布局。
Leading(头部触发,immediate = true)
- 第一次触发立即执行,然后进入 delay 冷却期。
- 冷却期内的触发只会不断刷新计时,不执行;直到安静 delay 后才恢复,下次触发又能立即执行。
- 特点:响应快(第一下就有反应),之后拦截高频重复。
- 场景:按钮防连点(第一下就提交,后面的点击忽略)、防止表单重复提交。
function debounce(fn, delay = 300, immediate = false) {
let timer = null;
const debounced = function (...args) {
if (timer) clearTimeout(timer);
if (immediate) {
if(timer === null) fn.apply(this, args);
timer = setTimeout(() => { timer = null; }, delay);
} else {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
}
};
debounced.cancel = () => { clearTimeout(timer); timer = null; };
return debounced;
}
5. 节流(throttle)
节流(throttle)的定义:在一段连续、高频的调用中,限制目标函数在固定时间间隔内最多执行一次。
核心是「稀释频率」。不管你在窗口期内触发多少次,函数只按 delay 这个节奏执行,把密集的调用压缩成均匀的、有上限频率的执行。
和防抖(debounce)的区别
这两个经常被搞混,对比着看最清楚:
| | 节流 throttle | 防抖 debounce | | ---------- | --------------------------------- | | 规则 | 每隔 delay 至多执行一次 | 停止触发后等 delay 再执行 | | 高频调用时 | 会按节奏执行(如每 200ms 执行一次) | 一直不执行,直到你停手 | | 类比 | 技能冷却 CD,期间狂点也没用 | 电梯门,有人进就重新等,没人了才关 |
触发时机的三种变体
「执行一次」具体在窗口的哪个时刻,分三种,这也是你上一个问题的关键:
- leading(首次执行):窗口一开始就立即执行。响应快,但停手后最后那次可能丢。
- trailing(末次执行):窗口结束时执行。能拿到最后一次的最新值,但首次有延迟。
- leading + trailing:两者都要——首次立即响应,连续调用结束后再补一次末次。你 2.js 里实现的就是这种,也是最常用、体验最好的一种。
典型场景
- 滚动监听 scroll、窗口 resize——避免每像素都触发回调
- 鼠标 mousemove、拖拽
- 按钮防连点、搜索联想的频率控制
一句话概括:防抖是「等你安静下来再干」,节流是「按固定节奏干,多催也没用」。
// 定时器版:最后一次也会执行
function throttle(fn, delay = 300) {
let timer = null;
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
}
};
}
// 时间戳版:立即执行,停止后不再执行
function throttle2(fn, delay = 300) {
let lastTime = 0;
return function (...args) {
const nowTime = Date.now();
if (nowTime - lastTime > delay) {
fn.apply(this, args);
lastTime = nowTime;
}
};
}
// 立即执行,最后一次也执行
function throttle(fn, delay) {
let pre = 0;
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
const now = +new Date();
if (now - pre >= delay) {
fn.apply(this, args);
pre = now;
timer = null;
} else {
timer = setTimeout(
() => {
fn.apply(this, args);
pre = +new Date();
timer = null;
},
delay - (now - pre),
);
}
};
}
6. 数组转树形结构
function listToTree(data) {
const map = {};
const tree = [];
data.forEach(item => { map[item.id] = { ...item, children: [] }; });
data.forEach(item => {
if (item.parentId === 0) {
tree.push(map[item.id]);
} else if (map[item.parentId]) {
map[item.parentId].children.push(map[item.id]);
}
});
return tree;
}
7. 函数柯里化
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return (...nextArgs) => curried.apply(this, [...args, ...nextArgs]);
};
}
// 使用
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
8. 手写 Promise.all
Promise.myAll = function (promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) return reject(new TypeError('参数必须是数组'));
const result = [];
let count = 0;
if (promises.length === 0) return resolve(result);
promises.forEach((p, index) => {
Promise.resolve(p).then(
res => {
result[index] = res;
count++;
if (count === promises.length) resolve(result);
},
err => reject(err)
);
});
});
};
9. 手写 Promise.race
Promise.myRace = function (promises) {
return new Promise((resolve, reject) => {
promises.forEach(p => {
Promise.resolve(p).then(resolve, reject);
});
});
};
10. 版本号比较
function compareVersion(v1, v2) {
const arr1 = v1.split('.').map(Number);
const arr2 = v2.split('.').map(Number);
const len = Math.max(arr1.length, arr2.length);
for (let i = 0; i < len; i++) {
const a = arr1[i] || 0;
const b = arr2[i] || 0;
if (a > b) return 1;
if (a < b) return -1;
}
return 0;
}
compareVersion('1.0.1', '1.0.0'); // 1
compareVersion('1.0.0', '1.0.1'); // -1
compareVersion('1.0', '1.0.0'); // 0
11. 洗牌算法(Fisher-Yates)
function shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
12. URL 解析 query 参数
function parseQuery(url) {
const params = {};
const queryString = url.split('?')[1];
if (!queryString) return params;
queryString.split('&').forEach(pair => {
const [key, val] = pair.split('=');
params[decodeURIComponent(key)] = val ? decodeURIComponent(val) : '';
});
return params;
}
13. 数组扁平化
function flatten(arr, depth = 1) {
if (depth === 0) return arr;
return arr.reduce(
(acc, val) =>
acc.concat(Array.isArray(val) ? flatten(val, depth - 1) : val),
[]
);
}
// 完全扁平
function flattenDeep(arr) {
return arr.reduce(
(acc, val) => acc.concat(Array.isArray(val) ? flattenDeep(val) : val),
[]
);
}
14. 大数相加
function addBigNumber(a, b) {
let i = a.length - 1, j = b.length - 1, carry = 0, result = '';
while (i >= 0 || j >= 0 || carry) {
const sum = (+a[i] || 0) + (+b[j] || 0) + carry;
result = (sum % 10) + result;
carry = Math.floor(sum / 10);
i--; j--;
}
return result;
}
15. 模拟 setTimeout 实现 setInterval
function mySetInterval(fn, delay) {
let timer = null;
let cancelled = false;
function execute() {
if (cancelled) return;
fn();
timer = setTimeout(execute, delay);
}
timer = setTimeout(execute, delay);
return {
clear: () => {
cancelled = true;
clearTimeout(timer);
}
};
}
// 为什么不用 setInterval?
// setInterval 不会等回调执行完就继续触发下一次,可能造成回调堆积。
// 用 setTimeout 递归可以保证每次执行完才启动下一次。
16. 实现 compose 中间件(Koa 洋葱模型)
function compose(middlewares) {
return function (context) {
let index = -1;
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'));
index = i;
const fn = middlewares[i];
if (!fn) return Promise.resolve();
try {
return Promise.resolve(fn(context, () => dispatch(i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
return dispatch(0);
};
}
// 测试
const m1 = async (ctx, next) => { console.log(1); await next(); console.log(1.1); };
const m2 = async (ctx, next) => { console.log(2); await next(); console.log(2.2); };
compose([m1, m2])({});
// 输出: 1 → 2 → 2.2 → 1.1
17. 实现 instanceof
function myInstanceof(obj, constructor) {
let proto = Object.getPrototypeOf(obj);
while (proto) {
if (proto === constructor.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
二、算法与数据结构
1. LRU 缓存机制(必考 ⭐⭐⭐⭐⭐)
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return -1;
const val = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, val); // 移到最"新"位置
return val;
}
put(key, value) {
if (this.cache.has(key)) this.cache.delete(key);
this.cache.set(key, value);
if (this.cache.size > this.capacity) {
// Map 的 keys() 返回插入顺序,第一个就是最久未使用的
const oldest = this.cache.keys().next().value;
this.cache.delete(oldest);
}
}
}
2. 超时自动删除的 LRU(2026 新变种)
class LRUCacheWithTTL {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
this.timers = new Map();
}
get(key) {
if (!this.cache.has(key)) return -1;
const { value, ttl, timestamp } = this.cache.get(key);
if (ttl && Date.now() - timestamp > ttl) {
this.cache.delete(key);
this._clearTimer(key);
return -1;
}
this.cache.delete(key);
this.cache.set(key, { value, ttl, timestamp: Date.now() });
return value;
}
put(key, value, ttl) {
if (this.cache.has(key)) {
this.cache.delete(key);
this._clearTimer(key);
}
this.cache.set(key, { value, ttl, timestamp: Date.now() });
if (ttl) {
this.timers.set(key, setTimeout(() => {
this.cache.delete(key);
this.timers.delete(key);
}, ttl));
}
if (this.cache.size > this.capacity) {
const oldest = this.cache.keys().next().value;
this.cache.delete(oldest);
this._clearTimer(oldest);
}
}
_clearTimer(key) {
if (this.timers.has(key)) {
clearTimeout(this.timers.get(key));
this.timers.delete(key);
}
}
}
3. 岛屿数量(必考 ⭐⭐⭐⭐⭐)
function numIslands(grid) {
let count = 0;
const m = grid.length, n = grid[0].length;
function dfs(i, j) {
if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] === '0') return;
grid[i][j] = '0'; // "沉岛"
dfs(i + 1, j); dfs(i - 1, j);
dfs(i, j + 1); dfs(i, j - 1);
}
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (grid[i][j] === '1') {
count++;
dfs(i, j);
}
}
}
return count;
}
4. 二叉树非递归遍历(高频)
// 前序:根 → 左 → 右
function preorder(root) {
if (!root) return [];
const stack = [root], res = [];
while (stack.length) {
const node = stack.pop();
res.push(node.val);
if (node.right) stack.push(node.right);
if (node.left) stack.push(node.left);
}
return res;
}
// 中序:左 → 根 → 右
function inorder(root) {
const stack = [], res = [];
let cur = root;
while (cur || stack.length) {
while (cur) {
stack.push(cur);
cur = cur.left;
}
cur = stack.pop();
res.push(cur.val);
cur = cur.right;
}
return res;
}
// 后序:左 → 右 → 根 (前序反过来)
function postorder(root) {
if (!root) return [];
const stack = [root], res = [];
while (stack.length) {
const node = stack.pop();
res.unshift(node.val);
if (node.left) stack.push(node.left);
if (node.right) stack.push(node.right);
}
return res;
}
// 层序遍历
function levelOrder(root) {
if (!root) return [];
const queue = [root], res = [];
while (queue.length) {
const len = queue.length;
const level = [];
for (let i = 0; i < len; i++) {
const node = queue.shift();
level.push(node.val);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
res.push(level);
}
return res;
}
5. 其他高频算法题速查
| 题号 | 题目 | 类型 | 难度 | 频率 |
|---|---|---|---|---|
| LeetCode 1 | 两数之和 | 哈希表 | Easy | ⭐⭐⭐⭐⭐ |
| LeetCode 3 | 无重复字符的最长子串 | 滑动窗口 | Medium | ⭐⭐⭐⭐ |
| LeetCode 5 | 最长回文子串 | 动态规划 | Medium | ⭐⭐⭐ |
| LeetCode 15 | 三数之和 | 双指针 | Medium | ⭐⭐⭐⭐ |
| LeetCode 20 | 有效括号 | 栈 | Easy | ⭐⭐⭐⭐⭐ |
| LeetCode 53 | 最大子序和 | DP | Easy | ⭐⭐⭐⭐ |
| LeetCode 70 | 爬楼梯 | DP | Easy | ⭐⭐⭐⭐⭐ |
| LeetCode 88 | 合并有序数组 | 双指针 | Easy | ⭐⭐⭐⭐ |
| LeetCode 102 | 二叉树层序遍历 | BFS | Medium | ⭐⭐⭐⭐⭐ |
| LeetCode 121 | 买卖股票最佳时机 | DP | Easy | ⭐⭐⭐⭐ |
| LeetCode 146 | LRU 缓存 | 设计 | Medium | ⭐⭐⭐⭐⭐ |
| LeetCode 198 | 打家劫舍 | DP | Medium | ⭐⭐⭐⭐ |
| LeetCode 200 | 岛屿数量 | DFS/BFS | Medium | ⭐⭐⭐⭐⭐ |
| LeetCode 206 | 反转链表 | 链表 | Easy | ⭐⭐⭐⭐ |
| LeetCode 300 | 最长递增子序列 | DP/二分 | Medium | ⭐⭐⭐⭐ |
| LeetCode 322 | 零钱兑换 | DP | Medium | ⭐⭐⭐⭐ |
| LeetCode 415 | 字符串相加(大数加法) | 字符串 | Easy | ⭐⭐⭐⭐ |
| LeetCode 695 | 岛屿最大面积 | DFS | Medium | ⭐⭐⭐⭐ |
| LeetCode 994 | 腐烂的橘子 | BFS | Medium | ⭐⭐⭐ |
三、场景设计题
1. 大文件上传(分片 + 断点续传 + 秒传)
核心思路:
- 前端用
Blob.slice()分片 - 用 SparkMD5 计算文件 hash 实现秒传
- 上传前先请求
/check接口,返回已上传的切片索引 - 并发上传未完成的切片(配合并发调度器)
- 全部上传完成后调
/merge合并
class BigFileUploader {
constructor(file, options = {}) {
this.file = file;
this.chunkSize = options.chunkSize || 1024 * 1024 * 5; // 5MB
this.concurrent = options.concurrent || 3;
this.scheduler = new Scheduler(this.concurrent);
}
async upload() {
const chunks = this.createChunks();
const hash = await this.computeHash(chunks);
// 1. 检查已上传切片
const uploaded = await this.checkUpload(hash);
// 2. 上传未完成的切片
const tasks = chunks
.filter((_, i) => !uploaded.includes(i))
.map(chunk => this.scheduler.add(() => this.uploadChunk(chunk)));
await Promise.all(tasks);
// 3. 合并
await this.merge(hash, chunks.length);
}
createChunks() {
const chunks = [];
let start = 0;
while (start < this.file.size) {
chunks.push({
file: this.file.slice(start, start + this.chunkSize),
index: chunks.length,
hash: `${this.file.name}-${chunks.length}`,
});
start += this.chunkSize;
}
return chunks;
}
async computeHash(chunks) {
// 简化版:真实场景用 SparkMD5 + Web Worker 计算 hash
return `${this.file.name}-${this.file.size}`;
}
async checkUpload(hash) {
const res = await fetch(`/check?hash=${hash}`).then(r => r.json());
return res.uploaded || [];
}
async uploadChunk(chunk) {
const formData = new FormData();
formData.append('chunk', chunk.file);
formData.append('index', chunk.index);
formData.append('hash', chunk.hash);
return fetch('/upload', { method: 'POST', body: formData });
}
async merge(hash, total) {
return fetch('/merge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hash, total }),
});
}
}
2. 虚拟滚动 / 虚拟列表
核心思路:只渲染可视区域内的 DOM 节点,通过 transform: translateY 占位。
class VirtualList {
constructor(container, items, itemHeight) {
this.container = container;
this.items = items;
this.itemHeight = itemHeight;
this.visibleCount = Math.ceil(container.clientHeight / itemHeight) + 2; // +2 缓冲
this.startIndex = 0;
this.scrollEl = container;
this.render();
this.scrollEl.addEventListener('scroll', () => this.onScroll());
}
onScroll() {
const scrollTop = this.scrollEl.scrollTop;
this.startIndex = Math.floor(scrollTop / this.itemHeight);
this.render();
}
render() {
this.container.innerHTML = '';
const list = document.createElement('div');
list.style.height = `${this.items.length * this.itemHeight}px`;
list.style.position = 'relative';
const endIndex = Math.min(this.startIndex + this.visibleCount, this.items.length);
for (let i = this.startIndex; i < endIndex; i++) {
const item = document.createElement('div');
item.style.position = 'absolute';
item.style.top = `${i * this.itemHeight}px`;
item.style.height = `${this.itemHeight}px`;
item.textContent = this.items[i];
list.appendChild(item);
}
this.container.appendChild(list);
}
}
3. JS 执行 100 万个任务不卡顿(时间分片)
function performChunk(tasks, budget = 5) {
let i = 0;
function _run() {
const start = performance.now();
// 只要还有任务,且本帧预算没用完,就继续执行
while (i < tasks.length && performance.now() - start < budget) {
tasks[i]();
i++;
}
if (i < tasks.length) {
requestAnimationFrame(_run);
}
}
requestAnimationFrame(_run);
}
4. 批量请求失败只弹一个 toast
class ToastManager {
constructor() {
this.toasting = false;
this.pendingCount = 0;
}
async request(fn) {
try {
return await fn();
} catch (err) {
this.pendingCount++;
if (!this.toasting) {
this.toasting = true;
// 延迟收集,合并同一批错误
setTimeout(() => {
this.showToast(`共 ${this.pendingCount} 个请求失败`);
this.toasting = false;
this.pendingCount = 0;
}, 100);
}
throw err;
}
}
showToast(msg) {
console.log('Toast:', msg);
}
}
5. 列表分页快速翻页下的竞态问题
class PageManager {
constructor() {
this.requestId = 0;
}
async fetchPage(page) {
const id = ++this.requestId;
const data = await fetch(`/api/list?page=${page}`).then(r => r.json());
// 只处理最新请求,丢弃过时的
if (id === this.requestId) {
this.render(data);
}
}
}
6. 前端截图实现
思路:
- 常规方案:
html2canvas(遍历 DOM → 绘制到 Canvas →toDataURL/toBlob) - 跨域图片需要服务端代理或设置 CORS +
crossorigin="anonymous" - 部分特殊元素(iframe、视频)需要降级处理
7. 微前端 JS 隔离原理
| 方案 | 隔离方式 | 特点 |
|---|---|---|
| qiankun | 快照沙箱 / Proxy 沙箱 | 快照沙箱兼容性好但有性能开销;Proxy 沙箱现代浏览器可用 |
| MicroApp | Proxy + CustomEvent | 更轻量,基于 Web Components |
| wujie | iframe 隔离 + Web Components | 天然隔离,但通信成本高 |
| Module Federation | 构建时隔离 | Webpack 5 原生,共享依赖 |
四、框架与原理
1. Vue3 响应式原理简写
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key); // 依赖收集
const result = Reflect.get(target, key, receiver);
return typeof result === 'object' ? reactive(result) : result; // 懒代理
},
set(target, key, value, receiver) {
const oldVal = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldVal !== value) {
trigger(target, key); // 触发更新
}
return result;
}
});
}
function ref(value) {
return {
get value() { track(this, 'value'); return value; },
set value(newVal) {
if (newVal !== value) {
value = newVal;
trigger(this, 'value');
}
}
};
}
// computed
function computed(getter) {
let dirty = true;
let cached;
const result = {
get value() {
track(result, 'value');
if (dirty) {
cached = getter();
dirty = false;
}
return cached;
}
};
effect(() => { dirty = true; trigger(result, 'value'); }, getter);
return result;
}
2. React Hooks 闭包陷阱及解决
// 问题:setInterval 内拿到的 count 始终是初始值
function BadCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 始终输出 0
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
}
// 解决方案1:用函数式 setState
function GoodCounter1() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1); // 不依赖外部 count
}, 1000);
return () => clearInterval(timer);
}, []);
}
// 解决方案2:useRef 保存最新值
function GoodCounter2() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const timer = setInterval(() => {
setCount(countRef.current + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
}
3. React 简易 Virtual DOM 转 HTML
function vdomToHtml(node) {
if (typeof node === 'string') return node;
const { type, props = {} } = node;
const attrs = Object.entries(props)
.filter(([k]) => k !== 'children')
.map(([k, v]) => ` ${k}="${v}"`)
.join('');
const children = (props.children || [])
.map(child => vdomToHtml(child))
.join('');
if (type === 'br' || type === 'hr' || type === 'img' || type === 'input') {
return `<${type}${attrs} />`;
}
return `<${type}${attrs}>${children}</${type}>`;
}
const vdom = {
type: 'div',
props: {
class: 'container',
children: [
{ type: 'h1', props: { children: ['Hello'] } },
{ type: 'p', props: { children: ['World'] } },
]
}
};
// vdomToHtml(vdom) => <div class="container"><h1>Hello</h1><p>World</p></div>
五、2025-2026 新增趋势题
1. SSE 流式输出处理
class SSEClient {
constructor(url) {
this.url = url;
this.abortController = null;
}
async connect(onMessage, onError) {
this.abortController = new AbortController();
try {
const response = await fetch(this.url, {
signal: this.abortController.signal,
headers: { 'Accept': 'text/event-stream' },
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // 保留不完整的行
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') return;
try {
onMessage(JSON.parse(data));
} catch {
onMessage(data);
}
}
}
}
} catch (err) {
if (err.name !== 'AbortError') onError(err);
}
}
disconnect() {
this.abortController?.abort();
}
}
2. TypeScript 类型体操
// DeepReadonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function ? T[K] : DeepReadonly<T[K]>
: T[K];
};
// DeepPartial
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
// Promise 返回值类型提取
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;
// 元组转联合类型
type TupleToUnion<T extends any[]> = T[number];
3. QQ 在线峰值问题
题目:给定一组登录登出时间 [start, end],求同时在线的最大用户数。
function maxOnline(users) {
// users = [[loginTime, logoutTime], ...]
const events = [];
for (const [login, logout] of users) {
events.push([login, 1]); // 上线 +1
events.push([logout, -1]); // 下线 -1
}
events.sort((a, b) => a[0] - b[0] || a[1] - b[1]); // 先下线再上线
let max = 0, current = 0;
for (const [, delta] of events) {
current += delta;
max = Math.max(max, current);
}
return max;
}
4. MCP 协议基础概念(2026 新增考察点)
- MCP (Model Context Protocol):AI 模型与外部工具/数据源之间的标准协议
- 核心概念:Tool、Resource、Prompt
- 前端关注点:如何在应用中集成 MCP 客户端、流式响应处理、工具调用 UI
5. AI 相关高频问答题
- 如何设计 AI 辅助编码平台:代码补全服务(SSE 流式)、上下文收集(RAG)、生成代码质量审核
- Token 消耗优化:前端侧如何控制 context 长度、压缩历史消息、缓存公共 prompt
- D2C (Design to Code) 的质量保障策略
- AI 生成代码的 Code Review 流程
面试流程与备考策略
字节前端面试轮次
| 轮次 | 时长 | 考察重点 |
|---|---|---|
| 一面 | ~60min | JS 基础 + 2~3 道手写代码题 + 项目浅挖 |
| 二面 | ~60min | 框架原理 + 场景设计 + 架构能力 + 算法 |
| 三面 | ~60min | 系统设计 + 技术视野 + 业务理解 + 跨团队协作 |
| HR 面 | ~50min | 职业规划 + 薪资期望 + 软素质 |
备考优先级
- 手写题(一面敲门砖):Promise 调度器、深拷贝、防抖节流,必须能 15 分钟无 bug 默写
- 算法:LRU、岛屿数量、二叉树遍历,字节高频 TOP 3
- 场景设计(二面/三面):大文件上传、虚拟滚动、并发控制、SSE 流式
- 框架原理:Vue3 响应式 / React Fiber + Hooks 闭包陷阱
- AI 相关(2026 新增):SSE 流式处理、AI 编码工具的使用和原理
数据来源:掘金、牛客网、CSDN、语雀等平台 2025-2026 字节前端面经
声明:以上题目均为面试者分享的回忆版,实际面试题目可能有所不同,仅供参考学习。