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.

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.

题目Questions

筛出 105 道 · 第 4 / 9 页。105 of 105 questions · page 4 / 9.
JavaScriptJavaScript#303

this 指向什么

What does 'this' refer to

看答案Show answer

一句话:this调用时决定的, 不是定义时。看「谁调用的」

四条规则,按优先级从高到低—— 这个顺序是标准答案:

  1. new 绑定——new Foo()this 是新创建的对象。
  2. 显式绑定——call / apply /bind 指定的那个。
  3. 隐式绑定——obj.fn()thisobj看点号左边)。
  4. 默认绑定—— 都不满足时, 严格模式下是 undefined, 否则是 window / global

箭头函数是例外,它不参与这四条—— 它没有自己的 this, 用的是定义时外层作用域的 this, 而且 call / bind改不了它

最经典的坑:隐式丢失。const fn = obj.method 之后单独调fn(),点号没了,this 就丢了。 把方法当回调传出去(setTimeout(obj.method)onClick={this.handle}) 都是这个问题 ——这就是 React 类组件必须在构造器里bind 的原因

会追问:「DOM 事件回调里 this 是什么?」—— 普通函数是绑定事件的那个元素(等于 e.currentTarget), 箭头函数则是外层的 this

In one line: this is decided when the function is called, not where it was written. Look at who called it.

Four rules, highest priority first — this order is the model answer:

  1. new binding — with new Foo(), this is the object that was just created.
  2. Explicit binding — whatever you handed to call / apply / bind.
  3. Implicit bindingobj.fn() makes this the obj (look left of the dot).
  4. Default binding — when none of the above applies: undefined in strict mode, otherwise window / global.

Arrow functions are the exception; they do not play by those four rules — an arrow has no this of its own, it uses the this of the scope it was defined in, and call / bind cannot change that.

The classic trap: the implicit binding gets lost. Do const fn = obj.method and then call fn() on its own — the dot is gone, so this is gone. Handing a method off as a callback (setTimeout(obj.method), onClick={this.handle}) is the same bug — and it is exactly why React class components had to bind in the constructor.

Follow-up: “What is this inside a DOM event handler?” — in a normal function it is the element the listener is attached to (the same as e.currentTarget); in an arrow function it is the outer this.

JavaScript四条规则与隐式丢失The four rules, and losing the implicit binding示意Illustrative
1const obj = {
2 name: "obj",
3 show() { console.log(this.name); },
4};
5
6obj.show(); // "obj" 隐式绑定,看点号左边
7const f = obj.show;
8f(); // undefined 隐式丢失
9f.call({ name: "call" }); // "call" 显式绑定
10setTimeout(obj.show, 0); // undefined 传出去就丢了
11setTimeout(() => obj.show(), 0); // "obj" 包一层就保住了
12
13// 箭头函数不参与规则,call 也改不了
14const arrow = () => console.log(this);
15arrow.call({ a: 1 }); // 还是外层的 this
16
17// React 类组件为什么要 bind
18class Btn extends React.Component {
19 constructor(p) {
20 super(p);
21 this.handle = this.handle.bind(this); // 不 bind,onClick 里 this 就是 undefined
22 }
23 handle() { console.log(this.props); }
24}
1const obj = {
2 name: "obj",
3 show() { console.log(this.name); },
4};
5
6obj.show(); // "obj" implicit binding: look left of the dot
7const f = obj.show;
8f(); // undefined the implicit binding is lost
9f.call({ name: "call" }); // "call" explicit binding
10setTimeout(obj.show, 0); // undefined pass it out and the binding is gone
11setTimeout(() => obj.show(), 0); // "obj" one wrapper keeps it
12
13// An arrow function ignores these rules, and call cannot change it either
14const arrow = () => console.log(this);
15arrow.call({ a: 1 }); // still the outer this
16
17// Why a React class component needs bind
18class Btn extends React.Component {
19 constructor(p) {
20 super(p);
21 this.handle = this.handle.bind(this); // without bind, this is undefined in onClick
22 }
23 handle() { console.log(this.props); }
24}
JavaScriptJavaScript#304

call、apply、bind 的区别

What are the differences between call, apply & bind

看答案Show answer

一句话:三个都是改thiscallapply立即执行,bind 返回一个新函数call 参数一个个传,apply 传数组。

是否立即执行参数形式
fn.call(ctx, a, b)立即逐个(Comma)
fn.apply(ctx, [a, b])立即数组(Array)
fn.bind(ctx, a)不执行,返回新函数逐个,且可以只绑一部分

记法:Apply 收 Array,Call 用 Comma。

