DrillLab
八股题库Question bank

105 道问答题,一道一卡105 questions, one card each

默认只显示问题 —— 先自己在心里答一遍,再展开对答案。答不上来就标「不会」,下次抽认卡会先抽它。Only the question shows by default. Answer it in your head first, then open the answer. Mark the ones you miss; the flashcard round puts those first.

只看这一节One lesson only异步与事件循环六问6 道6 questions看全部 105 道 →All 105 questions →
0 / 105道自评过self-assessed
0Got it0模糊Shaky0不会No idea105还没做Not seen
正在读这台浏览器里的标记…Reading your marks from this browser…
标记不是打分,是给下一轮排队Marks are a queue, not a score

标「不会」的题会排到抽认卡最前面,标「会」的进低频池排最后。所以别客气 —— 觉得答得磕磕巴巴就标「模糊」。准备好了就去抽认卡“No idea” jumps to the front of the next round; “got it” drops to the back. So be honest — if it came out shaky, mark it shaky. Then go run a flashcard round.

找一道题Find one
按方向、掌握状态筛Filter by topic and mark

题目Questions

筛出 6 道。6 of 6 questions.
JavaScriptJavaScript#305

事件循环是怎么工作的

What does the event loop

看答案Show answer

一句话:JS只有一个主线程, 事件循环负责在「调用栈空了」的时候, 从任务队列里取下一个任务放上去执行。

完整的一轮(这段是标准答案):

  1. 执行完当前的同步代码(调用栈清空)。
  2. 把微任务队列全部清空—— 注意是「全部」,而且清微任务时新产生的微任务也在这一轮里执行完
  3. (浏览器)需要的话渲染一帧。
  4. 一个宏任务执行,回到第 2 步。

谁是微任务:Promise.then/catch/finallyawait 之后的代码、queueMicrotaskMutationObserver
谁是宏任务:setTimeout / setInterval、 DOM 事件回调、网络回调、requestAnimationFrame(严格说它在渲染前,单独一档)。

一句话记住优先级:Promise 一定比setTimeout 先跑, 即使 setTimeout(…, 0)

会追问:「异步是谁做的?」—— 不是引擎, 是宿主环境(浏览器的 Web API / Node 的 libuv)。引擎只管执行 JS。 这条和 #276 是一组。
setTimeout(fn, 0)真的 0 毫秒吗?」—— 不是,浏览器最小约 4ms, 而且要等主线程空闲。所以它只是「尽快,但不是现在」。

Node 的差别(问到就是加分): Node 的宏任务分了六个阶段 (timers / pending / poll / check / close…),setImmediate 在 check 阶段,process.nextTick比所有微任务都优先

In one line: JS has one main thread, and the event loop’s job is to pull the next task off a queue and put it on the stack whenever the call stack goes empty.

One full turn — this part is the model answer:

  1. Run the synchronous code to the end (the call stack empties).
  2. Drain the microtask queue completely — note “completely”: microtasks queued while draining also run inside this same turn.
  3. (In a browser) paint a frame if one is needed.
  4. Take one macrotask, run it, go back to step 2.

Microtasks: Promise.then/catch/finally, the code after an await, queueMicrotask, MutationObserver.
Macrotasks: setTimeout / setInterval, DOM event handlers, network callbacks, requestAnimationFrame (strictly it runs just before paint, in a class of its own).

The priority in one line: a Promise always runs before a setTimeout, even setTimeout(…, 0).

Follow-up: “Who actually does the async work?” — not the engine, the host environment (the browser’s Web APIs, libuv in Node). The engine only runs JS. This one pairs with #276.
“Is setTimeout(fn, 0) really zero milliseconds?” — no. Browsers clamp it to roughly 4ms, and it still waits for a free main thread. So it means “as soon as possible, but not now”.

How Node differs (a bonus point if it comes up): Node splits macrotasks into six phases (timers / pending / poll / check / close…), setImmediate lands in the check phase, and process.nextTick jumps ahead of every microtask.

