本篇来自于:前端周刊-面试资源,加群交流~
👤 作者:咸鱼翻身
关键词:
nextTick
、懒加载、trim
、Promise.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
/queueFlush
,nextTick
实际是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 start
→setTimeout
入宏任务 →async1
执行同步部分 →promise1
→script end
-
微任务队列:
await
的后续(async1 end
)与then
(promise2
) -
宏任务队列:
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) :垂直方向上父子/兄弟的外边距会折叠为更大者。
- 断开折叠:给父元素创建 BFC (
overflow: auto|hidden
、display: flow-root
、position: absolute/fixed
、float
)、或给父加padding/border
。
- 断开折叠:给父元素创建 BFC (
-
边界溢出(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);
}
面试常问点:为什么链式
setTimeout
比setInterval
更稳?------可控漂移、避免任务堆积。
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
跳到j
(j>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 思想):
-
慢启动 :
cwnd
从 1 MSS 指数增长,至ssthresh
; -
拥塞避免:线性增长;
-
丢包:
-
3 次重复 ACK:快速重传 + 快速恢复(乘法减小,进入拥塞避免);
-
超时 :回到慢启动,
ssthresh=cwnd/2
。
-
-
心得:应用层别乱开无脑并发;配合应用层限速/重试,避免"拥塞雪崩"。
6)为什么数据库索引用 B+ 树
-
高扇出、低高度:节点存多Key,多级磁盘I/O少;
-
叶子链表 :范围查询天然友好;
-
非叶仅存 Key:内存命中率高;
-
稳定性:插删平衡成本可控。
对比:B 树叶/内节点都存数据,范围扫描没 B+ 流畅;跳表/哈希适合点查,不擅长范围。
7)进程调度方式
- FCFS 、SJF/短作业优先 (平均等待短但易饿死)、优先级 、时间片轮转 、多级反馈队列(MLFQ) 、Linux CFS(红黑树按虚拟运行时间公平)。
三面复盘 :先讲定义与痛点 ,再讲典型策略与取舍 ,最后给工程建议(比如 TCP 并发连接与限速策略、索引设计的过滤性/选择度评估)。
心态与策略(通关要点)
-
引导面试官 :自我介绍里明确你的强项与"能聊深的项目"。
-
代码要能跑:思路→复杂度→边界→可测试;别上来就写一屏。
-
遇到不确定 :先约束版本(如单位用"分"、空输入返回啥),再实现。
-
算法题 :先给朴素解 ,再给优化思路(如 DFS→并查集 / O(n²)→O(n))。
-
心态 :紧张就复述题意 + 列边界,把大脑切回"确定性步骤"。
小抄区(可直接面试时口述/板书)
事件循环口诀 :同步 → 微任务(await
/then
)→ 宏任务(setTimeout
) nextTick
:微任务触发回调队列 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 说得出优缺点与适用场景