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.

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

题目Questions

筛出 7 道。7 of 7 questions.
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.

JavaScriptJavaScript#312

什么是 npm

What is npm

看答案Show answer

一句话:Node 的包管理器 + 全球最大的包仓库。 它负责装依赖、锁版本、跑脚本。

三件核心事:

  • 装依赖—— 读 package.jsondependencies, 递归下载到 node_modules
  • 锁版本——package-lock.json记下每一个包的确切版本和哈希, 保证队友和 CI 装出来的一模一样。它必须提交到版本库。
  • 跑脚本——npm run dev, 而且会把node_modules/.bin加到 PATH,所以能直接写vitest 而不用写全路径。

必答的两个区分:

  • dependencies vs devDependencies—— 前者是运行时需要的 (React),后者只在开发和构建时需要 (TypeScript、测试框架、打包工具)。 生产安装可以用npm ci --omit=dev 跳过后者。
  • npm install vs npm ci——install 会在需要时更新 lock 文件ci 严格按 lock 装, 对不上就直接报错。CI 里应该用 ci

会追问:^1.2.3~1.2.3 什么区别?」——^ 允许小版本和补丁升级 (<2.0.0),~ 只允许补丁 (<1.3.0)。正是因为 ^ 的存在, lock 文件才必不可少—— 否则不同时间装出来的版本会不同。

In one line: Node’s package manager, plus the largest package registry in the world. It installs dependencies, pins versions and runs scripts.

Three core jobs:

  • Install dependencies — read dependencies out of package.json and download the whole tree into node_modules.
  • Pin versionspackage-lock.json records the exact version and hash of every package, so a teammate and CI install precisely what you did. It has to be committed.
  • Run scriptsnpm run dev. It also puts node_modules/.bin on the PATH, which is why you can write vitest instead of a full path.

Two distinctions you must be able to make:

  • dependencies vs devDependencies — the first is what you need at runtime (React), the second only while developing and building (TypeScript, the test runner, the bundler). A production install can skip the second with npm ci --omit=dev.
  • npm install vs npm ci install will update the lock file when it has to; ci installs strictly from the lock and errors out if the two disagree. CI should use ci.

Follow-up: “What is the difference between ^1.2.3 and ~1.2.3?” — ^ allows minor and patch upgrades (<2.0.0), ~ allows patches only (<1.3.0). It is precisely because ^ exists that the lock file is indispensable — otherwise installing on two different days gives you two different trees.

JavaScriptJavaScript#283

Webpack 是怎么工作的

How does Webpack work

看答案Show answer

一句话:从入口出发递归分析所有 import, 建出一张依赖图, 路上用 loader 把各种文件转成 JS, 最后按规则打成若干个 bundle。

四个核心概念(必答):

  • entry—— 从哪开始找依赖。
  • output—— 打到哪里、叫什么名 (带 [contenthash] 做缓存)。
  • loader——把非 JS 转成 JSbabel-loader 转 JSX 和新语法、css-loader 让 CSS 能被importloader 是从右到左(从下到上)执行的, 这个细节常问。
  • plugin—— 在构建生命周期的各个钩子上干活, 能力比 loader 大得多 (生成 HTML、抽 CSS、压缩、分析体积)。

为什么需要打包(这才是问题的本质):浏览器早期不支持模块、 不认识 JSX 和 TS、 请求数多会慢; 打包解决的是模块化、转译、 合并压缩、以及 tree shaking

会追问:「Vite 为什么快?」—— 这题现在很常问。 开发时 Vite 不打包, 直接用浏览器原生 ESM 按需提供模块, 所以启动是常数时间, 不随项目变大而变慢; 依赖预构建用 esbuild(Go 写的,快一个量级); 生产才用 Rollup 打包。
Webpack 是「先全量打包再服务」, Vite 是「先服务再按需转换」—— 这一句就说到了根本区别。

In one line: start at the entry, walk every import recursively and build a dependency graph, push files through loaders to turn them into JS along the way, and emit a handful of bundles according to your rules.

Four core concepts you have to name:

  • entry — where the dependency walk starts.
  • output — where the bundles land and what they are called (with [contenthash] for caching).
  • loaderturns non-JS into JS. babel-loader handles JSX and new syntax, css-loader makes CSS importable. Loaders run right to left (bottom to top) — that detail gets asked a lot.
  • plugin — hooks into the build lifecycle and can do far more than a loader (generate HTML, extract CSS, minify, analyse bundle size).

Why bundling exists at all — that is the real question: early browsers had no modules, they do not understand JSX or TS, and a lot of requests is slow. Bundling buys you modularity, transpilation, merging and minifying, and tree shaking.

