一面(JS/浏览器 & 基础编码)
Vue.nextTick 的实现原理(Vue2/3 通吃的本质)Promise.then > setTimeout。
Promise / MutationObserver / setImmediate / setTimeout 多重降级;维护 callbacks 队列 + pending 标志,统一 flushCallbacks。
scheduler)里有 queueJob/queueFlush,nextTick 实际是 Promise.resolve().then(flushJobs)。
// 极简 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); // 微任务
}
}方案A:IntersectionObserver(推荐)
<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(),结合节流避免频繁触发。
trimfunction 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);
}Promise.allfunction 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)
);
});
});
}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');输出顺序:
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把后续逻辑塞进微任务。
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;
}一面复盘:题目基础但要求准确。写代码时把边界条件说清楚,按测试思路讲解(空输入、最小规模、复杂连通块)。心态比什么都重要。
overflow: auto|hidden、display: flow-root、position: absolute/fixed、float)、或给父加 padding/border。overflow: hidden 同时会创建BFC,影响 margin 折叠与清浮动。
口诀:flow-root 断折叠,
overflow慎用(既裁剪又建BFC)。
二倍均值法(简单公平、无偏易实现):
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;
}工程要点:都用整数分;前端展示再转元;必要时可加上下限与偏度控制(例如不希望极端大红包)。
setTimeout,不用 setInterval)function print1ToN(n) {
let i = 1;
function tick() {
if (i > n) return;
console.log(i++);
setTimeout(tick, 1000);
}
setTimeout(tick, 1000);
}面试常问点:为什么链式
setTimeout比setInterval更稳?——可控漂移、避免任务堆积。
结论:最少 11 场。难点在剪枝与必须比较的最小集合推导。
二面复盘:面试官会引导,核心是你能否把不确定问题拆成可验证的子结论(谁必然无缘前4、谁还需比较)。代码题强调边界、复杂度、可测试性。
窗口函数版(推荐)
SELECT *
FROM (
SELECT e.*,
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rn
FROM employees e
) t
WHERE rn = 1;聚合连接版
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;[a,b,c,d],在 tbcacbdata 中找长度为4、恰好包含这4个字符(顺序无关)的连续子串起始下标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')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) 更新。
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)的技巧;说明可达性与负值处理。
cwnd 从 1 MSS 指数增长,至 ssthresh;
ssthresh=cwnd/2。
心得:应用层别乱开无脑并发;配合应用层限速/重试,避免“拥塞雪崩”。
对比:B 树叶/内节点都存数据,范围扫描没 B+ 流畅;跳表/哈希适合点查,不擅长范围。
三面复盘:先讲定义与痛点,再讲典型策略与取舍,最后给工程建议(比如 TCP 并发连接与限速策略、索引设计的过滤性/选择度评估)。
事件循环口诀:同步 → 微任务(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 说得出优缺点与适用场景