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 道 · 第 8 / 9 页。105 of 105 questions · page 8 / 9.
React 与生态React & ecosystem#354

解释一下 Redux 中间件

explain Redux Middleware

看答案Show answer

一句话:中间件是夹在「派发 action」和 「reducer 收到 action」之间的一层, 能拦截、改写、延迟、 甚至吞掉一个 action。

签名是三层柯里化—— 这个形状本身常被问到:store => next => action => {}。 多个中间件靠 next串成一条链,和 Express 的中间件是同一个模式

为什么需要它:因为reducer 必须是纯的, 所以异步和副作用无处安放。 中间件就是专门给副作用留的位置

常见的几个:

  • redux-thunk—— 让你能 dispatch 一个函数而不只是对象, 在里面做异步。最简单,RTK 默认装。
  • redux-saga—— 用 generator 描述复杂异步流程 (可取消、可重试、能编排多个请求)。 能力强但学习成本高。
  • redux-logger—— 打印每个 action 前后的 state。

会追问:「thunk 和 saga 怎么选?」—— 大部分项目 thunk 够了;只有在需要「取消、去抖、 复杂的流程编排」时 saga 才值那份复杂度
「能自己写一个吗?」—— 能,而且面试常让手写一个 logger。

In one line: middleware is a layer between “an action is dispatched” and “the reducer receives it”. It can intercept, rewrite, delay or even swallow an action.

The signature is curried three levels deep — the shape itself gets asked about: store => next => action => {}. Several middlewares chain together through next, and it is the same pattern as Express middleware.

Why you need it: because the reducer has to be pure, async work and side effects have nowhere to live. Middleware is the place reserved for side effects.

The common ones:

  • redux-thunk — lets you dispatch a function instead of only an object, and do your async work inside it. Simplest option, and RTK installs it by default.
  • redux-saga — describes complex async flows with generators (cancellable, retryable, able to orchestrate several requests). Powerful, but a steep learning curve.
  • redux-logger — prints the state before and after each action.

Follow-up: “thunk or saga?” — thunk is enough for most projects; saga only earns its complexity when you need cancellation, debouncing or real flow orchestration.
“Could you write one?” — yes, and interviewers often ask you to write a logger on the spot.

JavaScript中间件的形状与 thunk示意Illustrative
1// 手写一个 logger 中间件:注意那三层箭头
2const logger = (store) => (next) => (action) => {
3 console.log("派发:", action.type, action.payload);
4 const result = next(action); // 交给下一个中间件 / reducer
5 console.log("新状态:", store.getState());
6 return result;
7};
8
9// thunk 让 dispatch 能收函数
10const fetchUser = (id) => async (dispatch) => {
11 dispatch({ type: "user/loading" });
12 try {
13 const res = await fetch(`/api/users/${id}`);
14 if (!res.ok) throw new Error(`HTTP ${res.status}`); // 别忘了这一句
15 dispatch({ type: "user/loaded", payload: await res.json() });
16 } catch (e) {
17 dispatch({ type: "user/failed", payload: e.message });
18 }
19};
1// Writing a logger middleware yourself: note the three levels of arrows
2const logger = (store) => (next) => (action) => {
3 console.log("dispatching:", action.type, action.payload);
4 const result = next(action); // hand it to the next middleware or the reducer
5 console.log("new state:", store.getState());
6 return result;
7};
8
9// thunk lets dispatch accept a function
10const fetchUser = (id) => async (dispatch) => {
11 dispatch({ type: "user/loading" });
12 try {
13 const res = await fetch(`/api/users/${id}`);
14 if (!res.ok) throw new Error(`HTTP ${res.status}`); // do not forget this line
15 dispatch({ type: "user/loaded", payload: await res.json() });
16 } catch (e) {
17 dispatch({ type: "user/failed", payload: e.message });
18 }
19};
React 与生态React & ecosystem#355

JavaScript vs TypeScript

Javascript vs TypeScript

看答案Show answer

一句话:TS 是 JS 的超集—— 加了静态类型,编译后就是普通 JS, 运行时没有任何 TS 的东西

JavaScriptTypeScript
类型检查运行时才炸编译期就报
需要构建不需要需要(tsc / esbuild / SWC)
IDE 支持靠猜精确补全、跳转、重命名
重构靠搜字符串改一处,所有不兼容的地方都报出来

「运行时没有 TS」这句要强调, 因为它推出两个重要结论:

  • 类型不能用来做运行时校验。接口返回的数据是不是真的符合你写的interface, TS 管不了—— 要校验得用 zod 这类库。这是新手最大的误解。
  • as 断言只是「我保证」, 不做任何检查。滥用 asany 等于关掉了 TS。

代价(要主动说):多一步构建、 有学习成本(泛型、 条件类型、unknown vs any)、 第三方库缺类型时要自己写声明、 复杂类型报错很难读。

会追问:interfacetype 选哪个?」——interface能被重复声明合并、 更适合描述对象和 class 契约;type 能写联合、 交叉、映射、条件类型,能力更全。 实践上「对象形状用 interface, 其他用 type」,但团队统一比选哪个更重要

