字节前端三面复盘:基础不花哨,代码要扎实(含高频题解)

本篇来自于:前端周刊-面试资源,加群交流

👤 作者:咸鱼翻身

关键词:nextTick、懒加载、trimPromise.all、事件循环、岛屿数量、边界合并/溢出、拼手气红包、定时输出、64马8道、SQL分组取Top、动态规划(最大体力)、TCP可靠与拥塞、B+树、进程调度


一面(JS/浏览器 & 基础编码)

1)Vue.nextTick 的实现原理(Vue2/3 通吃的本质)

  • 本质:把回调压进微任务队列,等本轮 DOM 变更"flush"后再执行,保证你拿到的是真实更新后的 DOM。

  • 关键点:

    • 微任务优先级高于宏任务:Promise.then > setTimeout

    • Vue2:用 Promise / MutationObserver / setImmediate / setTimeout 多重降级;维护 callbacks 队列 + pending 标志,统一 flushCallbacks

    • Vue3:统一调度器(scheduler)里有 queueJob/queueFlushnextTick 实际是 Promise.resolve().then(flushJobs)

JavaScript 复制代码
// 极简 polyfill 示意(核心思路)
const callbacks = [];
let pending = false;
function flushCallbacks() {
  pending = false;
  const copies = callbacks.slice();
  callbacks.length = 0;
  for (const cb of copies) cb();
}
export function nextTick(cb) {
  callbacks.push(cb);
  if (!pending) {
    pending = true;
    Promise.resolve().then(flushCallbacks); // 微任务
  }
}

2)图片懒加载(两种实现)

方案A:IntersectionObserver(推荐)

HTML 复制代码
<img data-src="https://example.com/pic.jpg" alt="demo" />
<script>
const io = new IntersectionObserver(entries => {
  entries.forEach(e => {
    if (e.isIntersecting) {
      const img = e.target;
      img.src = img.dataset.src;
      io.unobserve(img);
    }
  });
}, { rootMargin: '200px' }); // 提前预加载

document.querySelectorAll('img[data-src]').forEach(img => io.observe(img));
</script>

方案B:滚动监听 + 节流(兼容兜底) :监听 scroll,计算 getBoundingClientRect(),结合节流避免频繁触发。


3)实现 trim

JavaScript 复制代码
function myTrim(str) {
  // 简洁可靠:去掉首尾空白(含各种空白符)
  return str.replace(/^\s+|\s+$/g, '');
}
// 或者手写指针法,避免正则
function myTrim2(str) {
  let l = 0, r = str.length - 1;
  while (l <= r && /\s/.test(str[l])) l++;
  while (r >= l && /\s/.test(str[r])) r--;
  return str.slice(l, r + 1);
}

4)实现 Promise.all

JavaScript 复制代码
function myPromiseAll(iterable) {
  return new Promise((resolve, reject) => {
    const arr = Array.from(iterable);
    const res = new Array(arr.length);
    let done = 0;
    if (arr.length === 0) return resolve([]);
    arr.forEach((p, i) => {
      Promise.resolve(p).then(
        v => {
          res[i] = v;
          if (++done === arr.length) resolve(res);
        },
        err => reject(err)
      );
    });
  });
}

5)事件循环"看输出"

JavaScript 复制代码
async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}
async function async2() {
  console.log('async2');
}
console.log('script start');
setTimeout(function() {
  console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {
  console.log('promise1');
  resolve();
}).then(function() {
  console.log('promise2');
});
console.log('script end');

输出顺序:

Plain 复制代码
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

解释要点:

  • 同步:script startsetTimeout入宏任务 → async1 执行同步部分 → promise1script end

  • 微任务队列:await 的后续(async1 end)与 thenpromise2

  • 宏任务队列:setTimeout

小抄:同步 → 微任务 → 宏任务await 把后续逻辑塞进微任务。


6)编程题:岛屿数量(DFS/BFS/并查集)

TypeScript 复制代码
function numIslands(grid: string[][]): number {
  if (!grid.length) return 0;
  const m = grid.length, n = grid[0].length;
  let count = 0;
  const dirs = [[1,0],[-1,0],[0,1],[0,-1]];

  const dfs = (i: number, j: number) => {
    if (i<0||j<0||i>=m||j>=n||grid[i][j]!=='1') return;
    grid[i][j] = '0';
    for (const [dx,dy] of dirs) dfs(i+dx, j+dy);
  };

  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;
}

一面复盘 :题目基础但要求准确 。写代码时把边界条件说清楚,按测试思路讲解(空输入、最小规模、复杂连通块)。心态比什么都重要。


二面(CSS/工程 & 思维题 + 实战编码)

1)"边界合并"和"边界溢出"

  • 边界合并(Margin Collapsing) :垂直方向上父子/兄弟的外边距会折叠为更大者

    • 断开折叠:给父元素创建 BFCoverflow: auto|hiddendisplay: flow-rootposition: absolute/fixedfloat)、或给父加 padding/border
  • 边界溢出(Overflow) :内容超过盒子产生滚动/裁剪;注意 overflow: hidden 同时会创建BFC,影响 margin 折叠与清浮动。