bind 的两个额外性质(追问点):

  • 能预置参数—— 所以它就是偏函数 (见 #299)。
  • 绑过一次就锁死了—— 再 bindcall都改不回来。new 能突破它, 因为 new 优先级最高。

还会追问:apply 现在还有用吗?」—— 展开语法出来后大部分被fn(...args) 取代了。但转发不定参数时还常用fn.apply(this, args)(防抖里就是这么写的,因为要同时转发thisargs)。

In one line: all three change this. call and apply run the function right away, bind hands you back a new one; call takes its arguments one by one, apply takes an array.

Runs immediately?Argument form
fn.call(ctx, a, b)YesOne by one (Comma)
fn.apply(ctx, [a, b])YesAn array (Array)
fn.bind(ctx, a)No — you get a new functionOne by one, and you may bind only some of them

How to remember it: Apply takes an Array, Call takes Commas.

Two extra properties of bind they will probe:

  • It can preset arguments — which makes it partial application (see #299).
  • Bind once and it is locked — another bind or a call cannot change it back. But new breaks through, because new has the highest priority.

Another follow-up: “Is apply still useful?” — spread syntax replaced most of it with fn(...args). It is still the normal way to forward an unknown argument list: fn.apply(this, args) — that is how debounce is written, because you have to forward this and args at the same time.

JavaScript三者对比与手写 bindThe three compared, and bind written by hand示意Illustrative
1function greet(greeting, mark) {
2 return `${greeting}, ${this.name}${mark}`;
3}
4const who = { name: "小明" };
5
6greet.call(who, "你好", "!"); // "你好, 小明!"
7greet.apply(who, ["你好", "!"]); // 同上,参数是数组
8const hi = greet.bind(who, "你好"); // 返回新函数,还预置了第一个参数
9hi("?"); // "你好, 小明?"
10
11// 手写一个 bind(高频现场题)
12Function.prototype.myBind = function (ctx, ...preset) {
13 const fn = this;
14 return function bound(...args) {
15 // new 调用时 this 是新对象,此时不该用 ctx —— 这是 bind 的规范行为
16 const isNew = this instanceof bound;
17 return fn.apply(isNew ? this : ctx, [...preset, ...args]);
18 };
19};
1function greet(greeting, mark) {
2 return `${greeting}, ${this.name}${mark}`;
3}
4const who = { name: "小明" };
5
6greet.call(who, "你好", "!"); // the greeting, then the name, then the mark
7greet.apply(who, ["你好", "!"]); // the same, but the arguments arrive as an array
8const hi = greet.bind(who, "你好"); // returns a new function with the first argument preset
9hi("?"); // the same string, ending in a question mark
10
11// Write bind by hand (a very common live-coding question)
12Function.prototype.myBind = function (ctx, ...preset) {
13 const fn = this;
14 return function bound(...args) {
15 // With new, this is the new object and ctx must be ignored —— the spec requires it
16 const isNew = this instanceof bound;
17 return fn.apply(isNew ? this : ctx, [...preset, ...args]);
18 };
19};
手写 bind 的加分项就是那个 isNew 判断:规范规定 new 一个 bound 函数时,绑定的 this 应该被忽略。多数人会漏。What earns extra credit when you write bind by hand is that isNew check: the spec says that when a bound function is called with new, the bound this must be ignored. Most people leave it out.
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 });
JavaScriptJavaScript#288

什么是 DOM,什么是 DOM 事件

What is the DOM and what is DOM event

看答案Show answer

一句话:DOM 是浏览器把 HTML 解析成的一棵对象树, 每个标签是一个节点; DOM 事件是这棵树上发生的事情 (点击、输入、加载完成), 你可以注册函数去响应。

关键概念要点清:DOM 不是 HTML 本身, 也不属于 JavaScript 语言—— 它是浏览器提供的 API(Web API)。 所以 Node 里没有 document。 这条和 #276 是一组。

为什么「操作 DOM 慢」:不是读写属性本身慢,而是它可能触发重排(reflow)和重绘(repaint)—— 浏览器要重新计算布局、重新画。 在循环里反复读 offsetHeight再改样式,会造成强制同步布局(layout thrashing), 这才是真正的性能杀手。