In one line: TS is a superset of JS — it adds static types, and after compilation it is ordinary JS with nothing of TS left at runtime.

JavaScriptTypeScript
Type checkingFails at runtimeReported at compile time
Build stepNot neededNeeded (tsc / esbuild / SWC)
IDE supportGuessworkPrecise completion, go-to-definition, rename
RefactoringSearch for stringsChange one place and every incompatible use lights up

Stress the “nothing of TS at runtime” line, because two important conclusions follow from it:

  • Types cannot validate anything at runtime. Whether the data an API returns really matches the interface you wrote is beyond TS — for that you need something like zod. This is the biggest beginner misconception.
  • An as assertion is just “trust me” and checks nothing. Overusing as and any is the same as turning TS off.

The costs — bring them up yourself: an extra build step, a learning curve (generics, conditional types, unknown vs any), writing your own declarations when a library ships none, and error messages for complex types that are hard to read.

Follow-up:interface or type?” — interface can be declared again and merged and suits object and class contracts; type can do unions, intersections, mapped and conditional types, so it is more capable. In practice: “interface for object shapes, type for everything else” — but a consistent team choice matters more than which one you pick.

React 与生态React & ecosystem#356

什么是静态类型检查,有什么好处

What is static type checking and how can developers benefit from it

看答案Show answer

一句话:不运行代码,只靠分析源码就找出类型不匹配的地方。 「静态」的意思就是「在编译期,而非运行期」。

四个具体收益(要给例子,别空谈):

  • 错误提前——user.nmae 拼错、 忘了处理 null、 给函数传少了参数,在编辑器里就红了, 而不是上线后用户报给你
  • 类型即文档—— 函数签名说明了它要什么、给什么。而且这份文档不会过期, 因为改了代码不改类型就编译不过。
  • 重构有底气—— 改一个字段名, 所有受影响的地方都会报错。这是 TS 最被低估的价值, 在大项目里比「防 bug」更实用。
  • IDE 能力—— 精确补全、跳定义、 安全重命名。