Follow-up: “Why is Vite fast?” — asked constantly now. In development Vite does not bundle; it serves modules on demand over the browser’s native ESM, so startup is constant time and does not degrade as the project grows. Dependency pre-bundling runs on esbuild (written in Go, an order of magnitude faster), and only production goes through Rollup.
Webpack bundles everything and then serves it; Vite serves first and transforms on demand — that single sentence gets at the fundamental difference.

JavaScriptJavaScript#387

fetch 和 axios 的区别

What is the difference between making server requests via fetch and axios?

看答案Show answer

一句话:fetch浏览器内置的, 很基础;axios 是第三方库, 把常用的事都替你做了。

fetchaxios
依赖,浏览器/Node 18+ 内置要装(约 13 KB)
4xx / 5xx不 reject, 要自己查 res.ok自动抛错
JSON要手动 await res.json()自动解析到 data
超时要自己配 AbortControllertimeout 一个选项
拦截器没有,要自己包一层内置(统一加 token、统一处理 401)
上传进度很麻烦支持
Node 里可用18+ 才有一直可以

「404 不 reject」是这题的核心考点, 也是真实 bug 的来源:不查res.ok 就会把错误页当数据渲染。从 axios 转过来的人最容易漏这一条, 因为 axios 会自己抛。

怎么选:简单项目、在意包体积、 或者只发几个请求 → fetch包一个自己的小 wrapper; 需要拦截器 / 统一错误处理 / 上传进度,或者要兼容老 Node → axios
更常见的现实答案:用TanStack Query / SWR管缓存和请求状态, 底下用哪个都行 —— 因为fetchaxios都不管缓存、去重、重试。 能这么答说明你想过分层。

In one line: fetch is built into the browser and very bare-bones; axios is a third-party library that does the routine work for you.

fetchaxios
DependencyNone, built into browsers and Node 18+Must be installed (about 13 KB)
4xx / 5xxDoes not reject; you check res.ok yourselfThrows for you
JSONYou call await res.json() by handParsed into data already
TimeoutWire up an AbortController yourselfOne timeout option
InterceptorsNone; wrap it yourselfBuilt in (attach a token everywhere, handle 401 in one place)
Upload progressPainfulSupported
Available in NodeOnly from 18Always has been

“404 does not reject” is the heart of this question, and a real source of bugs: skip the res.ok check and you render an error page as if it were data. People coming over from axios miss this one most often, because axios throws on their behalf.

How to choose: a small project, a tight bundle budget, or only a handful of requests → fetch with your own small wrapper. Interceptors, one place for error handling, upload progress, or an old Node to support → axios.
The more realistic answer: reach for TanStack Query or SWR to manage caching and request state, and whichever one sits underneath hardly matters — because neither fetch nor axios handles caching, deduplication or retries. Answering that way shows you have thought about the layers.

JavaScript两者的核心差别与自制 wrapperThe main difference, and a wrapper of your own示意Illustrative
1// fetch:404 也是「成功」,必须自己判
2const res = await fetch(url);
3if (!res.ok) throw new Error(`HTTP ${res.status}`); // ← 漏了这行就会出 bug
4const data = await res.json();
5
6// axios:非 2xx 自己抛,data 已经解析好
7const { data } = await axios.get(url);
8
9// 自己给 fetch 包一层,就能补上大部分差距
10async function request(url, opts = {}) {
11 const c = new AbortController();
12 const t = setTimeout(() => c.abort(), opts.timeout ?? 10000);
13 try {
14 const res = await fetch(url, { ...opts, signal: c.signal });
15 if (!res.ok) throw new Error(`HTTP ${res.status}`);
16 return await res.json();
17 } finally {
18 clearTimeout(t);
19 }
20}
1// fetch: a 404 also counts as "success", so you have to check it yourself
2const res = await fetch(url);
3if (!res.ok) throw new Error(`HTTP ${res.status}`); // ← leave this line out and you get a bug
4const data = await res.json();
5
6// axios: it throws on anything that is not 2xx, and data is already parsed
7const { data } = await axios.get(url);
8
9// Wrap fetch yourself and you close most of the gap
10async function request(url, opts = {}) {
11 const c = new AbortController();
12 const t = setTimeout(() => c.abort(), opts.timeout ?? 10000);
13 try {
14 const res = await fetch(url, { ...opts, signal: c.signal });
15 if (!res.ok) throw new Error(`HTTP ${res.status}`);
16 return await res.json();
17 } finally {
18 clearTimeout(t);
19 }
20}

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