JavaScript必须能推出来的那道题The question you have to be able to work out示意Illustrative
1console.log("1 同步");
2
3setTimeout(() => console.log("2 宏任务"), 0);
4
5Promise.resolve().then(() => console.log("3 微任务"));
6
7(async () => {
8 console.log("4 同步(await 之前是同步的)");
9 await null;
10 console.log("5 微任务(await 之后)");
11})();
12
13console.log("6 同步");
14
15// 输出:1 同步 -> 4 同步 -> 6 同步 -> 3 微任务 -> 5 微任务 -> 2 宏任务
16//
17// 关键两点:
18// · async 函数体在遇到第一个 await 之前是同步执行的
19// · await 之后的代码等价于 .then 里的代码,是微任务
1console.log("1 sync");
2
3setTimeout(() => console.log("2 macrotask"), 0);
4
5Promise.resolve().then(() => console.log("3 microtask"));
6
7(async () => {
8 console.log("4 sync (everything before await is sync)");
9 await null;
10 console.log("5 microtask (after await)");
11})();
12
13console.log("6 sync");
14
15// Output: 1 sync -> 4 sync -> 6 sync -> 3 microtask -> 5 microtask -> 2 macrotask
16//
17// The two key points:
18// · the body of an async function runs synchronously until the first await
19// · the code after await is the same as code inside .then, so it is a microtask
面试给的题基本是这个变体。抓住两条:同步先跑完;微任务在宏任务前,且一次清空。The question you get in an interview is almost always a variant of this one. Hold on to two rules: all synchronous code runs first; microtasks run before macrotasks, and the whole microtask queue is drained at once.
JavaScriptJavaScript#306

async/await vs Promise

Async/await vs Promise

看答案Show answer

一句话:async/await是 Promise 的语法糖—— 同一套机制,但把「链式回调」写成了 「像同步一样往下读」。async 函数 永远返回一个 Promise。

.then()async/await
可读性嵌套一深就难读线性,好读
错误处理.catch()普通 try/catch——和同步代码统一了
调试断点难打,栈信息乱能逐行断点,栈清楚
中间变量要靠嵌套或额外传参才能共享就是普通局部变量
并发天然并行(先建好再 all)容易写成串行—— 这是最常见的性能错误

那个「容易写成串行」的坑值得单独说: 两个互不依赖的请求, 写成两行 await就变成了「等第一个回来再发第二个」, 总耗时是两者之和。正确做法是先都发出去,再一起await Promise.all

会追问:「什么时候还是用 .then 更好?」—— 只需要一步、不需要中间变量时; 或者要故意不等(fire and forget)。 另外 .then在需要把 Promise 存起来传递时更自然。

In one line: async/await is syntax sugar over Promises — the same machinery, but a chain of callbacks now reads straight down like synchronous code. An async function always returns a Promise.

A .then() chainasync/await
ReadabilityHard to follow the moment it nestsLinear, easy to read
Error handling.catch()Plain try/catch the same as synchronous code
DebuggingBreakpoints are awkward, stacks are a messStep line by line, clean stacks
Intermediate valuesShared only by nesting or passing them alongJust ordinary local variables
ConcurrencyParallel by nature (build them, then all)Easy to make serial by accident — the most common performance mistake there is

That “serial by accident” trap is worth its own sentence: two requests that do not depend on each other, written as two await lines, turn into “wait for the first, then send the second”, so the total is the sum of both. Fire them both off first, then await Promise.all.

Follow-up: “When is .then still the better choice?” — for a single step with no intermediate values, or when you deliberately do not want to wait (fire and forget). .then also reads better when you are storing a Promise and passing it around.

JavaScriptasync/await 最常见的性能错误The most common performance mistake with async/await示意Illustrative
1// ✗ 串行:总耗时 = a + b
2const user = await fetchUser(); // 等 200ms
3const posts = await fetchPosts(); // 再等 200ms -> 共 400ms
4
5// ✓ 并行:总耗时 = max(a, b)
6const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
7// ↑ 两个请求同时发出去 -> 共 200ms
8
9// 有依赖时串行才是对的
10const user = await fetchUser();
11const posts = await fetchPosts(user.id); // 必须先有 user.id
1// ✗ One after the other: total time = a + b
2const user = await fetchUser(); // wait 200ms
3const posts = await fetchPosts(); // wait another 200ms -> 400ms in total
4
5// ✓ Side by side: total time = max(a, b)
6const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
7// ↑ both requests go out at once -> 200ms total
8
9// When one depends on the other, going one at a time is correct
10const user = await fetchUser();
11const posts = await fetchPosts(user.id); // user.id has to exist first
JavaScriptJavaScript#307

什么是回调地狱

What is callback hell

看答案Show answer

一句话:异步一步依赖上一步时, 回调套回调,缩进越来越深, 形成一个横着的三角形 —— 也叫「厄运金字塔」。

它真正的问题不只是难看,有三条:

  • 错误处理要写 n 遍。每一层都得判 if (err), 漏一个就静默失败。
  • 没法组合。想改成「这两步并行」几乎要重写。
  • 控制流全靠缩进表达, 加一步要改一堆括号。