局限(说出来才显得懂):它只保证「类型对」,不保证「逻辑对」—— 类型全过的代码照样能算错工资。 而且它管不到运行时的外部数据(见 #355),所以类型检查不能替代测试

会追问:strict 模式开不开?」——新项目一定开。 最有价值的是strictNullChecks—— 它把「忘了判空」这一整类 运行时错误变成编译错误。
顺带一个真实例子:React 那门课的源项目npm run build 就是因为tsc 报了 10 个错误而失败的 (测试文件缺 vitest 全局类型)—— 这说明类型检查是构建的一部分, 不是可选的 lint

In one line: without running the code, purely by analysing the source, it finds places where the types do not line up. “Static” just means “at compile time, not at run time”.

Four concrete benefits — give examples, do not speak in the abstract:

  • Errors surface earlier — a typo like user.nmae, a forgotten null case, a missing argument: they go red in the editor instead of arriving as a user report after release.
  • Types are documentation — a signature says what it wants and what it gives back. And this documentation cannot go stale, because changing the code without changing the types fails the build.
  • Refactoring with confidence — rename one field and every affected place errors. This is TS’s most underrated value, and on a large codebase it is more useful than bug prevention.
  • Editor power — accurate completion, jump to definition, safe rename.

The limits — saying them is what shows you get it: it only guarantees the types are right, not that the logic is — fully typed code can still calculate the wrong salary. And it has no reach over external data at runtime (see #355), so type checking is not a substitute for tests.

Follow-up: “Do you turn on strict?” — always on a new project. The most valuable piece is strictNullChecks — it turns an entire class of “forgot the null check” runtime errors into compile errors.
A real example to go with it: npm run build on the source project for the React course fails precisely because tsc reports 10 errors (the test file is missing the vitest globals) — which shows type checking is part of the build, not an optional lint.

Node / ExpressNode / Express#313

Node.js 的事件循环是怎么工作的

How does the event loop work in Node.js

看答案Show answer

一句话:Node 是单线程执行 JS + 多线程做 I/O。 主线程跑 JS, 耗时的 I/O 交给 libuv 的线程池或操作系统, 完成后把回调放进队列, 事件循环再取出来执行。

六个阶段(顺序要记住):

  1. timers—— 到期的 setTimeout /setInterval
  2. pending callbacks—— 上一轮延后的系统回调
  3. idle / prepare —— 内部用
  4. poll——取新的 I/O 事件, 大部分时间待在这里
  5. check——setImmediate 的回调
  6. close callbacks——socket.on("close") 这类

关键:每个阶段之间都会把微任务清空。而 Node 的微任务分两级:process.nextTick的优先级比 Promise 更高—— 它有自己的队列,在所有 Promise 微任务之前执行

和浏览器的差别(这是考点):浏览器只有「宏任务 / 微任务」两档, 每次只取一个宏任务; Node 分了六个阶段, 同一阶段里的队列会一次取完
另外 Node 多了setImmediateprocess.nextTick这两个浏览器没有的东西。

会追问:setTimeout(fn, 0)setImmediate 谁先?」——不确定! 在主模块里两者顺序取决于进程启动耗时; 但在 I/O 回调里,setImmediate 一定更早(因为 check 阶段紧跟在 poll 后面, 而 timers 要等下一轮)。能答出「主模块里不确定」很加分。
「CPU 密集任务怎么办?」—— 单线程会被卡死。 用 worker_threads、 子进程,或者干脆交给别的服务。

In one line: Node runs your JavaScript on one thread and its I/O on many. The main thread runs JS; anything slow goes to libuv’s thread pool or straight to the OS, and when it finishes the callback is queued for the event loop to pick up.

Six phases, and the order matters:

  1. timerssetTimeout and setInterval callbacks that are due
  2. pending callbacks — system callbacks deferred from the previous turn
  3. idle / prepare — internal use
  4. pollpicks up new I/O events; this is where the process spends most of its time
  5. checksetImmediate callbacks
  6. close callbacks — things like socket.on("close")

The key point: microtasks are drained between every phase. And Node has two levels of them — process.nextTick outranks Promises. It gets its own queue and runs before any Promise microtask.

How this differs from the browser (this is the part they are testing): the browser has just two tiers, macrotask and microtask, and takes one macrotask per turn. Node has six phases, and within a phase it drains the whole queue.
Node also has setImmediate and process.nextTick, neither of which exists in a browser.

Follow-up: “Which fires first, setTimeout(fn, 0) or setImmediate?” — it is not guaranteed. At the top level of the main module the order depends on how long startup took. But inside an I/O callback setImmediate always wins, because check comes right after poll while timers has to wait for the next turn. Saying “undefined in the main module” is the answer that earns you points.
“What about CPU-bound work?” — one thread means it blocks everything. Use worker_threads, a child process, or hand the job to a different service entirely.

JavaScriptNode 的执行顺序示意Illustrative
1console.log("1 同步");
2
3setTimeout(() => console.log("2 timers"), 0);
4setImmediate(() => console.log("3 check"));
5Promise.resolve().then(() => console.log("4 promise 微任务"));
6process.nextTick(() => console.log("5 nextTick"));
7
8console.log("6 同步");
9
10// 输出:1 同步 -> 6 同步 -> 5 nextTick -> 4 promise 微任务
11// -> 2 timers 和 3 check(这两个的相对顺序在主模块里不保证)
12//
13// 记住:nextTick 比 Promise 更优先,这是 Node 独有的
1console.log("1 sync");
2
3setTimeout(() => console.log("2 timers"), 0);
4setImmediate(() => console.log("3 check"));
5Promise.resolve().then(() => console.log("4 promise microtask"));
6process.nextTick(() => console.log("5 nextTick"));
7
8console.log("6 sync");
9
10// Output: 1 sync -> 6 sync -> 5 nextTick -> 4 promise microtask
11// -> 2 timers and 3 check (their relative order is not guaranteed in the main module)
12//
13// Remember: nextTick runs before Promise, and that part is specific to Node
Node / ExpressNode / Express#314

请求 - 响应周期是怎样的

Explain the request & response cycle

看答案Show answer

一句话:客户端发请求 → 经过一串中间件 → 匹配到路由 → 处理逻辑(查库等)→发出一个响应→ 结束。

Express 里具体经过什么:

  1. 解析——express.json()把请求体解析成 req.body不加这个中间件req.body 就是undefined, 这是最常见的新手问题)
  2. 通用中间件—— CORS、日志、关联 id、限流
  3. 认证 / 鉴权—— 验 token,把用户挂到req.user
  4. 路由匹配—— 按注册顺序找第一个匹配的 method + path
  5. 校验 → 业务逻辑 → 查库
  6. 响应——res.status(200).json(...)
  7. 兜底—— 404 处理 + 错误处理中间件

三个必须知道的规则:

  • 中间件按注册顺序执行, 必须调 next()才会往下走 —— 忘了调请求就永远挂着, 客户端一直转圈。
  • 一个请求只能响应一次。重复 res.send会报Cannot set headers after they are sent所以调用响应之后一定要return
  • 错误处理中间件有四个参数(err, req, res, next)——少一个参数 Express 就认不出它是错误处理器, 这个坑很经典。

会追问:「异步函数里抛的错误 Express 会自动捕获吗?」——Express 4 里不会, 会变成未处理的 rejection,请求挂死。 要么自己 try/catchnext(err), 要么包一层 asyncHandler。Express 5 才支持自动转发。

In one line: the client sends a request, it walks through a chain of middleware, a route matches, the handler does its work (hits the database and so on), one response goes out, done.

What it actually passes through in Express:

  1. Parsingexpress.json() turns the body into req.body (without that middleware req.body is undefined, which is the single most common beginner bug)
  2. Generic middleware — CORS, logging, a correlation id, rate limiting
  3. Authentication and authorisation — verify the token, hang the user off req.user
  4. Route matching — the first registered method + path that matches wins
  5. Validation, then business logic, then the database
  6. Response res.status(200).json(...)
  7. Catch-alls — the 404 handler and the error-handling middleware

Three rules you have to know:

  • Middleware runs in registration order and you must call next() for the chain to continue. Forget it and the request hangs forever while the client spins.
  • One request gets exactly one response. A second res.send throws Cannot set headers after they are sent. So always return after you respond.
  • Error middleware takes four arguments, (err, req, res, next) drop one and Express no longer recognises it as an error handler. A classic trap.

Follow-up: “Does Express catch errors thrown inside an async function?” — not in Express 4. It becomes an unhandled rejection and the request hangs. Either try/catch yourself and call next(err), or wrap the handler in an asyncHandler. Express 5 forwards them for you.

JavaScript一个请求经过的全部环节示意Illustrative
1app.use(express.json()); // 1 解析 body
2app.use(cors()); // 2 通用
3app.use(correlationId); // 关联 id,方便串联日志
4app.use(auth); // 3 认证
5
6app.get("/orders/:id", async (req, res, next) => {
7 try {
8 const order = await db.find(req.params.id);
9 if (!order) return res.status(404).json({ error: "not found" }); // 记得 return
10 res.json(order);
11 } catch (e) {
12 next(e); // Express 4 不会自动接异步错误
13 }
14});
15
16app.use((req, res) => res.status(404).json({ error: "no route" }));
17
18// 错误处理中间件:必须是四个参数,少一个就不生效
19app.use((err, req, res, next) => {
20 console.error(err);
21 res.status(500).json({ error: "internal" });
22});
1app.use(express.json()); // 1 parse the body
2app.use(cors()); // 2 general
3app.use(correlationId); // a correlation id, so logs can be tied together
4app.use(auth); // 3 authentication
5
6app.get("/orders/:id", async (req, res, next) => {
7 try {
8 const order = await db.find(req.params.id);
9 if (!order) return res.status(404).json({ error: "not found" }); // do not forget return
10 res.json(order);
11 } catch (e) {
12 next(e); // Express 4 does not catch async errors for you
13 }
14});
15
16app.use((req, res) => res.status(404).json({ error: "no route" }));
17
18// The error-handling middleware must take four arguments; with three it never runs
19app.use((err, req, res, next) => {
20 console.error(err);
21 res.status(500).json({ error: "internal" });
22});
Node / ExpressNode / Express#315

查询参数 vs 路径参数

Query parameters vs Path parameters

看答案Show answer

一句话:路径参数标识「哪一个资源」/users/42),查询参数描述「怎么取」?page=2&sort=name)。

