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 道 · 第 2 / 9 页。105 of 105 questions · page 2 / 9.
CSSCSS#274

什么是响应式设计,怎么做

What is responsive web design and how to achieve this

看答案Show answer

一句话:一套代码在不同屏幕尺寸下 都给出合适的排版,而不是给手机单独做一个站。

四个手段,按重要性排:

  • viewport meta——前提,没有它后面全白干(见 #381)。
  • 弹性单位—— 宽度用 % / fr /min() / clamp(), 字号用 rem,别到处写死 pxclamp(16px, 4vw, 24px)一行就能做出「有上下限的流式字号」。
  • 媒体查询——@media (max-width: 768px) 改布局。
  • 弹性布局——flex-wrapgrid-template-columns: repeat(auto-fit, minmax(240px, 1fr)),很多时候一行都不用写媒体查询就自适应了。

会追问:「断点怎么定?」—— 正确答案是「按内容定,不是按设备定」: 把浏览器慢慢拉窄,哪里开始难看就在哪里加断点。 追着 iPhone 型号列表定断点是过时做法, 因为设备尺寸年年变。

还会追问 mobile-first:默认样式写窄屏,用 min-width 往上加。 好处是移动端加载的 CSS 最少, 而且「加东西」比「删东西」好写 —— 用 max-width 往下覆盖经常要反复清理属性。

In one line: one codebase that lays out sensibly at any screen size — rather than building a separate mobile site.

Four techniques, most important first:

  • The viewport meta tag the precondition; without it nothing else matters (see #381).
  • Flexible units — widths in % /fr / min() / clamp(), font sizes in rem; stop hard-coding px everywhere. clamp(16px, 4vw, 24px) gives you a fluid font size with hard limits in a single line.
  • Media queries @media (max-width: 768px) to change layout.
  • Flexible layoutflex-wrap and grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)) often adapt without a single media query.

Follow-up: “How do you choose breakpoints?” — the right answer is “from the content, not from device sizes”: drag the window narrower and add a breakpoint wherever it starts looking wrong. Chasing a list of iPhone dimensions is outdated, because device sizes change every year.

They will also ask about mobile-first: write the narrow layout as the default and add to it with min-width. Mobile then downloads the least CSS, and “adding” is easier to reason about than “undoing” — overriding downwards with max-width usually means repeatedly resetting properties.

CSS现在真正会写的响应式示意Illustrative
1/* 不写一行媒体查询的自适应网格 */
2.cards {
3 display: grid;
4 grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
5 gap: 16px;
6}
7
8/* 有上下限的流式字号 */
9h1 { font-size: clamp(24px, 5vw, 44px); }
10
11/* mobile-first:默认窄屏,往上加 */
12.layout { display: block; }
13@media (min-width: 768px) {
14 .layout { display: grid; grid-template-columns: 240px 1fr; }
15}
1/* A responsive grid without a single media query */
2.cards {
3 display: grid;
4 grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
5 gap: 16px;
6}
7
8/* A fluid font size with an upper and lower bound */
9h1 { font-size: clamp(24px, 5vw, 44px); }
10
11/* mobile-first: narrow by default, add from there upwards */
12.layout { display: block; }
13@media (min-width: 768px) {
14 .layout { display: grid; grid-template-columns: 240px 1fr; }
15}
JavaScriptJavaScript#276

什么是 JavaScript 引擎

What is the JavaScript engine

看答案Show answer

一句话:引擎是把 JS 源码变成机器能执行的东西的那个程序。Chrome / Node 用 V8,Firefox 用 SpiderMonkey, Safari 用 JavaScriptCore。

大致流程:源码 → 解析成 AST → 生成字节码 →解释器先跑起来, 同时监控哪段代码被反复执行(热点), 把热点交给 JIT 编译器编译成机器码。 这套「先解释、后即时编译」的做法让 JS 既能马上启动、 又能在热点上接近原生速度。