怎么解,按历史顺序:命名函数拆平 → Promise 链 (把嵌套变成链式,错误集中到一个.catch)→async/await(彻底变线性)。

会追问:「Promise 解决了回调地狱的哪个问题?」——主要是「错误处理」和「组合」, 缩进只是顺带。 能这么答说明你理解本质, 而不是「Promise 让代码变平了」这种表面回答。

In one line: when each async step depends on the last, callbacks nest inside callbacks, the indentation keeps growing, and you end up with a sideways triangle — also called the pyramid of doom.

The real damage is not that it looks bad. There are three problems:

  • Error handling gets written n times. Every level needs its own if (err), and one missed check is a silent failure.
  • Nothing composes. Making two of those steps run in parallel means rewriting nearly all of it.
  • Control flow is expressed only by indentation, so inserting a step means shuffling a pile of braces.

The fixes, in historical order: pull the callbacks out into named functions → a Promise chain (nesting becomes chaining, errors collapse into one .catch) → async/await, which makes it fully linear.

Follow-up: “Which part of callback hell did Promises actually solve?” — mostly error handling and composition; the indentation came along for the ride. Answering that way shows you understand the substance, not just “Promises flatten the code”.

JavaScript同一段逻辑的两种写法The same logic written two ways示意Illustrative
1// 回调地狱
2getUser(id, (err, user) => {
3 if (err) return handle(err);
4 getPosts(user.id, (err, posts) => {
5 if (err) return handle(err); // 又来一遍
6 getComments(posts[0].id, (err, comments) => {
7 if (err) return handle(err); // 再来一遍
8 render(comments);
9 });
10 });
11});
12
13// async/await
14try {
15 const user = await getUser(id);
16 const posts = await getPosts(user.id);
17 const comments = await getComments(posts[0].id);
18 render(comments);
19} catch (err) {
20 handle(err); // 一处兜住全部
21}
1// Callback hell
2getUser(id, (err, user) => {
3 if (err) return handle(err);
4 getPosts(user.id, (err, posts) => {
5 if (err) return handle(err); // the same line again
6 getComments(posts[0].id, (err, comments) => {
7 if (err) return handle(err); // and once more
8 render(comments);
9 });
10 });
11});
12
13// async/await
14try {
15 const user = await getUser(id);
16 const posts = await getPosts(user.id);
17 const comments = await getComments(posts[0].id);
18 render(comments);
19} catch (err) {
20 handle(err); // one place catches all of them
21}
JavaScriptJavaScript#309

Promise 链里的 finally() 有什么用

What is the purpose of the finally() method in a Promise chain

看答案Show answer

一句话:无论成功还是失败都会执行, 用来做收尾 —— 关 loading、 释放资源、上报耗时。

三个性质要说清:

  • 拿不到值也拿不到错误—— 回调不接参数。它的定位就是「不关心结果的清理」。
  • 透传—— 它把原来的值或错误原样往下传, 不影响链的状态。 所以 finallyreturn一个值不会改变结果
  • 但它里面抛错会覆盖原来的结果—— 这是唯一能改变链状态的方式。

为什么这题值得问:因为它对应一个 真实 bug —— 只在成功路径里setLoading(false), 出错时界面就永远卡在 Loading。我们那道 fetch 变式题的常见错误里就有这一条。

会追问:「和 try/catch/finallyfinally 一样吗?」—— 语义一样,都是「一定执行」。async/await 里直接用try/finally 就行,不用.finally()

In one line: it runs whether the promise succeeded or failed, so it is where the cleanup goes — turn off loading, release resources, report how long it took.

Three properties to state clearly:

  • It sees neither the value nor the error — the callback takes no arguments. Its whole job is cleanup that does not care about the result.
  • It passes through — the original value or error continues down the chain unchanged, so the state of the chain is untouched. A return inside finally changes nothing.
  • But throwing inside it does override the result — that is the only way it can change the chain.

Why this question earns its place: it maps onto a real bug — call setLoading(false) only on the success path and the UI sits on Loading forever when the request fails. That exact mistake is on the common-errors list for our fetch variant question.

Follow-up: “Is it the same as the finally in try/catch/finally?” — same meaning: it always runs. With async/await just use try/finally; you do not need .finally().