路径参数查询参数
形式/users/:id/users?role=admin
Express 里读req.params.idreq.query.role
Spring 里读@PathVariable@RequestParam
必填性必填(是路径的一部分)通常可选,有默认值
语义标识资源筛选、排序、分页、字段裁剪

设计判据一句话:「去掉它之后,还是同一个资源吗?」去掉 42 就不知道是谁了 → 路径参数; 去掉 ?page=2还是同一批用户,只是换一页 → 查询参数。

会追问:「密码能放查询参数吗?」——绝对不行。 URL 会进浏览器历史、 服务器访问日志、 Referer 头、CDN 日志 ——即使用了 HTTPS, URL 本身也会被大量记录。 敏感数据放请求体或请求头。
「查询参数有长度限制吗?」—— 规范没有,但实践上服务器和 CDN 通常限制 URL 在 2 KB 到 8 KB。 复杂查询条件多的搜索接口 有时会改用 POST 带 body。

In one line: a path parameter says which resource (/users/42); a query parameter says how you want it (?page=2&sort=name).

Path parameterQuery parameter
Shape/users/:id/users?role=admin
Read in Expressreq.params.idreq.query.role
Read in Spring@PathVariable@RequestParam
Required?Yes — it is part of the pathUsually optional, often with a default
MeaningIdentifies the resourceFiltering, sorting, paging, field selection

One test to decide: “Take it away — is it still the same resource?” Drop the 42 and you no longer know who you mean → path parameter. Drop ?page=2 and it is still the same set of users, just a different page → query parameter.

Follow-up: “Can a password go in a query parameter?” — absolutely not. URLs land in browser history, server access logs, the Referer header and CDN logs. HTTPS encrypts the connection; it does not stop the URL from being written down all over the place. Sensitive values go in the body or a header.
“Is there a length limit on query strings?” — not in the spec, but in practice servers and CDNs cap the URL somewhere between 2 KB and 8 KB. Search endpoints with complex filters sometimes switch to POST with a body for that reason.

Node / ExpressNode / Express#316

什么是 CRUD

What is CRUD

看答案Show answer

一句话:Create / Read / Update / Delete —— 数据操作的四种基本类型。 REST 把它们映射到 HTTP 方法。

操作方法路径成功状态码
CreatePOST/orders201 Created
Read(列表)GET/orders200
Read(单个)GET/orders/:id200,没有则 404
Update(整体)PUT/orders/:id200
Update(部分)PATCH/orders/:id200
DeleteDELETE/orders/:id204 No Content