口诀:flow-root 断折叠overflow 慎用(既裁剪又建BFC)。


2)拼手气"红包"算法(单位:分;每人至少1分)

二倍均值法(简单公平、无偏易实现):

JavaScript 复制代码
function splitLucky(totalCents, n) {
  if (n <= 0 || totalCents < n) throw new Error('非法参数');
  const res = [];
  let remain = totalCents, remainN = n;

  for (let i = 0; i < n - 1; i++) {
    // 每次在 [1, 2*平均-1] 间随机(保证至少留1分给后面每人)
    const max = Math.floor((remain / remainN) * 2) - 1;
    const cur = Math.max(1, Math.floor(Math.random() * max) + 1);
    res.push(cur);
    remain -= cur;
    remainN--;
  }
  res.push(remain); // 最后一个兜底
  return res;
}

工程要点:都用整数分 ;前端展示再转元;必要时可加上下限偏度控制(例如不希望极端大红包)。


3)每隔一秒输出一个数(用 setTimeout,不用 setInterval

JavaScript 复制代码
function print1ToN(n) {
  let i = 1;
  function tick() {
    if (i > n) return;
    console.log(i++);
    setTimeout(tick, 1000);
  }
  setTimeout(tick, 1000);
}

面试常问点:为什么链式 setTimeoutsetInterval 更稳?------可控漂移、避免任务堆积。


4)智力题:64匹马、8赛道,最少场次找最快的4匹

  • 思路扩展自"25匹马5赛道求前三"。

  • 步骤 : 1)分组比赛:8组×8匹 → 8场 ,得到每组名次; 2)冠军赛:8组第一再赛一场 → 第9场 ,得到组排名 A>B>C>D>E>F>G>H; 3)候选仅可能来自前4组:A(前4名)、B(前3)、C(前2)、D(前1) ------ 共 10 匹 (其余不可能进前4)。 4)由于赛道只有8条,再加两场筛选即可 ,总计 最少 11 场

  • 说明:第10场先赛 8 匹(如 A2,A3,A4,B1,B2,B3,C1,C2),第11场把边缘选手(如 D1 与第10场的第2~5名)再比较,结合已知组内/冠军赛相对次序即可判定前4。

结论:最少 11 场 。难点在剪枝与必须比较的最小集合推导。

二面复盘 :面试官会引导,核心是你能否把不确定问题拆成可验证的子结论 (谁必然无缘前4、谁还需比较)。代码题强调边界、复杂度、可测试性


三面(全栈/系统 + 算法 & 数据库/网络/OS)

1)线程安全是什么?

  • 定义:多线程并发访问共享数据时,程序的行为可预测不出现竞态(数据竞争、可见性、原子性问题)。

  • 手段:锁(互斥量/读写锁)、原子操作、线程局部存储、无锁结构(CAS)、不变对象、消息传递等。


2)SQL:按部门取工资最高的员工

窗口函数版(推荐)

SQL 复制代码
SELECT *
FROM (
  SELECT e.*, 
         ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rn
  FROM employees e
) t
WHERE rn = 1;

聚合连接版

SQL 复制代码
SELECT e.*
FROM employees e
JOIN (
  SELECT department_id, MAX(salary) AS max_salary
  FROM employees
  GROUP BY department_id
) m ON e.department_id = m.department_id AND e.salary = m.max_salary;

3)滑窗:给定字符集 [a,b,c,d],在 tbcacbdata 中找长度为4、恰好包含这4个字符(顺序无关)的连续子串起始下标

TypeScript 复制代码
function findAnagramPos(s: string, keys: string[]): number {
  const need = new Map<string, number>();
  for (const ch of keys) need.set(ch, (need.get(ch) || 0) + 1);

  const win = new Map<string, number>();
  let valid = 0, left = 0, right = 0;
  const reqKinds = need.size, L = keys.length;

  while (right < s.length) {
    const c = s[right++];
    if (need.has(c)) {
      win.set(c, (win.get(c) || 0) + 1);
      if (win.get(c) === need.get(c)) valid++;
    }
    while (right - left >= L) { // 固定窗口长度
      if (valid === reqKinds) return left;
      const d = s[left++];
      if (need.has(d)) {
        if (win.get(d) === need.get(d)) valid--;
        win.set(d, win.get(d)! - 1);
      }
    }
  }
  return -1;
}
// 例:findAnagramPos('tbcacbdata', ['a','b','c','d']) -> 3(子串 'acbd')

4)动态规划:100格跳跃 + 蘑菇增减体力,初始体力 m,跳距离消耗=距离,问能否到终点并最大剩余体力

  • 建模:格子 0..100,已知每格蘑菇能量 delta[i](可正可负)。 从 i 跳到 jj>i)后体力:dp[i] - (j - i) + delta[j]。 递推:dp[j] = max_{i<j}(dp[i] + i) - j + delta[j];若 <0 视为不可达。

  • 线性优化:维护当前 best = max(dp[i] + i),每步 O(1) 更新。