JavaScriptfinally 的三个性质Three properties of finally示意Illustrative
1fetch(url)
2 .then((r) => r.json())
3 .then(setData)
4 .catch(setError)
5 .finally(() => setLoading(false)); // 成功失败都要关 loading
6
7// 等价的 async 写法
8try {
9 const r = await fetch(url);
10 setData(await r.json());
11} catch (e) {
12 setError(e.message);
13} finally {
14 setLoading(false); // ← 放这里,别只放在 try 末尾
15}
16
17// finally 透传,return 不生效
18Promise.resolve(1).finally(() => 99).then(console.log); // 1,不是 99
19// 但抛错会覆盖
20Promise.resolve(1).finally(() => { throw new Error("x"); }).catch(e => console.log(e.message)); // "x"
1fetch(url)
2 .then((r) => r.json())
3 .then(setData)
4 .catch(setError)
5 .finally(() => setLoading(false)); // turn loading off whether it worked or not
6
7// The same thing written with async
8try {
9 const r = await fetch(url);
10 setData(await r.json());
11} catch (e) {
12 setError(e.message);
13} finally {
14 setLoading(false); // ← put it here, not only at the end of try
15}
16
17// finally passes the value through; its return value is ignored
18Promise.resolve(1).finally(() => 99).then(console.log); // 1, not 99
19// But throwing does replace it
20Promise.resolve(1).finally(() => { throw new Error("x"); }).catch(e => console.log(e.message)); // "x"
JavaScriptJavaScript#310

错误处理怎么做

Error Handling

看答案Show answer

一句话:同步用try/catch, Promise 用 .catch()async/awaittry/catch最上层要有兜底

最重要的一条:try/catch抓不到「回调里」的异步错误。因为 setTimeout的回调是在另一轮事件循环里执行的, 那时 try 块早就出栈了。