两个高频追问:

① PUT vs PATCH。PUT整体替换—— 没传的字段应该被清空;PATCH局部更新—— 只改传了的字段。实践中很多人把 PUT 当 PATCH 用, 这是错的,但要知道现实如此。

② 幂等性。同一个请求发多次,结果一样就叫幂等。

  • GET / PUT /DELETE ——幂等
  • POST ——不幂等(发两次会创建两条)
  • PATCH ——看实现{ n: 5 } 幂等,{ n: { $inc: 1 } } 不幂等)

为什么重要:客户端重试、 网关超时重发时, 幂等的接口是安全的,POST 需要幂等键(idempotency key)来防重复下单。

再一个追问:「删一个不存在的资源返回什么?」——可以是 204 也可以是 404。 返 204 更符合幂等语义 (「删完了」这个结果达成了); 返 404 信息更明确。关键是团队内一致, 并且写进接口文档。

In one line: Create / Read / Update / Delete — the four basic things you do to data. REST maps them onto HTTP methods.

OperationMethodPathStatus on success
CreatePOST/orders201 Created
Read (list)GET/orders200
Read (one)GET/orders/:id200, or 404 if it is not there
Update (whole)PUT/orders/:id200
Update (partial)PATCH/orders/:id200
DeleteDELETE/orders/:id204 No Content

Two follow-ups they almost always ask:

① PUT vs PATCH. PUT is a full replacement — fields you leave out should be cleared. PATCH is a partial update — only the fields you send change. Plenty of real code uses PUT as if it were PATCH, which is wrong, but know that it happens.

② Idempotency. Send the same request more than once, get the same result — that is idempotent.

  • GET / PUT / DELETE idempotent
  • POSTnot idempotent (send it twice and you get two records)
  • PATCHdepends on the payload ({ n: 5 } is idempotent, { n: { $inc: 1 } } is not)

Why it matters: when a client retries or a gateway resends after a timeout, an idempotent endpoint is safe. POST needs an idempotency key to stop the same order being placed twice.

One more follow-up: “What do you return when deleting something that does not exist?” — 204 or 404, both defensible. 204 fits the idempotent reading (the outcome you asked for is true); 404 tells the caller more. What matters is that the team agrees and it is in the API docs.

数据库Databases#317

关系型数据库 vs 非关系型数据库

Relational database vs Non-relational database

看答案Show answer

一句话:关系型先定好表结构、用 JOIN 关联、 强调一致性; 非关系型结构灵活、 按查询模式组织数据、 强调扩展性

关系型(MySQL、PostgreSQL)非关系型(MongoDB、Redis)
结构表 + 行 + 列,schema 固定文档 / 键值 / 图,schema 灵活
关联JOIN嵌套文档,或应用层自己拼
事务ACID 是强项有但较弱(MongoDB 4.0+ 支持多文档事务)
扩展纵向为主(加配置),分库分表麻烦横向为主(加机器)
适合数据关系复杂、要强一致—— 订单、账务、库存结构多变、读多写多、 单次查询取一整块—— 日志、内容、会话、缓存

选型的正确说法:「看数据形状和访问模式」——

  • 一次查询要取的东西总是在一起(一篇文章连着它的所有段落)→ 文档型合适。
  • 同一份数据要从很多角度关联查(用户 × 订单 × 商品 × 优惠券)→ 关系型合适,因为文档型要么冗余存多份、要么在应用层做 JOIN
  • 需要转账那样的强一致 → 关系型。

会追问:「MongoDB 没有 schema 是优点吗?」——是双刃剑。 前期迭代快,但约束跑到了应用层, 时间长了同一个集合里会存在好几代不同形状的文档。 所以实践中一般还是用 Mongoose 这类工具在应用层加回 schema
「现在还有清楚的界限吗?」—— 在模糊:PostgreSQL 的jsonb 让它能存文档并建索引, 所以「先上 Postgres, 需要文档就用 jsonb」是很常见的现实选择。

In one line: relational means you define the schema up front, relate rows with JOINs, and lean on consistency; non-relational means a flexible shape, data laid out for the queries you actually run, and easier scale-out.

Relational (MySQL, PostgreSQL)Non-relational (MongoDB, Redis)
ShapeTables, rows, columns — fixed schemaDocuments / key-value / graph — flexible schema
Relating dataJOINNested documents, or you stitch it in the app
TransactionsACID is the whole pointPresent but weaker (MongoDB 4.0+ has multi-document)
ScalingMostly vertical; sharding is painfulMostly horizontal — add machines
Good forComplex relationships and strong consistency — orders, ledgers, inventoryShifting shapes, heavy read and write, one query pulling a whole blob — logs, content, sessions, caches

The right way to talk about choosing: “Look at the shape of the data and the access pattern.”

  • Everything one query needs always travels together (an article and all its paragraphs) → document store fits.
  • The same data gets related from many angles (users × orders × products × coupons) → relational fits, because a document store either duplicates it or makes you JOIN in application code.
  • You need transfer-money-level consistency → relational.