会追问:「引擎和运行时(runtime)什么区别?」 —— 这是真考点。引擎只管执行 JS 语言本身, 它不认识 setTimeoutfetchdocumentfs—— 这些都是宿主环境(浏览器 / Node)提供的 API。
所以「事件循环属于引擎吗?」答案是不属于, 它是运行时的一部分(见 #305)。这个区分答对了很加分。

还会追问内存:引擎管两块 ——调用栈(执行上下文、原始值)和(对象、数组、函数)。 这正好对应下一题。

In one line: the engine is the program that turns JS source into something the machine can run. Chrome and Node use V8, Firefox uses SpiderMonkey, Safari uses JavaScriptCore.

Roughly the pipeline: source → parse into an AST → emit bytecode → the interpreter starts running it while watching which parts run over and over (the hot paths), and hands those to the JIT compiler to be turned into machine code. Interpret first, compile later — that is what lets JS start instantly and still hit near-native speed where it counts.

Follow-up: “What is the difference between the engine and the runtime?” — this is the real question. The engine only executes the language itself. It knows nothing about setTimeout, fetch, document or fs — those are APIs the host environment (browser or Node) hands you.
So “is the event loop part of the engine?” — no, it belongs to the runtime (see #305). Getting this distinction right earns real credit.

Another follow-up, on memory: the engine manages two areas — the call stack (execution contexts, primitive values) and the heap (objects, arrays, functions). Which lines up exactly with the next question.

JavaScriptJavaScript#277

什么是 REPL

What is REPL

看答案Show answer

一句话:Read-Eval-Print-Loop —— 读一行、求值、打印结果、再等下一行。 终端里敲 node 回车进去的那个交互环境, 以及浏览器 DevTools 的 Console,都是 REPL。

用来干什么:验证一小段语法、试一个 API 的返回值、 快速算个东西。不适合写多行逻辑—— 改一行要重敲一遍。

会追问:「在 REPL 里 let x = 1 会打印什么?」——undefined。 因为它打印的是表达式的值, 而变量声明语句的值就是 undefined。 这个小细节能看出你是不是真用过。

In one line: Read-Eval-Print-Loop — read a line, evaluate it, print the result, wait for the next one. Typing node in a terminal drops you into one, and the DevTools Console in a browser is one too.

What you use it for: checking a bit of syntax, seeing what an API returns, working out a quick number. Not for multi-line logic — changing one line means retyping the lot.

Follow-up: “What does let x = 1 print in a REPL?” — undefined. It prints the value of the expression, and the value of a variable declaration is undefined. A small detail, but it shows whether you have actually used one.

JavaScriptJavaScript#278

原始值 vs 引用值

Primitive data types vs Reference data types

看答案Show answer

一句话:原始值存在栈上、直接存值; 引用值存在堆上、栈上只存一个地址

七种原始类型(背下来,会让你数):stringnumberbooleanundefinednullsymbolbigint
其余全是引用类型: 对象、数组、函数、DateMapSet、正则…… (数组和函数本质都是对象)。

三个可观察的差别:

  • 不可变性—— 原始值本身改不了。s.toUpperCase()返回新字符串,原来那个没动。
  • 比较—— 原始值比, 引用值比地址。所以{} === {}false[1] === [1] 也是 false
  • 赋值—— 原始值复制一份; 引用值只复制地址,两个变量指向同一个对象。 这就是 #284。

会追问:typeof null 是什么?」——"object",这是个沿用了 30 年的 bug, 因为兼容性一直没修。判断 null 要用x === null
「怎么可靠地判断数组?」——Array.isArray(x), 别用 typeof(会得到 "object")。

In one line: a primitive sits on the stack and holds the value itself; a reference value sits on the heap, and the stack holds only an address.

Seven primitive types (learn the list — they will make you count them): string, number, boolean, undefined, null, symbol, bigint.
Everything else is a reference type: objects, arrays, functions, Date, Map, Set, regexes… (arrays and functions are objects underneath).

Three differences you can observe:

  • Immutability — a primitive value cannot be changed.s.toUpperCase() returns a new string; the original never moved.
  • Comparison — primitives compare by value, references compare by address. So {} === {} is false, and so is [1] === [1].
  • Assignment — a primitive gets copied; a reference copies only the address, so two variables point at the same object. That is #284.

Follow-up: “What is typeof null? ” — "object", a bug that has survived 30 years because fixing it would break too much. Test for null with x === null.
“How do you reliably detect an array?” — Array.isArray(x). Never typeof, which just says "object".

JavaScript值和地址Values and addresses示意Illustrative
1// 原始值:复制值
2let a = 1;
3let b = a;
4b = 2;
5console.log(a); // 1 —— a 没变
6
7// 引用值:复制地址
8let o1 = { n: 1 };
9let o2 = o1; // 同一个对象的第二个遥控器
10o2.n = 2;
11console.log(o1.n); // 2 ← 变了!
12
13// 比较
14console.log({} === {}); // false(两个不同的地址)
15console.log("a" === "a"); // true (比的是值)
16console.log(typeof null); // "object" ← 历史 bug
17console.log(Array.isArray([])); // true ← 判断数组要用这个
1// Primitive value: the value is copied
2let a = 1;
3let b = a;
4b = 2;
5console.log(a); // 1 —— a did not change
6
7// Reference value: the address is copied
8let o1 = { n: 1 };
9let o2 = o1; // a second remote control for the same object
10o2.n = 2;
11console.log(o1.n); // 2 ← it changed!
12
13// Comparing
14console.log({} === {}); // false (two different addresses)
15console.log("a" === "a"); // true (the values are compared)
16console.log(typeof null); // "object" ← a historical bug
17console.log(Array.isArray([])); // true ← use this to test for an array
JavaScriptJavaScript#279 / #386

隐式转换 vs 显式转换

Type coercion vs Type conversion

看答案Show answer

一句话:转换(conversion / casting)是你主动写的强制转换(coercion)是引擎背着你干的

谁发起例子
显式(conversion)Number("42")String(42)Boolean(0)parseInt("42px")
隐式(coercion)引擎"5" * 21 + "1"if (arr.length)[] == false

隐式转换的两条核心规则(记住这两条, 大部分怪题就能推出来):

  • + 只要有一边是字符串,就变成拼接; 其他算术运算符(-*/)一律转成数字。 所以 1 + "1" === "11""3" - 1 === 2
  • 对象参与运算时先 valueOf()toString()。 数组的 toString() 是元素 join 逗号, 所以 [] + [] 得到空字符串,[] + {} 得到"[object Object]"

六个假值背下来(其余全是真):false0""nullundefinedNaN
注意 []{} 都是真值—— 所以判断数组空不空要看 arr.length

会追问:parseIntNumber 什么区别?」——parseInt("42px")42(从头读到读不动为止),Number("42px")NaN(整体不合法就失败)。 所以校验用户输入该用 NumberparseInt 会把脏数据悄悄放过去。

In one line: conversion (casting) is what you write on purpose; coercion is the engine doing it behind your back.

Who starts itExamples
Explicit (conversion)YouNumber("42"), String(42), Boolean(0), parseInt("42px")
Implicit (coercion)The engine"5" * 2, 1 + "1", if (arr.length), [] == false

Two rules cover almost all coercion — hold on to these and you can derive most of the trick questions:

  • + becomes concatenation the moment one side is a string; every other arithmetic operator (-, *, /) converts to number. Hence 1 + "1" === "11" but "3" - 1 === 2.
  • An object in an operation goes through valueOf() first, then toString(). An array’s toString() joins its elements with commas, so [] + [] gives an empty string and [] + {} gives "[object Object]".

Memorise the six falsy values (everything else is truthy): false, 0, "", null, undefined, NaN.
Watch out — [] and {} are both truthy, so check arr.length to tell whether an array is empty.

Follow-up: “What is the difference between parseInt and Number?” — parseInt("42px") gives 42 (it reads from the front until it cannot go on), while Number("42px") gives NaN (the whole string has to be valid). So validate user input with Number; parseInt waves dirty data straight through.

JavaScript隐式转换速查Coercion quick reference示意Illustrative
11 + "1" // "11" + 有字符串 -> 拼接
2"3" - 1 // 2 - 一律转数字
3"3" * "4" // 12
41 + true // 2 true -> 1
51 + null // 1 null -> 0
61 + undefined // NaN undefined -> NaN
7
8[] + [] // "" 两个空数组 toString 都是 ""
9[] + {} // "[object Object]"
10[1,2] + [3] // "1,23" join 逗号再拼
11
12Number("42px") // NaN 整体不合法
13parseInt("42px") // 42 读到读不动为止
14Number("") // 0 ← 注意,空字符串转数字是 0
15Number(" ") // 0 ← 空白也是 0,校验输入要小心
11 + "1" // "11" + with a string means concatenate
2"3" - 1 // 2 - always converts to number
3"3" * "4" // 12
41 + true // 2 true -> 1
51 + null // 1 null -> 0
61 + undefined // NaN undefined -> NaN
7
8[] + [] // "" toString of an empty array is ""
9[] + {} // "[object Object]"
10[1,2] + [3] // "1,23" join with commas, then concatenate
11
12Number("42px") // NaN the whole string has to be valid
13parseInt("42px") // 42 reads until it cannot read further
14Number("") // 0 ← note: an empty string converts to 0
15Number(" ") // 0 ← whitespace is 0 too, so validate input carefully
面试不会让你背全表,但会给两三个式子让你推。掌握「+ 看字符串、其他看数字」和「六个假值」就够推。An interview will not ask you to recite the whole table, but it will give you two or three expressions to work out. Remember that + looks for a string while every other operator converts to number, plus the six falsy values, and that is enough.
JavaScriptJavaScript#280

== 和 === 的区别

What is the difference between == and ===

看答案Show answer

一句话:===类型不同直接 false==先把两边转成同一类型再比

== 的转换规则很绕,但实际只要记住四条:

  • null == undefinedtrue, 但它们和其他任何值都不 ==(包括 0"")。
  • 数字和字符串比 → 字符串转数字。
  • 布尔参与 → 布尔先转数字true→1,false→0)。 这就是 [] == falsetrue 的原因:[]""0false0
  • 对象和原始值比 → 对象先转原始值。

结论:一律用 ===唯一被普遍接受的 == 用法是x == null—— 一次同时判掉nullundefined

会追问:NaN === NaN?」——falseNaN和自己都不相等。 判断要用 Number.isNaN(x)(别用全局 isNaN,它会先做隐式转换,isNaN("abc")true)。
「有没有更严格的比较?」——Object.is(x, y)。它和 ===只有两处不同:Object.is(NaN, NaN)trueObject.is(0, -0)falseReact 判断 state 变没变用的就是它。

In one line: === returns false straight away when the types differ; == converts both sides to one type first, then compares.

The == conversion rules are convoluted, but four points cover practice:

  • null == undefined is true, and neither one is == to anything else (not 0, not "").
  • Number against string → the string becomes a number.
  • A boolean involved → the boolean becomes a number (true→1, false→0). That is why [] == false is true: []""0, and false0.
  • Object against primitive → the object becomes a primitive.

The conclusion: use === everywhere. The one == that everybody accepts is x == null — it rules out null and undefined in a single check.

Follow-up:NaN === NaN?” — false. NaN is not even equal to itself. Test it with Number.isNaN(x) — not the global isNaN, which coerces first, so isNaN("abc") is true.
“Is there anything stricter?” — Object.is(x, y). It differs from === in exactly two places: Object.is(NaN, NaN) is true, and Object.is(0, -0) is false. This is what React uses to decide whether state changed.

JavaScript为什么别用 ==Why you should not use ==示意Illustrative
10 == "0" // true 字符串转数字
20 == "" // true "" -> 0
30 == false // true false -> 0
4null == undefined // true 特例
5null == 0 // false null 只和 undefined 相等
6[] == false // true [] -> "" -> 0
7NaN == NaN // false 和自己都不相等
8
90 === "0" // false 类型不同,到此为止
10Object.is(NaN, NaN) // true ← React 用它判断 state 变没变
10 == "0" // true the string converts to a number
20 == "" // true "" -> 0
30 == false // true false -> 0
4null == undefined // true a special case
5null == 0 // false null only equals undefined
6[] == false // true [] -> "" -> 0
7NaN == NaN // false not even equal to itself
8
90 === "0" // false different types, so it stops there
10Object.is(NaN, NaN) // true ← React uses this to decide whether state changed
JavaScriptJavaScript#281

什么是短路求值

What is short-circuit evaluation

看答案Show answer

一句话:&&||结果一旦确定就不再算右边, 而且它们返回的是操作数本身,不是布尔值

  • a && b—— a 为假就返回 a,否则返回 b。「都真才真」, 所以遇到假就可以收工。
  • a || b—— a 为真就返回 a,否则返回 b。
  • a ?? b(空值合并)——只有 a 是 nullundefined才返回 b。

||??的区别是高频追问,而且是真实 bug 来源:count || 10count0 时会给出 10—— 因为 0 是假值。要默认值就用 ??

React 里最常见的用法是条件渲染:{loading && <Spinner />}坑在这儿:如果左边是list.length 而列表为空,0 && … 返回 0, 而 React 会把 0 渲染出来—— 页面上凭空多一个「0」。 所以要写成 list.length > 0 && …

In one line: && and || stop evaluating the moment the result is decided, and they hand back an operand, not a boolean.

  • a && b — returns a if a is falsy, otherwise b. “Both must be true”, so one falsy value ends the job.
  • a || b — returns a if a is truthy, otherwise b.
  • a ?? b (nullish coalescing) — returns b only when a is null or undefined.

The difference between || and ?? is a frequent follow-up and a real source of bugs: count || 10 gives you 10 when count is 0, because 0 is falsy. For default values, reach for ??.

The most common React use is conditional rendering: {loading && <Spinner />}. Here is the trap: if the left side is list.length and the list is empty, then 0 && … returns 0, and React renders that 0 — a stray “0” shows up on the page out of nowhere. Write list.length > 0 && … instead.

JSX短路的三个实际用法与两个坑Three real uses of short-circuiting, and two traps示意Illustrative
1// 短路返回的是操作数本身
2console.log(1 && 2); // 2
3console.log(0 && 2); // 0 ← 不是 false
4console.log("" || "默认"); // "默认"
5
6// || 和 ?? 的区别
7const count = 0;
8count || 10 // 10 ✗ 0 被当成「没传」
9count ?? 10 // 0 ✓
10
11// React 条件渲染的经典坑
12{list.length && <List />} // ✗ 空列表时页面上多一个 0
13{list.length > 0 && <List />} // ✓
14{list.length ? <List /> : null} // ✓ 也可以
1// Short-circuiting returns the operand itself
2console.log(1 && 2); // 2
3console.log(0 && 2); // 0 ← not false
4console.log("" || "default"); // "default"
5
6// The difference between || and ??
7const count = 0;
8count || 10 // 10 ✗ 0 is treated as "nothing was passed"
9count ?? 10 // 0 ✓
10
11// The classic React conditional-rendering trap
12{list.length && <List />} // ✗ an empty list prints a stray 0 on the page
13{list.length > 0 && <List />} // ✓
14{list.length ? <List /> : null} // ✓ this works too
JavaScriptJavaScript#282

var、let、const 的区别

What is the difference between var, let and const

看答案Show answer

一句话:var 是函数作用域、会提升成undefined、能重复声明;let / const 是块作用域、 有 TDZ、不能重复声明;const 还不能重新赋值。

varletconst
作用域函数{}
声明前访问undefinedReferenceErrorReferenceError
重复声明可以不行不行
重新赋值可以可以不行
挂到 window顶层会挂不挂不挂

TDZ(暂时性死区)let / const「从块开始到声明那一行」之间的区域。 变量确实被提升了,但被标记为「还不能用」, 所以访问会抛错而不是给 undefined。 这是刻意设计的 —— 让错误早暴露。

会追问:const 的对象能改属性吗?」——const 锁的是绑定(这个名字不能再指向别的东西), 不是。 想冻结内容用 Object.freeze()

还会追问循环那道经典题——for (var i…)setTimeout 会打出三个 3,let 会打出 0 1 2。 因为 let每次迭代都创建一个新的绑定, 而 var 全程只有一个 i。 这题和闭包(#298)连着考。

In one line: var is function-scoped, hoists to undefined and can be redeclared; let / const are block-scoped, have a TDZ and cannot be redeclared; const on top of that cannot be reassigned.

varletconst
ScopeFunctionBlock {}Block
Read before the declarationundefinedthrows ReferenceErrorthrows ReferenceError
RedeclareAllowedNoNo
ReassignAllowedAllowedNo
Lands on windowYes at top levelNoNo

The TDZ (temporal dead zone) is the stretch between the start of the block and the let / const line itself. The variable really is hoisted, but flagged as “not usable yet”, so reading it throws instead of handing you undefined. That is deliberate — it makes mistakes surface early.

Follow-up: “Can you change the properties of a const object?” — yes. const locks the binding (the name cannot point at anything else), not the value. To freeze the contents, use Object.freeze().

Another follow-up — the classic loop question: for (var i…) with setTimeout prints three 3s; let prints 0 1 2. Because let creates a fresh binding on every iteration, while var has one single i the whole way through. This gets asked together with closures (#298).

JavaScript三个必背的例子Three examples worth memorising示意Illustrative
1// 经典循环题
2for (var i = 0; i < 3; i++) setTimeout(() => console.log(i));
3// 3 3 3 —— 只有一个 i,回调跑的时候它已经是 3
4
5for (let j = 0; j < 3; j++) setTimeout(() => console.log(j));
6// 0 1 2 —— 每次迭代一个新的 j
7
8// TDZ
9console.log(a); // undefined var 提升成 undefined
10var a = 1;
11
12console.log(b); // ReferenceError: Cannot access 'b' before initialization
13let b = 1;
14
15// const 锁绑定,不锁内容
16const o = { n: 1 };
17o.n = 2; // ✓ 可以
18o = { n: 3 }; // ✗ TypeError: Assignment to constant variable
1// The classic loop question
2for (var i = 0; i < 3; i++) setTimeout(() => console.log(i));
3// 3 3 3 —— there is only one i, and by the time the callbacks run it is 3
4
5for (let j = 0; j < 3; j++) setTimeout(() => console.log(j));
6// 0 1 2 —— every iteration gets a new j
7
8// TDZ
9console.log(a); // undefined var is hoisted as undefined
10var a = 1;
11
12console.log(b); // ReferenceError: Cannot access 'b' before initialization
13let b = 1;
14
15// const locks the binding, not the contents
16const o = { n: 1 };
17o.n = 2; // ✓ allowed
18o = { n: 3 }; // ✗ TypeError: Assignment to constant variable
JavaScriptJavaScript#284

传值 vs 传引用

Pass by Value vs Pass by Reference

看答案Show answer

一句话(这句要说准):JavaScript 永远是按值传递—— 只是当值是对象时,传的那个「值」是一个地址。 严格说叫 pass by sharing

为什么这个说法重要?因为它一句话解释了两个看似矛盾的现象:

  • 函数里 obj.n = 2外面会变—— 因为两个变量指着同一个对象。
  • 函数里 obj = { n: 2 }外面不变—— 因为你只是让函数内部那个参数变量指向了新对象, 外面的变量还指着旧的。

如果真是「传引用」,第二种情况外面也会变。所以是传值。

会追问:「怎么避免改到外面?」—— 先复制。{ ...obj } /arr.slice()浅拷贝(只复制一层,嵌套对象还是共享); 深拷贝用 structuredClone(obj)(现代浏览器和 Node 17+ 原生支持, 比 JSON.parse(JSON.stringify()) 强 —— 后者会丢掉 undefined、函数、Date 会变字符串、还处理不了循环引用)。

这题和 React 直接相关:React 判断 state 变没变是比引用, 所以「改了对象属性但界面不动」就是这个原理 —— 必须造新对象。

In one line — get this sentence exactly right: JavaScript is always pass by value. It is just that when the value happens to be an object, the “value” being passed is an address. The precise name for this is pass by sharing.

Why does the wording matter? Because it explains two things that look like a contradiction:

  • obj.n = 2 inside the function does show up outside — both variables point at the same object.
  • obj = { n: 2 } inside the function does not show up outside — all you did was point the parameter variable inside the function at a new object; the outer variable still points at the old one.

If this really were pass by reference, the second case would change the outside too. So it is pass by value.

Follow-up: “How do you avoid mutating the caller’s data?” — copy it first. { ...obj } and arr.slice() are shallow copies (one level only; nested objects are still shared). For a deep copy use structuredClone(obj) — native in modern browsers and Node 17+, and better than JSON.parse(JSON.stringify()), which drops undefined and functions, turns Date into a string, and cannot handle circular references at all.

This one ties straight into React: React compares references to decide whether state changed, which is exactly why “I changed a property and the UI did not move” happens — you have to build a new object.

JavaScript改属性 vs 换指向Changing a property vs pointing somewhere else示意Illustrative
1function mutate(o) { o.n = 2; } // 改属性
2function reassign(o) { o = { n: 3 }; } // 换指向
3
4const obj = { n: 1 };
5mutate(obj); console.log(obj.n); // 2 ← 外面变了
6reassign(obj); console.log(obj.n); // 2 ← 外面没变(不是 3)
7
8// 浅拷贝只管一层
9const a = { x: 1, inner: { y: 2 } };
10const b = { ...a };
11b.x = 9; console.log(a.x); // 1 ✓ 独立
12b.inner.y = 9; console.log(a.inner.y); // 9 ✗ 还是共享的
13
14const c = structuredClone(a); // 深拷贝,嵌套也独立
1function mutate(o) { o.n = 2; } // change a property
2function reassign(o) { o = { n: 3 }; } // point at something else
3
4const obj = { n: 1 };
5mutate(obj); console.log(obj.n); // 2 ← the outside changed
6reassign(obj); console.log(obj.n); // 2 ← the outside did not change (not 3)
7
8// A shallow copy only covers one level
9const a = { x: 1, inner: { y: 2 } };
10const b = { ...a };
11b.x = 9; console.log(a.x); // 1 ✓ independent
12b.inner.y = 9; console.log(a.inner.y); // 9 ✗ still shared
13
14const c = structuredClone(a); // a deep copy, so nested objects are separate too
JavaScriptJavaScript#286

Set vs Array

Set vs Array

看答案Show answer

一句话:Set元素唯一、查找是 O(1)、没有下标; 数组允许重复、有顺序和下标、includes 是 O(n)。

ArraySet
重复元素允许自动去重
查「在不在」includes O(n)has O(1)
下标访问arr[0]没有
取长度lengthsize
map / filter没有,要先转数组

什么时候用 Set:去重、以及在循环里反复判断「见过没有」—— 后者是性能差别最大的场景, 数组的 includes 会让复杂度从 O(n) 变 O(n²)。

会追问:「Set 去重能去掉重复的对象吗?」——不能。Set 用的是SameValueZero(≈===), 两个内容一样的对象是不同的引用。 要按内容去重得自己用Map 按某个 key 存。
NaN 呢?」—— Set 里NaN 只会存一个, 这是 SameValueZero=== 唯一的差别。

In one line: a Set holds unique elements, checks membership in O(1), and has no indices; an array allows duplicates, has order and indices, and its includes is O(n).

ArraySet
DuplicatesAllowedDropped automatically
“Is it in there?”includes O(n)has O(1)
Index accessarr[0]None
Lengthlengthsize
map / filterYesNo — convert to an array first

When to use a Set: deduping, and answering “have I seen this before?” inside a loop — the second is where the performance gap is biggest, because an array includes turns O(n) into O(n²).

Follow-up: “Can a Set dedupe identical objects?” — no. A Set uses SameValueZero (≈===), and two objects with the same contents are still two different references. To dedupe by content, key them yourself in a Map.
“What about NaN?” — a Set stores NaN only once. That is the only difference between SameValueZero and ===.

JavaScriptSet 的两个真实用途Two real uses for Set示意Illustrative
1// 去重一行
2const unique = [...new Set([1, 2, 2, 3])]; // [1, 2, 3]
3
4// 循环里判重:O(n²) -> O(n)
5const seen = new Set();
6for (const x of list) {
7 if (seen.has(x)) continue; // O(1),换成 arr.includes 就是 O(n)
8 seen.add(x);
9}
10
11// 去不掉对象
12new Set([{ id: 1 }, { id: 1 }]).size; // 2 ← 引用不同
13
14// 按内容去重要用 Map
15const byId = new Map(items.map((i) => [i.id, i]));
16const deduped = [...byId.values()];
1// Dedupe in one line
2const unique = [...new Set([1, 2, 2, 3])]; // [1, 2, 3]
3
4// Checking for duplicates in a loop: O(n²) -> O(n)
5const seen = new Set();
6for (const x of list) {
7 if (seen.has(x)) continue; // O(1); arr.includes here would be O(n)
8 seen.add(x);
9}
10
11// It cannot dedupe objects
12new Set([{ id: 1 }, { id: 1 }]).size; // 2 ← different references
13
14// To dedupe by content, use a Map
15const byId = new Map(items.map((i) => [i.id, i]));
16const deduped = [...byId.values()];
JavaScriptJavaScript#287

Map vs Object

Map vs Object

看答案Show answer

一句话:Map键可以是任何类型保证插入顺序、有 size、 能直接遍历;对象的键只能是字符串或 symbol, 而且带着一条原型链。

ObjectMap
键的类型字符串 / symbol(数字会被转成字符串任何值,包括对象和函数
顺序整数键会被排序,其余按插入严格按插入顺序
大小Object.keys(o).lengthmap.size
遍历要先 Object.entries本身可迭代,直接 for…of
原型污染有风险 —— o["toString"]本来就有值没有,Map 是干净的
JSON 序列化直接可以不行,要先转数组

怎么选:

  • 用 Object:结构固定的记录 (一个用户、一份配置),要 JSON 序列化, 字段名写死在代码里。
  • 用 Map:键是动态的、会频繁增删、 数量大、键不是字符串。

会追问:「为什么说对象有原型污染风险?」—— 因为空对象也「有」toStringconstructor 这些继承来的键。 拿对象当字典时,if (dict[key]) 遇到用户输入"constructor" 会误判成存在。Map 没这个问题, 非要用对象就 Object.create(null)
WeakMap 呢?」—— 键必须是对象, 而且不阻止垃圾回收。 适合给对象挂额外数据又不想造成内存泄漏。

In one line: a Map takes keys of any type, guarantees insertion order, has size, and is iterable on its own; an object’s keys can only be strings or symbols, and it drags a prototype chain along with it.

ObjectMap
Key typesString / symbol (numbers become strings)Any value, objects and functions included
OrderInteger keys get sorted, the rest are insertion orderStrictly insertion order
SizeObject.keys(o).lengthmap.size
IterationNeeds Object.entries firstIterable itself — for…of just works
Prototype pollutionA risk — o["toString"] already has a valueNone; a Map is clean
JSON serialisationWorks directlyNo — convert to an array first

How to choose:

  • Use an Object for records with a fixed shape (one user, one config), for anything you serialise to JSON, and when the field names are written into the code.
  • Use a Map when the keys are dynamic, entries come and go often, there are a lot of them, or the keys are not strings.

Follow-up: “Why is an object a prototype pollution risk?” — because even an empty object “has” inherited keys like toString and constructor. Use an object as a dictionary and if (dict[key]) reports a hit when the user types "constructor". A Map does not have this problem; if you must use an object, build it with Object.create(null).
“And WeakMap?” — its keys must be objects, and it does not hold off garbage collection. Good for hanging extra data on an object without leaking memory.

JavaScript两个真实会踩的差别Two differences you will actually hit示意Illustrative
1// 对象的键会被转成字符串
2const o = {};
3o[1] = "a";
4o["1"] = "b";
5console.log(o); // { "1": "b" } ← 只有一个键!
6
7const m = new Map();
8m.set(1, "a").set("1", "b");
9console.log(m.size); // 2 ← 数字 1 和字符串 "1" 是不同的键
10
11// 原型污染
12const dict = {};
13console.log(dict["toString"]); // ƒ toString() ← 凭空有值
14console.log(new Map().get("toString")); // undefined ✓ 干净
15
16// Map 不能直接 JSON
17JSON.stringify(m); // "{}" ← 全丢了
18JSON.stringify([...m]); // '[[1,"a"],["1","b"]]' ✓
1// Object keys are converted to strings
2const o = {};
3o[1] = "a";
4o["1"] = "b";
5console.log(o); // { "1": "b" } ← only one key!
6
7const m = new Map();
8m.set(1, "a").set("1", "b");
9console.log(m.size); // 2 ← the number 1 and the string "1" are different keys
10
11// Prototype pollution
12const dict = {};
13console.log(dict["toString"]); // ƒ toString() ← a value appears from nowhere
14console.log(new Map().get("toString")); // undefined ✓ clean
15
16// A Map does not turn into JSON directly
17JSON.stringify(m); // "{}" ← everything is lost
18JSON.stringify([...m]); // '[[1,"a"],["1","b"]]' ✓
JavaScriptJavaScript#285

有几种定义函数的方式

How many ways to define a function

看答案Show answer

一句话:五种 —— 函数声明、函数表达式、箭头函数、Function 构造器、以及对象/类里的方法简写。

但面试真正想听的是它们的差别, 尤其是前三种:

提升thisarguments能否 new
函数声明整体提升,声明前可调用调用时决定
函数表达式只提升变量名调用时决定
箭头函数只提升变量名定义时的外层 this没有不能

箭头函数不是「更短的 function」—— 它没有自己的 this、 没有 arguments、 不能当构造器、没有 prototype。 所以对象方法里想用this 指向该对象,就不能用箭头函数

会追问:Function 构造器为什么不用?」—— 它接收字符串当函数体,相当于 eval: 有注入风险、拿不到闭包、 而且引擎没法优化。知道它存在但说明不该用就是正确答案。

In one line: five — function declaration, function expression, arrow function, the Function constructor, and method shorthand inside an object or class.

But what the interview actually wants is the differences, especially between the first three:

HoistingthisargumentsCan you new it
Function declarationHoisted whole, callable before its lineDecided at call timeYesYes
Function expressionOnly the variable name is hoistedDecided at call timeYesYes
Arrow functionOnly the variable name is hoistedThe enclosing this where it was writtenNoNo

An arrow function is not “a shorter function” — it has no this of its own, no arguments, cannot be a constructor, and has no prototype. So if an object method needs this to be that object, it cannot be an arrow function.

Follow-up: “Why does nobody use the Function constructor?” — it takes the body as a string, which makes it eval in disguise: an injection risk, no access to the surrounding closure, and nothing the engine can optimise. Knowing it exists and saying it should not be used is the right answer.

JavaScript三种写法的实际差别How the three forms actually differ示意Illustrative
1sayHi(); // ✓ 能跑 —— 函数声明整体提升
2function sayHi() { console.log("hi"); }
3
4sayHey(); // ✗ TypeError: sayHey is not a function
5var sayHey = function () {}; // 只提升了变量名,此刻还是 undefined
6
7// 箭头函数的 this 是定义时的外层 this
8const obj = {
9 name: "A",
10 arrow: () => console.log(this.name), // undefined ← 外层是模块/window
11 normal() { console.log(this.name); }, // "A" ← 调用时决定
12};
1sayHi(); // ✓ runs —— a function declaration is hoisted whole
2function sayHi() { console.log("hi"); }
3
4sayHey(); // ✗ TypeError: sayHey is not a function
5var sayHey = function () {}; // only the name was hoisted; it is still undefined here
6
7// An arrow function's this is the outer this at the place it was defined
8const obj = {
9 name: "A",
10 arrow: () => console.log(this.name), // undefined ← the outside is the module/window
11 normal() { console.log(this.name); }, // "A" ← decided at call time
12};

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