这直接解释了虚拟 DOM 的价值(见 #330): 它把多次操作合并成一次, 并且尽量只改变化的部分。

会追问:「事件对象上 targetcurrentTarget 什么区别?」——target真正被点的那个元素currentTarget当前监听器挂在哪个元素上事件委托全靠这个区别, 下一题就是。

In one line: the DOM is the tree of objects the browser builds when it parses your HTML — one node per tag. A DOM event is something that happens on that tree (a click, some typing, a load finishing), and you register functions to respond to it.

Be precise about the key point: the DOM is not the HTML itself, and it is not part of the JavaScript language — it is an API the browser hands you (a Web API). That is why Node has no document. This one pairs with #276.

Why “touching the DOM is slow”: reading and writing a property is not the slow part. The cost is that it can trigger reflow and repaint — the browser has to recompute layout and paint again. Reading offsetHeight and then changing a style, over and over inside a loop, causes layout thrashing, and that is the real performance killer.

This is exactly what makes the virtual DOM worth something (see #330): it batches many operations into one and tries to touch only what changed.

Follow-up: “What is the difference between target and currentTarget on the event object?” — target is the element that was actually clicked, currentTarget is the element this listener is attached to. Event delegation rides entirely on that difference, which is the next question.

JavaScriptJavaScript#289

事件传播 vs 事件委托

Event propagation vs Event delegation

看答案Show answer

一句话:传播是浏览器的机制(捕获 → 目标 → 冒泡,见 #380);委托是我们利用这个机制的技巧—— 把监听器挂在父元素上, 通过 e.target 判断实际点了哪个子元素。

委托解决两个问题:

  • 监听器数量—— 1000 行的表格挂 1000 个监听器, 内存和绑定开销都很可观;委托只要 1 个。
  • 动态元素—— 后来才插进来的子元素自动就有了行为, 不用重新绑定。这一条往往更实用。

写法的关键是 e.target.closest()—— 因为用户可能点在按钮里的<span> 上, 直接比 e.target.matches() 会漏。

会追问(重点):「React 的事件是委托的吗?」——,而且这是它的核心设计: React 把事件统一挂在根容器上 (React 17 之前挂在 document, 17 之后挂到 root 节点,这是为了支持一个页面里多个 React 版本共存), 然后用合成事件(SyntheticEvent)模拟一套跨浏览器一致的事件系统。
推论:所以在 React 里e.stopPropagation()拦得住 React 组件之间的传播, 但拦不住原生监听器—— 因为原生的已经先跑完了。 这个点答出来会很加分。

In one line: propagation is the browser’s mechanism (capture → target → bubble, see #380); delegation is the trick we play with it — put the listener on the parent and use e.target to work out which child was really clicked.

Delegation solves two problems:

  • The number of listeners — a 1000-row table with 1000 listeners costs real memory and real binding time; delegation needs one.
  • Dynamic elements — children inserted later already have the behaviour, with nothing to rebind. In practice this is often the bigger win.

The key to writing it is e.target.closest() — the user may have clicked a <span> inside the button, so a bare e.target.matches() misses it.

Follow-up, and this is the one that matters: “Are React events delegated?” — yes, and it is central to the design: React attaches events to the root container (to document before React 17, to the root node from 17 onwards, so that several React versions can coexist on one page), then wraps them in a SyntheticEvent to present one event system that behaves the same across browsers.
The consequence: inside React, e.stopPropagation() does stop propagation between React components, but it cannot stop a native listener — the native one already ran. Landing this point scores well.

JavaScript事件委托的标准写法The standard way to write event delegation示意Illustrative
1// ✗ 每一行一个监听器,而且新增的行没有行为
2document.querySelectorAll("tr .del").forEach((btn) =>
3 btn.addEventListener("click", onDelete),
4);
5
6// ✓ 委托:一个监听器,新增的行自动有行为
7table.addEventListener("click", (e) => {
8 // closest 而不是 matches —— 用户可能点在按钮里的图标上
9 const btn = e.target.closest(".del");
10 if (!btn) return; // 点到空白处,直接退出
11 onDelete(btn.dataset.id);
12});
13
14// target vs currentTarget
15// e.target = 真正被点的元素(可能是按钮里的 span)
16// e.currentTarget = 监听器挂在哪(这里永远是 table)
1// ✗ One listener per row, and rows added later have no behaviour
2document.querySelectorAll("tr .del").forEach((btn) =>
3 btn.addEventListener("click", onDelete),
4);
5
6// ✓ Delegation: one listener, and new rows behave correctly on their own
7table.addEventListener("click", (e) => {
8 // closest, not matches —— the user may click the icon inside the button
9 const btn = e.target.closest(".del");
10 if (!btn) return; // a click on empty space just returns
11 onDelete(btn.dataset.id);
12});
13
14// target vs currentTarget
15// e.target = the element actually clicked (maybe a span inside the button)
16// e.currentTarget = where the listener is attached (always the table here)
JavaScriptJavaScript#301

ES6 有哪些新特性

Name the new ES6 features

看答案Show answer

一句话:2015 年那一版改动最大, 十来个东西今天天天在用

按重要性排(面试挑五六个说清就够, 别背清单):

  • let / const—— 块作用域(#282)
  • 箭头函数—— 简写 + 词法 this(#285)
  • 模板字符串——`${x}`,支持多行
  • 解构——const { a, b } = objReact 里到处在用
  • 展开 / 剩余——...不可变更新的基础
  • 默认参数
  • Promise—— 异步的转折点(#306)
  • class—— 原型的语法糖(#302)
  • ES 模块——import / export(#308)
  • Map / Set(#286、#287)、Symbolfor…of 与迭代器、生成器

会追问:「ES6 之后还有什么好用的?」—— 这题答得出来会显得你在跟进:async/await(ES2017)、 可选链 ?. 和空值合并??(ES2020)、Object.entries(ES2017)、Array.flat(ES2019)、at(-1)(ES2022)、structuredClone
可选链和 ??这两个尤其值得提, 因为它们直接减少了大量防御式代码。

In one line: the 2015 edition changed the most, and a dozen or so of its additions are things you use every single day.

Ordered by weight — pick five or six and explain them properly, do not recite the list:

  • let / const — block scope (#282)
  • Arrow functions — shorter, plus a lexical this (#285)
  • Template literals`${x}`, and they span lines
  • Destructuring const { a, b } = obj, used everywhere in React
  • Spread and rest..., the basis of immutable updates
  • Default parameters
  • Promise — the turning point for async (#306)
  • class — sugar over prototypes (#302)
  • ES modulesimport / export (#308)
  • Map / Set (#286, #287), Symbol, for…of with iterators, generators

Follow-up: “What came after ES6 that you like?” — answering this makes you look like you keep up: async/await (ES2017), optional chaining ?. and nullish coalescing ?? (ES2020), Object.entries (ES2017), Array.flat (ES2019), at(-1) (ES2022), structuredClone.
Optional chaining and ?? are the two most worth naming, because they cut out a mountain of defensive code.

JavaScriptJavaScript#308

什么是 ES6 模块

What are ES6 modules

看答案Show answer

一句话:语言内置的模块系统 ——export 导出、import 导入, 每个文件一个作用域,在编译期就能确定依赖关系

和 CommonJS(Node 的老方案)的差别—— 这才是考点:

CommonJSES Module
语法require / module.exportsimport / export
时机运行时加载编译期确定依赖
能否动态路径能(require(x)静态 import 不能;要动态用import()(返回 Promise)
导出的是值的拷贝活的绑定(原值变了这边也变)
Tree shaking不行可以(因为静态可分析)
顶层 await不支持支持

「编译期确定依赖」为什么重要?因为打包工具能在不运行代码的情况下 知道谁用了什么, 于是能删掉没用到的导出(tree shaking)、 能做代码分割。这是 ESM 最大的实际价值, 不是语法更好看。

会追问:「具名导出和默认导出怎么选?」—— 默认导出对重命名没有约束 (import 时可以叫任何名字), 不利于搜索和自动补全;具名导出更利于 tree shaking 和重构。 实践上「一个文件一个主体」用 default, 工具函数集合用具名。
「Node 里怎么用 ESM?」——package.json"type": "module", 或者文件名用 .mjs。 这就是 Federation 那门课里 Jest 需要--experimental-vm-modules 的原因。

In one line: the module system built into the language — export to expose, import to pull in, one scope per file, and dependencies are known at compile time.

How it differs from CommonJS, Node’s older scheme — this is the part being tested:

CommonJSES Module
Syntaxrequire / module.exportsimport / export
TimingLoaded at runtimeDependencies resolved at compile time
Dynamic pathsYes (require(x))Not with a static import; for that use import(), which returns a Promise
What gets exportedA copy of the valueA live binding (the original changes, so does this one)
Tree shakingNoYes, because it is statically analysable
Top-level awaitNot supportedSupported

Why does “resolved at compile time” matter? Because a bundler can see who uses what without running the code, so it can drop exports nobody imported (tree shaking) and split code. That is ESM’s real practical value, not prettier syntax.

Follow-up: “Named exports or a default export?” — a default export puts no constraint on the name, since an importer can call it anything, which hurts search and autocomplete; named exports are friendlier to tree shaking and refactoring. In practice: default for “one file, one main thing”, named for a bag of utilities.
“How do you use ESM in Node?” — "type": "module" in package.json, or name the file .mjs. That is why Jest needs --experimental-vm-modules in the Federation course.

这些题从哪来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.