Follow-up: “Is MongoDB being schema-less an advantage?” — it cuts both ways. You move faster early on, but the constraints move into your application code, and given enough time one collection holds three generations of document shapes. Which is why teams reach for something like Mongoose to put a schema back on top.
“Is the line still clear today?” — it is blurring. PostgreSQL’s jsonb stores documents and indexes them, so “start on Postgres, use jsonb where you need a document” is a very common real-world answer.

数据库Databases#318

主键 vs 外键

Primary key vs Foreign key

看答案Show answer

一句话:主键唯一标识本表的一行外键指向另一张表的主键, 用来表达关联并保证引用有效。

主键(Primary Key)外键(Foreign Key)
作用唯一标识一行指向另一表的主键
唯一性必须唯一可以重复(一个用户多个订单)
能否为 NULL不能可以(表示「暂时没关联」)
每表几个一个(可以是多列组成的复合主键)多个
索引自动建不一定自动建—— MySQL 会,PostgreSQL 不会

「外键索引」那一条是加分点: PostgreSQL 里外键列不会自动建索引, 而 JOIN 和级联删除都要用到它 ——忘了手动建索引是很常见的性能问题

外键的核心价值是引用完整性: 数据库拒绝你插入一条 指向不存在用户的订单, 也拒绝你删掉还有订单的用户。这是数据库帮你兜住的一致性, 不用在应用层写检查。

会追问删除行为—— 这个一定要会:

  • RESTRICT /NO ACTION——有引用就不许删(默认,最安全)
  • CASCADE——连着子记录一起删(很方便也很危险, 删一个用户可能连带删掉几万条记录)
  • SET NULL—— 把子记录的外键置空 (适合「作者被删了,文章保留为匿名」)

还会追问:「主键用自增 id 还是 UUID?」—— 自增:短、索引局部性好、 但暴露数据量、分库时会冲突。 UUID:全局唯一、 适合分布式和前端预生成, 但更长、随机写入对 B+ 树索引不友好折中是 ULID / UUIDv7(带时间前缀,有序)—— 这个答出来会显得很专业。

In one line: a primary key uniquely identifies a row in its own table; a foreign key points at another table’s primary key, expressing the relationship and keeping the reference valid.

Primary keyForeign key
JobIdentifies one rowPoints at another table’s primary key
Unique?Must beCan repeat (one user, many orders)
Nullable?NoYes — meaning “not linked yet”
How many per tableOne (possibly composite, several columns)Many
IndexCreated for youNot always — MySQL does, PostgreSQL does not

That last row is the bonus point. In PostgreSQL a foreign key column gets no index automatically, and both JOINs and cascading deletes need one — forgetting to add it by hand is a very common performance bug.

The real value of a foreign key is referential integrity: the database refuses to insert an order pointing at a user who does not exist, and refuses to delete a user who still has orders. That is consistency the database holds for you, so you do not write the check in application code.

They will ask about delete behaviour — know these three:

  • RESTRICT / NO ACTION refuse the delete while references exist (the default, and the safest)
  • CASCADE delete the children along with it (convenient and dangerous; deleting one user can take tens of thousands of rows with it)
  • SET NULL — null out the child’s foreign key (fits “the author is gone, keep the article as anonymous”)

Another follow-up: “Auto-increment id or UUID?” — auto-increment is short, gives good index locality, but leaks how much data you have and collides when you shard. UUID is globally unique, good for distributed systems and for generating ids on the client, but longer, and random inserts are unkind to a B+ tree index. The middle ground is ULID or UUIDv7 — time-prefixed, so they sort — and saying that makes you sound like you have done this before.

Text建表时的三个要点示意Illustrative
1CREATE TABLE users (
2 id BIGSERIAL PRIMARY KEY, -- 主键:唯一、非空、自动建索引
3 email TEXT UNIQUE NOT NULL -- 唯一约束 ≠ 主键
4);
5
6CREATE TABLE orders (
7 id BIGSERIAL PRIMARY KEY,
8 user_id BIGINT NOT NULL
9 REFERENCES users(id)
10 ON DELETE RESTRICT, -- 还有订单就不许删用户
11 total NUMERIC(10,2) NOT NULL
12);
13
14-- PostgreSQL 不会自动给外键列建索引,JOIN 会慢
15CREATE INDEX idx_orders_user_id ON orders(user_id);
1CREATE TABLE users (
2 id BIGSERIAL PRIMARY KEY, -- primary key: unique, not null, indexed automatically
3 email TEXT UNIQUE NOT NULL -- a unique constraint is not the same as a primary key
4);
5
6CREATE TABLE orders (
7 id BIGSERIAL PRIMARY KEY,
8 user_id BIGINT NOT NULL
9 REFERENCES users(id)
10 ON DELETE RESTRICT, -- a user with orders left cannot be deleted
11 total NUMERIC(10,2) NOT NULL
12);
13
14-- PostgreSQL does not index a foreign key column for you, and the JOIN gets slow
15CREATE INDEX idx_orders_user_id ON orders(user_id);
网络与安全Web & security#360