TypeScript 复制代码
function maxRemainEnergy(m: number, delta: number[]): number|false {
  const n = 100; // 终点编号
  const dp = Array(n+1).fill(-Infinity);
  dp[0] = m; // 起点不吃蘑菇
  let best = dp[0] + 0;

  for (let j = 1; j <= n; j++) {
    dp[j] = best - j + (delta[j] || 0);
    if (dp[j] < 0) dp[j] = -Infinity; // 体力耗尽即死亡
    if (dp[j] !== -Infinity) best = Math.max(best, dp[j] + j);
  }
  return dp[n] === -Infinity ? false : dp[n]; // false 表示到不了
}

面试点:把"任意跳"转成 max(dp[i]+i)技巧 ;说明可达性负值处理


5)TCP 如何保证可靠 + 拥塞控制流程

  • 可靠性:序号/确认(Seq/Ack)、重传(超时/快速重传)、校验和、滑动窗口、流量控制(接收窗口)、乱序重组。

  • 拥塞控制(典型 Reno/CUBIC 思想):

    1. 慢启动cwnd 从 1 MSS 指数增长,至 ssthresh

    2. 拥塞避免:线性增长;

    3. 丢包

      • 3 次重复 ACK:快速重传 + 快速恢复(乘法减小,进入拥塞避免);

      • 超时 :回到慢启动,ssthresh=cwnd/2

心得:应用层别乱开无脑并发;配合应用层限速/重试,避免"拥塞雪崩"。


6)为什么数据库索引用 B+ 树

  • 高扇出、低高度:节点存多Key,多级磁盘I/O少;

  • 叶子链表范围查询天然友好;

  • 非叶仅存 Key:内存命中率高;

  • 稳定性:插删平衡成本可控。

对比:B 树叶/内节点都存数据,范围扫描没 B+ 流畅;跳表/哈希适合点查,不擅长范围。


7)进程调度方式

  • FCFSSJF/短作业优先 (平均等待短但易饿死)、优先级时间片轮转多级反馈队列(MLFQ)Linux CFS(红黑树按虚拟运行时间公平)。

三面复盘 :先讲定义与痛点 ,再讲典型策略与取舍 ,最后给工程建议(比如 TCP 并发连接与限速策略、索引设计的过滤性/选择度评估)。


心态与策略(通关要点)

  • 引导面试官 :自我介绍里明确你的强项与"能聊深的项目"。

  • 代码要能跑:思路→复杂度→边界→可测试;别上来就写一屏。

  • 遇到不确定 :先约束版本(如单位用"分"、空输入返回啥),再实现。

  • 算法题 :先给朴素解 ,再给优化思路(如 DFS→并查集 / O(n²)→O(n))。

  • 心态 :紧张就复述题意 + 列边界,把大脑切回"确定性步骤"。


小抄区(可直接面试时口述/板书)

事件循环口诀 :同步 → 微任务(await/then)→ 宏任务(setTimeoutnextTick :微任务触发回调队列 flush,Vue2 多重降级,Vue3 统一调度器 岛屿 :网格 DFS/BFS,把访问过的置 0 红包 :二倍均值法,单位用分,注意兜底 64马8道 :最少 11 场 (8 组赛 + 冠军赛 + 2 场筛选) SQL TopN :窗口函数 ROW_NUMBER() DP 最大体力dp[j] = max(dp[i]+i) - j + delta[j] TCP :Seq/Ack/重传/窗口 + 慢启动→拥塞避免→快重传/快恢复 B+树 :高扇出、叶子链表、范围查询友好 调度:MLFQ/CFS 说得出优缺点与适用场景

相关推荐
张较瘦_22 分钟前
[论文阅读] 人工智能 | 当Hugging Face遇上GitHub:预训练语言模型的跨平台同步难题与解决方案
论文阅读·人工智能·github
阳光阴郁大boy1 小时前
一个基于纯前端技术实现的五子棋游戏,无需后端服务,直接在浏览器中运行。
前端·游戏
石小石Orz1 小时前
效率提升一倍!谈谈我的高效开发工具链
前端·后端·trae
EndingCoder1 小时前
测试 Next.js 应用:工具与策略
开发语言·前端·javascript·log4j·测试·全栈·next.js
xw51 小时前
免费的个人网站托管-PinMe篇
服务器·前端
!win !1 小时前
免费的个人网站托管-PinMe篇
前端·前端工具
牧天白衣.1 小时前
CSS中linear-gradient 的用法
前端·css
军军3601 小时前
Git大型仓库的局部开发:分步克隆 + 指定目录拉取
前端·git
前端李二牛1 小时前
Vue3 特性标志
前端·javascript
coding随想2 小时前
JavaScript事件处理程序全揭秘:从HTML到IE的各种事件绑定方法!
前端