四个层次的实践:

  • 该抛就抛—— 别把错误吞掉换成return null, 调用方无法区分「没有」和「出错了」。
  • Error 对象, 别抛字符串 —— 否则没有堆栈。 需要区分类型就class NotFoundError extends Error
  • 只在能处理的地方 catch。 catch 了却什么都不做(catch (e) {}) 是最坏的写法。
  • 兜底—— 浏览器 window.onerror +unhandledrejection; Node process.on("uncaughtException"); React 用错误边界(见 #333); Express 用错误中间件

会追问:「fetch 的错误怎么处理?」——陷阱题fetch 只在网络层失败时 reject,404 / 500 是 resolve 的, 必须自己检查 res.ok。 这也是 React 那门课里 fetch 变式题的第一个考点。

In one line: try/catch for synchronous code, .catch() for Promises, try/catch again for async/await; and always keep a backstop at the very top.

The most important point: try/catch cannot catch an async error thrown inside a callback. A setTimeout callback runs in a later turn of the event loop, and by then the try block is long off the stack.

Four levels of practice:

  • Throw when you should throw — do not swallow the error and hand back return null; the caller then cannot tell “not there” from “it broke”.
  • Throw an Error object, never a string — a string carries no stack. When callers need to tell cases apart, write class NotFoundError extends Error.
  • Only catch where you can actually handle it. Catching and then doing nothing (catch (e) {}) is the worst thing you can write.
  • Keep a backstop — in the browser window.onerror plus unhandledrejection; in Node process.on("uncaughtException"); in React an error boundary (see #333); in Express error-handling middleware.

Follow-up: “How do you handle errors from fetch?” — this is a trick question. fetch only rejects when the network layer fails; 404 and 500 both resolve, so you have to check res.ok yourself. That is also the first thing the fetch variant question in the React course tests.

JavaScript四条实践Four practices示意Illustrative
1// ✗ 抓不到 —— 回调在下一轮事件循环里跑
2try {
3 setTimeout(() => { throw new Error("炸了"); }, 0);
4} catch (e) {
5 console.log("抓不到这里");
6}
7
8// ✓ 异步错误要在异步链里抓
9try {
10 await somethingAsync();
11} catch (e) { /* ✓ */ }
12
13// ✗ 吞掉错误,调用方分不清「没有」还是「出错」
14async function getUser(id) {
15 try { return await api.get(id); }
16 catch { return null; }
17}
18
19// ✓ 让它抛,或者抛一个带类型的错误
20class NotFoundError extends Error {}
21if (!row) throw new NotFoundError(`user ${id} 不存在`);
22
23// 兜底
24window.addEventListener("unhandledrejection", (e) => report(e.reason));
1// ✗ Never caught —— the callback runs in a later turn of the event loop
2try {
3 setTimeout(() => { throw new Error("failed"); }, 0);
4} catch (e) {
5 console.log("this line is never reached");
6}
7
8// ✓ Catch an async error inside the async chain
9try {
10 await somethingAsync();
11} catch (e) { /* ✓ */ }
12
13// ✗ Swallowing the error: the caller cannot tell "not there" from "it failed"
14async function getUser(id) {
15 try { return await api.get(id); }
16 catch { return null; }
17}
18
19// ✓ Let it throw, or throw an error that has a type
20class NotFoundError extends Error {}
21if (!row) throw new NotFoundError(`user ${id} not found`);
22
23// A last line of defence
24window.addEventListener("unhandledrejection", (e) => report(e.reason));
JavaScriptJavaScript#311

怎么处理异步操作

Handle asynchronous operations

看答案Show answer

一句话(按历史讲最清楚):回调 → Promise → async/await, 外加事件和 for await…of处理流式数据。

但这题真正的考点是 Promise 的四个静态方法怎么选, 一定会追问:

方法什么时候 resolve用在哪
all全部成功才成功,一个失败立刻失败几个都必须成功(页面必需的多个接口)
allSettled全部结束就成功, 不管成败批量操作,要知道每一个的结果 (批量上传,报告哪几个失败了)
race第一个结束的说话, 成功失败都算超时控制
any第一个成功的, 全失败才失败多个镜像源取最快能用的那个

all 的坑: 它是 fail-fast 的 —— 一个失败,其他已经在飞的请求不会被取消, 而且你拿不到其他的结果。 「批量操作要逐个报告」的场景该用allSettled

会追问:「怎么给一个请求加超时?」——Promise.race配一个定时 reject 的 Promise; 更好的是用 AbortController, 因为它能真的把请求掐掉race 只是不等了。 这两个的区别在我们那道 fetch 变式题里也讲过。

In one line, and history tells it best: callbacks → Promises → async/await, plus events and for await…of for streaming data.

But what this question is really after is how you pick among the four static Promise methods, and they will ask:

MethodWhen it resolvesWhere you use it
allSucceeds only if all of them succeed, and fails the instant one failsEverything must succeed (several calls the page needs)
allSettledSucceeds once everything has finished, win or loseBulk work where you need each result (a batch upload, reporting which ones failed)
raceWhoever finishes first speaks, success or failureTimeouts
anyThe first success; it fails only if all failSeveral mirrors, take the fastest one that works

The trap in all: it is fail-fast — one rejection and the other requests already in flight are not cancelled, and you never see their results. When bulk work has to report item by item, reach for allSettled.

Follow-up: “How do you put a timeout on a request?” — Promise.race against a Promise that rejects on a timer. Better is AbortController, because it actually kills the request, whereas race merely stops waiting. We walk through that difference in the fetch variant question too.

JavaScript四个方法与超时The four methods, and timeouts示意Illustrative
1// 都必须成功
2const [user, posts] = await Promise.all([getUser(), getPosts()]);
3
4// 要逐个知道结果
5const results = await Promise.allSettled(files.map(upload));
6const failed = results.filter((r) => r.status === "rejected");
7
8// 超时:race 只是「不等了」,请求还在飞
9const withTimeout = (p, ms) =>
10 Promise.race([
11 p,
12 new Promise((_, rej) => setTimeout(() => rej(new Error("超时")), ms)),
13 ]);
14
15// 更好:AbortController 真的掐掉请求
16const c = new AbortController();
17setTimeout(() => c.abort(), 5000);
18await fetch(url, { signal: c.signal });
1// All of them have to succeed
2const [user, posts] = await Promise.all([getUser(), getPosts()]);
3
4// You need the result of each one
5const results = await Promise.allSettled(files.map(upload));
6const failed = results.filter((r) => r.status === "rejected");
7
8// Timeout: race only means "stop waiting"; the request is still in flight
9const withTimeout = (p, ms) =>
10 Promise.race([
11 p,
12 new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), ms)),
13 ]);
14
15// Better: AbortController really does cancel the request
16const c = new AbortController();
17setTimeout(() => c.abort(), 5000);
18await fetch(url, { signal: c.signal });

这些题从哪来Where these come from

99 道来自面试题库 #269–#387;TypeScript 深度那 6 道是 DrillLab 自出的(senior 补强,卡片上有标注)。答案都是 DrillLab 写的,所以讲解里的代码块一律标「示意」。每道题都能点回它出处的那一节课。99 questions come from the interview bank (#269–#387); the 6 TypeScript deep-dive ones are DrillLab-made (marked on the card). All answers are written by DrillLab, so every code block here is labelled “demo”. Each card links back to the lesson it came from.