什么是 CORS,怎么解决 CORS 错误

What is CORS and how to solve the CORS error

看答案Show answer

一句话:浏览器的同源策略默认禁止页面读取 跨源响应;CORS 是服务器通过响应头「授权」某些跨源请求的机制。

三条最关键的认知(这才是区分度):

  • 是浏览器在拦,不是服务器拒绝。请求通常已经发出去了、 服务器也已经处理了—— 只是浏览器不让 JS 读响应。所以看到 CORS 错误不等于接口没执行(非幂等接口尤其要注意, 可能已经创建了数据)。
  • 所以前端改不了。必须服务端加响应头, 或者走代理。在前端加什么请求头都没用。
  • 同源 = 协议 + 域名 + 端口 三者全同。httphttps 不同源,30003001 不同源。

简单请求 vs 预检请求:GET / HEAD /POST 且只用安全头、Content-Type 限于三种 (form-urlencodedmultipart/form-datatext/plain)→ 直接发。
其他情况先发一个OPTIONS 预检注意 application/json就会触发预检—— 这就是为什么「明明是 POST 却多了一个 OPTIONS 请求」。

四种解法:

  1. 服务端加头(正解)——Access-Control-Allow-Origin, Express 里 app.use(cors())
  2. 开发时用 dev server 代理—— Vite 的 server.proxy, 让浏览器以为是同源。
  3. 生产用同源部署或网关—— 前端和 API 挂在同一个域下的不同路径。
  4. (历史方案)JSONP —— 只支持 GET,已淘汰。

会追问:「要带 cookie 怎么办?」—— 前端 credentials: "include", 服务端 Allow-Credentials: true而且此时Allow-Origin不能是 *,必须写具体域名。 这是最常见的「配了 cors 还是不行」的原因。
「预检能缓存吗?」——Access-Control-Max-Age, 避免每个请求都多一次往返。

In one line: the browser’s same-origin policy stops a page from reading a cross-origin response by default; CORS is the mechanism by which the server uses response headers to authorise some of those requests.

Three things to understand — this is what separates people:

  • The browser blocks it; the server did not refuse. The request usually went out and the server usually handled it — the browser just will not let your JS read the response. So a CORS error does not mean the endpoint did not run. Watch out with non-idempotent endpoints: the record may already exist.
  • Which is why the front end cannot fix it. The server has to send the headers, or you go through a proxy. No request header you add on the client will help.
  • Same origin means scheme, host and port all match. http and https are different origins; 3000 and 3001 are different origins.

Simple requests vs preflighted ones: GET / HEAD / POST with only safe headers and a Content-Type limited to three values (form-urlencoded, multipart/form-data, text/plain) go straight out.
Anything else sends an OPTIONS preflight first. Note that application/json triggers a preflight — that is why “it is a POST but I see an extra OPTIONS request”.

Four ways to fix it:

  1. Send the headers from the server (the real fix) — Access-Control-Allow-Origin, or app.use(cors()) in Express.
  2. Proxy through the dev server while developing — Vite’s server.proxy, so the browser thinks it is same-origin.
  3. In production, deploy same-origin or put a gateway in front — front end and API on the same domain, different paths.
  4. (Historical) JSONP — GET only, obsolete.

Follow-up: “What if I need to send cookies?” — credentials: "include" on the client, Allow-Credentials: true on the server, and at that point Allow-Origin cannot be * — it must name the origin. This is the most common reason for “I configured cors and it still does not work”.
“Can the preflight be cached?” — Access-Control-Max-Age, so you do not pay a round trip per request.

JavaScript两种最常用的解法示意Illustrative
1// 服务端(正解)
2app.use(cors({
3 origin: "https://app.example.com", // 带 cookie 时不能用 *
4 credentials: true,
5 maxAge: 86400, // 缓存预检结果
6}));
7
8// 开发时代理(vite.config.ts)
9server: {
10 proxy: { "/api": { target: "http://localhost:4000", changeOrigin: true } },
11}
12// 浏览器看到的是同源的 /api/...,不触发 CORS
1// On the server (the correct way)
2app.use(cors({
3 origin: "https://app.example.com", // with cookies you cannot use *
4 credentials: true,
5 maxAge: 86400, // cache the preflight result
6}));
7
8// A proxy during development (vite.config.ts)
9server: {
10 proxy: { "/api": { target: "http://localhost:4000", changeOrigin: true } },
11}
12// The browser sees /api/... on the same origin, so CORS is never triggered
网络与安全Web & security#358

HTTPS vs HTTP

HTTPS vs HTTP

看答案Show answer

一句话:HTTPS = HTTP + TLS 加密层。 同一套协议,只是传输过程被加密和验证了。

它提供三样东西(要说全):

  • 加密—— 中间人看不到内容
  • 身份验证—— 证书证明「你连的确实是这个域名的服务器」,这一条常被忽略, 但它才是防钓鱼的关键
  • 完整性—— 内容被篡改会被发现

握手大致过程:客户端打招呼 → 服务器发证书 → 客户端验证书链 →用非对称加密协商出一个对称密钥 → 之后用对称加密传数据。为什么要混用两种加密?非对称安全但慢, 对称快但要先安全地交换密钥 ——所以用非对称来交换对称密钥。 这一句是加分点。

端口:HTTP 80, HTTPS 443。

会追问:「HTTPS 慢吗?」—— 握手有额外开销, 但TLS 1.3 把握手压到一次往返, 而且HTTP/2 和 HTTP/3 只在 HTTPS 上可用—— 多路复用带来的收益通常超过加密的开销。 所以「用 HTTPS 会变慢」现在基本不成立。
「有了 HTTPS 就安全了吗?」——。它只保护传输过程。 XSS、SQL 注入、 弱口令、越权 一个都没解决。

In one line: HTTPS is HTTP plus a TLS layer. Same protocol; the transport is now encrypted and authenticated.

It gives you three things — name all three:

  • Encryption — someone in the middle cannot read the contents
  • Authentication — the certificate proves “you really are talking to the server for this domain”. People forget this one, and it is the part that stops phishing
  • Integrity — tampering is detected

Roughly how the handshake goes: client says hello → server sends its certificate → client verifies the chain → they use asymmetric crypto to agree on a symmetric key → everything after that is symmetric. Why mix the two? Asymmetric is secure but slow; symmetric is fast but needs the key exchanged safely first — so you use asymmetric to exchange the symmetric key. That sentence is the bonus point.

Ports: 80 for HTTP, 443 for HTTPS.

Follow-up: “Is HTTPS slow?” — the handshake costs something, but TLS 1.3 gets it down to one round trip, and HTTP/2 and HTTP/3 are only available over HTTPS — the multiplexing usually more than pays for the encryption. So “HTTPS makes it slower” no longer really holds.
“Does HTTPS make me secure?” — no. It protects the transport. XSS, SQL injection, weak passwords and broken authorisation are all still yours to solve.

网络与安全Web & security#359

什么是 JWT

What is JWT

看答案Show answer

一句话:JSON Web Token —— 一个自带签名的字符串, 服务器不用存它也能验证它没被篡改。

三段结构,用点分隔:header.payload.signature

  • header—— 用什么算法签的
  • payload—— 用户 id、过期时间等业务数据
  • signature—— 对前两段用密钥算出的签名

最重要的一条(必答):前两段只是Base64URL 编码,不是加密——任何人都能解出来看。 所以payload 里绝不能放密码、 身份证号这类敏感信息。 签名保证的是「没被改过」,不是「看不到」

优点:无状态—— 服务器不用存 session, 天然适合多实例和微服务 (任何一台都能独立验证)。

缺点(这半边是重点):

  • 没法主动失效。签出去就有效到过期。 用户改密码、 管理员封号,旧 token 照样能用
  • 体积比 session id 大, 每个请求都要带。
  • 放哪都有风险——localStorage 怕 XSS, cookie 怕 CSRF。

会追问:「怎么让 JWT 提前失效?」—— 这是这题的分水岭:

  • 短过期 + refresh token—— access token 只活 15 分钟, 用一个可撤销的 refresh token 换新的。这是标准做法。
  • 黑名单—— 把要作废的 token id 存 Redis。但这就重新变成有状态了, 等于放弃了 JWT 的主要优点。

安全上还有一个经典坑:验证时必须指定期望的算法, 不能信 header 里写的 —— 否则攻击者把 alg改成 none 就绕过签名了。

In one line: a JSON Web Token is a string that carries its own signature, so the server can verify it has not been tampered with without storing it.

Three parts, separated by dots: header.payload.signature

  • header — which algorithm signed it
  • payload — the data: user id, expiry and so on
  • signature — the first two parts signed with your secret

The one thing you must say: the first two parts are Base64URL encoded, not encrypted anyone can decode and read them. So never put a password or a national id number in the payload. The signature guarantees “unchanged”, not “unreadable”.

The upside: it is stateless — the server stores no session, which suits many instances and microservices, since any one of them can verify it alone.

The downsides — this half is the real question:

  • You cannot revoke it. Once issued it is valid until it expires. User changes their password, an admin bans the account — the old token still works.
  • It is bigger than a session id and rides along on every request.
  • Every place you store it has a risk localStorage is exposed to XSS, a cookie is exposed to CSRF.

Follow-up: “How do you expire a JWT early?” — this is where the question separates people:

  • Short expiry plus a refresh token — the access token lives 15 minutes and you trade a revocable refresh token for a new one. This is the standard answer.
  • A blocklist — keep revoked token ids in Redis. But now you are stateful again, which gives up the main reason you chose JWT.

One classic security trap: when you verify, you must pin the algorithm you expect rather than trust the one in the header — otherwise an attacker sets alg to none and walks past the signature entirely.

这些题从哪来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.
八股题库 / Interview drills · DrillLab