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 道 · 第 3 / 9 页。105 of 105 questions · page 3 / 9.
JavaScriptJavaScript#290

什么是一等函数

What is a first class function

看答案Show answer

一句话:「一等」说的是函数和普通值地位相同—— 能赋给变量、能当参数传、能当返回值、 能放进数组和对象。

这是语言的性质,不是某个函数的性质。说「JavaScript 有一等函数」是对的, 说「这是一个一等函数」就怪了。

为什么重要:一等函数是回调、高阶函数、闭包、 函数式编程的前提arr.map(fn) 之所以能写, 就是因为 fn 可以当参数。

会追问:下面三题连着问 —— 一等(能当值)→ 一阶(不碰函数)→ 高阶(收或返函数)。先把这三个词分清。

In one line: “first class” means functions have the same standing as any other value — you can assign one to a variable, pass one as an argument, return one, and store them in arrays and objects.

This is a property of the language, not of a particular function. “JavaScript has first class functions” is correct; “this is a first class function” sounds wrong.

Why it matters: first class functions are the precondition for callbacks, higher order functions, closures and functional programming. You can only write arr.map(fn) because fn can be an argument in the first place.

Follow-up: the next three come as a set — first class (works as a value) → first order (touches no functions) → higher order (takes or returns a function). Get those three terms apart first.

JavaScriptJavaScript#291

什么是一阶函数

What is a first order function

看答案Show answer

一句话:参数里没有函数、返回值也不是函数的普通函数。

(a, b) => a + b 是一阶的。arr.map(fn) 不是 —— 它收了个函数。

这个词单独考的时候很少,它存在的意义就是和「高阶」形成对照。 答题时一句话说完,然后主动接到高阶函数上, 显得你知道这三题是一组。

In one line: an ordinary function that takes no function as an argument and returns no function either.

(a, b) => a + b is first order. arr.map(fn) is not — it takes a function.

The term almost never gets asked on its own. It exists to give “higher order” something to contrast with. Answer it in one sentence, then move on to higher order functions yourself — that shows you know these three questions travel together.

JavaScriptJavaScript#292

什么是高阶函数

What is a higher order function

看答案Show answer

一句话:参数里接收函数,或者返回一个函数—— 满足任一条就是高阶函数。

你天天在用:map / filter /reduce / forEach /sort(接收函数),setTimeout / addEventListener(接收函数),bind(返回函数)。

返回函数那一类才是考点, 因为它是柯里化、防抖节流、 和 React HOC 的共同底子:

会追问:「写一个防抖」—— 这是最常见的现场编码题, 本质就是「返回一个函数 + 闭包记住 timer」。 注意清理和 this 转发, 很多人会漏。

In one line: it takes a function as an argument, or it returns one — either one on its own makes it a higher order function.

You use them every day: map / filter / reduce / forEach / sort (they take a function), setTimeout / addEventListener (they take a function), and bind (it returns one).

The returning kind is what gets tested, because it is the shared foundation under currying, debounce and throttle, and React HOCs:

Follow-up: “Write a debounce” — the most common live-coding task there is, and at heart it is just “return a function and let the closure remember the timer”. Watch out for clearing the previous timer and forwarding this; plenty of people drop those.

JavaScript高阶函数的三种典型形态Three typical shapes of a higher-order function示意Illustrative
1// 返回函数:一个能记住次数的计数器
2function makeCounter() {
3 let n = 0; // 被闭包保住
4 return () => ++n;
5}
6const next = makeCounter();
7next(); next(); // 2
8
9// 高频现场题:防抖
10function debounce(fn, delay = 300) {
11 let timer = null; // 闭包里的状态
12 return function (...args) {
13 clearTimeout(timer); // 每次进来先撤销上一次
14 timer = setTimeout(() => fn.apply(this, args), delay);
15 }; // 用 function 而不是箭头,才能转发 this
16}
17
18// React 的 HOC 也是高阶函数:收组件、返组件
19const withLogger = (Comp) => (props) => {
20 console.log("render", Comp.name);
21 return <Comp {...props} />;
22};
1// Returning a function: a counter that remembers its count
2function makeCounter() {
3 let n = 0; // kept alive by the closure
4 return () => ++n;
5}
6const next = makeCounter();
7next(); next(); // 2
8
9// A very common live-coding question: debounce
10function debounce(fn, delay = 300) {
11 let timer = null; // state that lives in the closure
12 return function (...args) {
13 clearTimeout(timer); // every call first cancels the previous one
14 timer = setTimeout(() => fn.apply(this, args), delay);
15 }; // use function, not an arrow, to forward this
16}
17
18// A React HOC is a higher-order function too: takes a component, returns a component
19const withLogger = (Comp) => (props) => {
20 console.log("render", Comp.name);
21 return <Comp {...props} />;
22};
JavaScriptJavaScript#293

什么是纯函数

What is a pure function

看答案Show answer

一句话:两个条件。同样的输入永远给同样的输出; ② 没有副作用(不改外部变量、 不改参数、不发请求、不写 DOM、不打日志)。

不纯的常见来源:Math.random()new Date()、 读写全局变量、arr.push() 改了传进来的数组、console.log

为什么面试爱问:纯函数好测(给输入断输出,不用搭环境)、好缓存(输入一样就能复用结果, 这就是 memoization)、好并发(没有共享状态就没有竞争)。

直接连到 React:

  • 组件的渲染函数必须是纯的—— 同样的 props 和 state 要渲染出同样的 UI。 这是 StrictMode 故意渲染两次能发现问题的原因(见 #332)。
  • Redux 的 reducer 必须是纯的—— 不然时间旅行调试和重放就不成立(见 #352)。
  • 不可变更新之所以是铁律, 就是因为「改传进来的数组」会让函数不纯。

会追问:「那副作用写哪?」—— React 里写useEffect, Redux 里写中间件(thunk / saga)。把纯逻辑和副作用分开是这套设计的核心。

In one line: two conditions. the same input always produces the same output; ② no side effects (it does not touch outer variables, mutate its arguments, fire requests, write to the DOM, or log).

Where impurity usually creeps in: Math.random(), new Date(), reading or writing globals, an arr.push() on the array you were handed, and console.log.

Why interviewers like this one: pure functions are easy to test (give input, assert output, no setup), easy to cache (same input, reuse the result — that is memoization), and easy to run concurrently (no shared state means no races).

Straight into React:

  • A component’s render function must be pure — the same props and state have to produce the same UI. That is why StrictMode renders twice on purpose and catches things (see #332).
  • A Redux reducer must be pure — otherwise time-travel debugging and replay do not hold up (see #352).
  • Immutable updates are a hard rule precisely because mutating the array you were given makes the function impure.

Follow-up: “So where do the side effects go? ” — useEffect in React, middleware (thunk or saga) in Redux. Keeping pure logic and side effects apart is the core of that design.

JavaScript怎么把不纯改纯How to turn an impure function into a pure one示意Illustrative
1// ✗ 不纯:改了传进来的数组
2function addItem(list, item) {
3 list.push(item);
4 return list;
5}
6
7// ✓ 纯:返回新数组
8function addItem(list, item) {
9 return [...list, item];
10}
11
12// ✗ 不纯:输出取决于外部
13let rate = 0.1;
14const tax = (n) => n * rate;
15
16// ✓ 纯:所有依赖都从参数进来
17const tax = (n, rate) => n * rate;
1// ✗ Impure: it changes the array that was passed in
2function addItem(list, item) {
3 list.push(item);
4 return list;
5}
6
7// ✓ Pure: it returns a new array
8function addItem(list, item) {
9 return [...list, item];
10}
11
12// ✗ Impure: the output depends on something outside
13let rate = 0.1;
14const tax = (n) => n * rate;
15
16// ✓ Pure: every input arrives as a parameter
17const tax = (n, rate) => n * rate;
JavaScriptJavaScript#294

"use strict" 是干什么的

What is "use strict"

看答案Show answer

一句话:开启严格模式 —— 把一批「静默出错」的写法变成直接抛错, 并禁掉一些历史包袱。

具体管四件事(记两三条就够答):

  • 禁止隐式全局变量。x = 1 忘了 let, 非严格下会悄悄挂到 window, 严格下抛 ReferenceError这是它最大的价值。
  • 函数里的 thisundefined, 而不是 window—— 能让「忘了 bind」当场暴露。
  • 给只读属性赋值、删不可删的属性会抛错而不是静默失败。
  • with、禁重复参数名、arguments 不再和参数联动。

会追问:「现在还要手写吗?」—— 基本不用了ES 模块和 class 内部自动就是严格模式。 所以只有写老式 <script>或 CommonJS 时才需要手写。 能答出这条说明你知道现状。

顺带一个真实关联:这就是「Object.freeze 之后修改会抛错」的原因 —— 非严格模式下它只是静默失败。 我们的评论树那道题就是靠这个来验证不可变性的。

In one line: it switches on strict mode — a batch of things that used to fail silently now throw, and some historical baggage is banned outright.

It covers four things; two or three of them are enough to answer:

  • No implicit globals. x = 1 with a forgotten let quietly lands on window in sloppy mode, and throws ReferenceError in strict mode. This is its biggest single win.
  • this inside a plain function is undefined instead of window — so a forgotten bind throws right there.
  • Assigning to a read-only property, or deleting one that cannot be deleted, throws instead of failing silently.
  • with is banned, duplicate parameter names are banned, and arguments no longer tracks the parameters.

Follow-up: “Do you still type it by hand?” — almost never: ES modules and class bodies are strict automatically. You only write it for old-style <script> tags or CommonJS. Saying so shows you know where things stand today.

One real connection worth adding: this is why writing to an object after Object.freeze throws — in sloppy mode it just fails silently. Our comment-tree exercise leans on exactly that to verify immutability.

JavaScriptJavaScript#295

作用域有哪几种

What are the different type of scopes

看答案Show answer

一句话:四种 —— 全局、函数、块、模块。

  • 全局—— 最外层。浏览器里var 声明的会挂到 window
  • 函数—— var和函数参数的地盘,整个函数体内都可见
  • —— 任意一对 {}只对 let / const/ class 有效var 无视它。
  • 模块—— 每个 ES 模块文件自己一个作用域, 顶层声明不会污染全局

顺带一个常被忽略的:catch (e)e也有自己的作用域。

会追问:「函数作用域和块作用域差在哪,举个例子?」——ifvar 声明的变量出了 if 还能访问let 就不能。 这是把老代码从 var改成 let 时最常见的破坏点。

In one line: four — global, function, block and module.

  • Global — the outermost level. In a browser, a var declared here lands on window.
  • Function — the territory of var and the parameters, visible anywhere in the function body.
  • Block — any pair of {}. It only binds let / const / class; var ignores it completely.
  • Module — every ES module file gets a scope of its own, so top-level declarations do not pollute the global scope.

One that people forget: the e in catch (e) has its own scope too.

Follow-up: “Give me an example of function scope against block scope” — a var declared inside an if is still readable after the if; a let is not. That is the thing you break most often when converting old var code to let.

JavaScript函数作用域 vs 块作用域Function scope vs block scope示意Illustrative
1function f() {
2 if (true) {
3 var a = 1; // 函数作用域
4 let b = 2; // 块作用域
5 }
6 console.log(a); // 1 ✓ var 无视 {}
7 console.log(b); // ReferenceError
8}
1function f() {
2 if (true) {
3 var a = 1; // function scope
4 let b = 2; // block scope
5 }
6 console.log(a); // 1 ✓ var ignores {}
7 console.log(b); // ReferenceError
8}
JavaScriptJavaScript#296

什么是变量提升

What is hoisting

看答案Show answer

一句话:编译阶段引擎会先扫一遍, 把声明登记到作用域里, 所以「在声明之前引用」不一定报错 ——但只提升声明,不提升赋值

四种情况分清就够答:

声明前访问的结果
函数声明整个函数都能用(可以直接调用)
varundefined
let / constReferenceError(TDZ)
classReferenceError(也有 TDZ)

let 不提升」是个常见错误说法。确实提升了—— 否则内层的 let x不会遮蔽外层的 x。 只是它被标成「未初始化」,访问就抛错。能纠正这个说法很加分。

会追问:「函数声明和函数表达式呢?」——function f(){} 整体提升;var f = function(){}只提升 f(值是 undefined), 提前调用会得到TypeError: f is not a function注意这两个报错不一样, 这个细节常用来分辨背没背过。

In one line: during compilation the engine scans the code first and registers the declarations in the scope, so referring to something before its line does not always throw — but only the declaration is hoisted, never the assignment.

Four cases; keeping them apart is enough:

What you get before the declaration
Function declarationUsable throughout (you can call it)
varundefined
let / constthrows ReferenceError (TDZ)
classthrows ReferenceError (a TDZ as well)

let is not hoisted” is a common mistake. It is hoisted — otherwise an inner let x would not shadow an outer x. It is just marked uninitialised, so reading it throws. Correcting this scores well.

Follow-up: “What about function declarations against function expressions?” — function f(){} is hoisted whole; var f = function(){} hoists only f (whose value is undefined), so calling it early gives you TypeError: f is not a function. Note the two errors are different — that detail is how they tell recitation from understanding.

JavaScript四种提升行为Four hoisting behaviours示意Illustrative
1console.log(fn()); // "ok" 函数声明整体提升
2console.log(v); // undefined var 提升成 undefined
3console.log(l); // ReferenceError(TDZ)
4
5function fn() { return "ok"; }
6var v = 1;
7let l = 2;
8
9// 两种「不是函数」的报错要分清
10foo(); // TypeError: foo is not a function
11var foo = function () {};
12
13bar(); // ReferenceError: Cannot access 'bar' ...
14let bar = function () {};
1console.log(fn()); // "ok" a function declaration is hoisted whole
2console.log(v); // undefined var is hoisted as undefined
3console.log(l); // ReferenceError (TDZ)
4
5function fn() { return "ok"; }
6var v = 1;
7let l = 2;
8
9// Tell the two "not a function" errors apart
10foo(); // TypeError: foo is not a function
11var foo = function () {};
12
13bar(); // ReferenceError: Cannot access 'bar' ...
14let bar = function () {};
JavaScriptJavaScript#297

什么是作用域链

What is the scope chain?

看答案Show answer

一句话:找一个变量时,先在当前作用域找,找不到就往外一层, 一直找到全局,还没有就报ReferenceError。 这条「由内到外」的路径就是作用域链。

两个关键性质:

  • 只能往外找,不能往里找。外层看不见内层的变量。
  • 链在函数「定义」时就定下来了, 和在哪里「调用」无关—— 这叫词法作用域(静态作用域)。 这一句是本题的真考点,也是闭包的原理。

会追问:「那 this 也是这样吗?」——不是,这是最容易混的地方。变量查找是词法的(看定义在哪),this 是动态的(看怎么调用的)。 箭头函数的 this之所以「像变量一样」, 正因为它不自己定义 this, 而是顺着作用域链去外层拿。

In one line: to resolve a variable, JS looks in the current scope, then one level out, and keeps going until the global scope; if it is still not there you get a ReferenceError. That inside-out path is the scope chain.

Two properties that matter:

  • It only looks outward, never inward. An outer scope cannot see an inner one’s variables.
  • The chain is fixed where the function is defined, not where it is called — this is lexical (static) scoping. That one sentence is what the question is really testing, and it is how closures work.

Follow-up: “Does this behave the same way?” — no, and this is the easiest thing to mix up. Variable lookup is lexical (where it was written), this is dynamic (how it was called). An arrow function’s this feels “variable-like” exactly because it does not define a this of its own and walks the scope chain outward to find one.

JavaScript由内到外,且在定义时确定From inside out, and fixed where the function is defined示意Illustrative
1const g = "全局";
2
3function outer() {
4 const o = "外层";
5 function inner() {
6 const i = "内层";
7 console.log(i, o, g); // 三层都找得到:内 -> 外 -> 全局
8 }
9 inner();
10 // console.log(i); // ✗ 外层看不见内层
11}
12
13// 词法作用域:链看「定义在哪」,不看「在哪调用」
14const x = "定义时的 x";
15function show() { console.log(x); }
16
17function run() {
18 const x = "调用处的 x";
19 show(); // "定义时的 x" ← 不是调用处那个
20}
21run();
1const g = "global";
2
3function outer() {
4 const o = "outer";
5 function inner() {
6 const i = "inner";
7 console.log(i, o, g); // all three are found: inner -> outer -> global
8 }
9 inner();
10 // console.log(i); // ✗ the outer scope cannot see the inner one
11}
12
13// Lexical scope: the chain follows where it was defined, not where it is called
14const x = "the x at the definition site";
15function show() { console.log(x); }
16
17function run() {
18 const x = "the x at the call site";
19 show(); // "the x at the definition site" ← not the call-site one
20}
21run();
JavaScriptJavaScript#298

什么是闭包

What is a closure

看答案Show answer

一句话:函数记住了它定义时所在的作用域—— 即使外层函数已经执行完了, 里面的变量也还活着,因为这个函数还在引用它们。

为什么会这样?因为作用域链在定义时就绑好了(#297)。 外层函数返回后, 它的变量本该被回收, 但只要还有函数引用着, 垃圾回收就不会动它

四个真实用途(面试要的是用途,不是定义):

  • 私有状态—— 计数器、缓存。 外部拿不到那个变量,只能通过你暴露的方法改。
  • 防抖 / 节流—— 用闭包存 timer。
  • 柯里化 / 偏函数—— 记住已经传进来的参数(#299)。
  • React 的 Hooks 全靠它——useState 返回的setStateuseEffect 里的回调, 都是闭包捕获了那一次渲染的值。「过期闭包」这个 bug 就是它的副作用。

会追问的两道题:

循环里的 setTimeout(见 #282)。var 全程只有一个 i, 三个闭包共享它;let每次迭代新建一个绑定,所以各自记住自己那份。

闭包会不会造成内存泄漏?会 —— 如果闭包一直存活(比如挂在全局或未移除的事件监听里), 它引用的整个作用域都回收不了。解法是及时解绑, 这也是 React 里 useEffect必须写清理函数的原因之一。

In one line: a function remembers the scope it was defined in — even after the outer function has finished, the variables inside are still alive, because that function is still referencing them.

Why does that happen? Because the scope chain is wired up at definition time (#297). Once the outer function returns, its variables would normally be collected, but garbage collection leaves them alone as long as some function still references them.

Four real uses — the interview wants uses, not the definition:

  • Private state — counters, caches. Nothing outside can reach the variable; it can only go through the methods you expose.
  • Debounce and throttle — the closure holds the timer.
  • Currying and partial application — remembering the arguments received so far (#299).
  • Every React Hook rests on it — the setState returned by useState, and the callback inside useEffect, are closures that captured the values of one particular render. The “stale closure” bug is the flip side of that.

Two follow-ups to expect:

setTimeout inside a loop (see #282). With var there is one i for the whole loop and all three closures share it; let creates a new binding per iteration, so each closure remembers its own.

Can a closure leak memory? It can — if the closure stays alive (hanging off a global, or an event listener you never removed), the entire scope it references cannot be collected. The fix is to unsubscribe in time, which is one of the reasons a React useEffect needs its cleanup function.

JavaScript闭包的两道必考题Two closure questions that always come up示意Illustrative
1// 私有状态:外面拿不到 count,只能通过方法改
2function createCounter() {
3 let count = 0; // 外面访问不到
4 return {
5 inc: () => ++count,
6 get: () => count,
7 };
8}
9const c = createCounter();
10c.inc(); c.inc();
11c.get(); // 2
12c.count; // undefined ← 真的私有
13
14// 经典面试题:这里会打印什么?
15function f() {
16 const fns = [];
17 for (var i = 0; i < 3; i++) fns.push(() => i);
18 return fns.map((fn) => fn());
19}
20f(); // [3, 3, 3] —— 三个闭包共享同一个 i
21 // 把 var 换成 let 就是 [0, 1, 2]
1// Private state: the outside cannot reach count, only the methods change it
2function createCounter() {
3 let count = 0; // not reachable from outside
4 return {
5 inc: () => ++count,
6 get: () => count,
7 };
8}
9const c = createCounter();
10c.inc(); c.inc();
11c.get(); // 2
12c.count; // undefined ← genuinely private
13
14// A classic interview question: what does this print?
15function f() {
16 const fns = [];
17 for (var i = 0; i < 3; i++) fns.push(() => i);
18 return fns.map((fn) => fn());
19}
20f(); // [3, 3, 3] —— the three closures share one i
21 // change var to let and you get [0, 1, 2]
JavaScriptJavaScript#299

什么是柯里化

What is currying

看答案Show answer

一句话:把「一次收 n 个参数」的函数 改成「每次收一个、返回一个新函数」, 收满了才真正计算。

add(1, 2, 3) 变成add(1)(2)(3)。 实现靠闭包记住已经收到的参数

有什么用(别只说「炫技」):

  • 参数复用——const log = level => msg => console.log(`[${level}] ${msg}`),然后 const warn = log("WARN")
  • 延迟执行—— 参数没收齐就不干活, 适合配置式 API。
  • 函数组合—— 组合要求每个函数只收一个参数, 柯里化正好把多参函数改造成这个形状。

会追问:「柯里化和偏函数(partial application)什么区别?」—— 柯里化严格一次一个; 偏函数是一次固定几个、剩下的以后给bind 就是偏函数)。 这个区分问得不少。

还会让你手写一个通用 curry—— 思路是:参数够了就调,不够就返回一个继续收的函数。

In one line: take a function that receives n arguments at once and reshape it into one that takes one at a time and returns a new function, only computing once they are all in.

add(1, 2, 3) becomes add(1)(2)(3). The implementation rests on a closure remembering the arguments received so far.

What it is good for — do not just say “showing off”:

  • Reusing arguments const log = level => msg => console.log(`[${level}] ${msg}`), then const warn = log("WARN").
  • Deferred execution — nothing runs until every argument has arrived, which suits configuration-style APIs.
  • Function composition — composition wants every function to take a single argument, and currying reshapes multi-argument functions into exactly that.

Follow-up: “What is the difference between currying and partial application?” — currying is strictly one argument at a time; partial application fixes a few now and takes the rest later (bind is partial application). This distinction comes up a lot.

They will also ask you to write a generic curry — the idea is: if you have enough arguments, call the function; if not, return one that keeps collecting.

JavaScript柯里化Currying示意Illustrative
1// 手写通用柯里化:参数够了就算,不够就继续收
2function curry(fn) {
3 return function curried(...args) {
4 if (args.length >= fn.length) return fn.apply(this, args);
5 return (...rest) => curried.apply(this, [...args, ...rest]);
6 };
7}
8
9const add = curry((a, b, c) => a + b + c);
10add(1)(2)(3); // 6
11add(1, 2)(3); // 6
12add(1)(2, 3); // 6
13
14// 实际用途:参数复用
15const log = (level) => (msg) => console.log(`[${level}] ${msg}`);
16const warn = log("WARN");
17warn("磁盘快满了"); // [WARN] 磁盘快满了
1// A generic curry by hand: if there are enough arguments, run; otherwise keep collecting
2function curry(fn) {
3 return function curried(...args) {
4 if (args.length >= fn.length) return fn.apply(this, args);
5 return (...rest) => curried.apply(this, [...args, ...rest]);
6 };
7}
8
9const add = curry((a, b, c) => a + b + c);
10add(1)(2)(3); // 6
11add(1, 2)(3); // 6
12add(1)(2, 3); // 6
13
14// A real use: reusing an argument
15const log = (level) => (msg) => console.log(`[${level}] ${msg}`);
16const warn = log("WARN");
17warn("disk almost full"); // [WARN] disk almost full
关键是 fn.length —— 函数声明时的形参个数。注意带默认值或 ...rest 的参数不计入 length,所以这个通用实现对它们不适用。The key is fn.length —— the number of parameters the function declares. Note that a parameter with a default value, and a ...rest parameter, do not count towards length, so this generic implementation does not work for them.
JavaScriptJavaScript#300

什么是 IIFE

What is an IIFE

看答案Show answer

一句话:立即执行函数表达式 (Immediately Invoked Function Expression)—— 定义完马上调用,用来造一个隔离的作用域

为什么要包一层括号?因为以 function 开头的语句会被解析成函数声明,而声明不能直接调用。 外面套括号(或者前面加!+void)把它变成表达式

当年解决什么问题:ES5 没有块作用域和模块, 所有 <script> 共享全局命名空间。 IIFE 是唯一的隔离手段—— jQuery 插件、UMD 打包产物全是这么写的。

会追问(这才是这题的重点):「现在还需要吗?」——基本不需要了: 块作用域 + let 能隔离变量, ES 模块天然有自己的作用域。
还剩两个场合会用到: ① 需要在顶层 await而环境不支持时,包一个(async () => { … })(); ② 打包工具生成的产物里。
我们那道 fetch 变式题里useEffect 内部包的(async () => {…})()就是场合 ①—— effect 不能是 async, 所以用 IIFE 开一个异步作用域。

In one line: an Immediately Invoked Function Expression — defined and called on the spot, in order to create an isolated scope.

Why the extra parentheses? Because a statement that starts with function is parsed as a function declaration, and a declaration cannot be called directly. Wrapping it in parentheses (or putting !, + or void in front) turns it into an expression.

What it solved back then: ES5 had no block scope and no modules, and every <script> shared one global namespace. An IIFE was the only way to isolate anything — jQuery plugins and UMD bundles are all written that way.

Follow-up, and this is the point of the question: “Do you still need it?” — mostly not: block scope plus let isolates variables, and an ES module has a scope of its own already.
Two situations are left: ① you need await at the top level and the environment does not support it, so you wrap a (async () => { … })(); ② inside output generated by a bundler.
The (async () => {…})() inside useEffect in our fetch variant exercise is case ① — an effect cannot be async, so an IIFE opens an async scope for it.

JavaScriptIIFE 与它今天的位置The IIFE and where it fits today示意Illustrative
1// 经典写法
2(function () {
3 var private = "外面看不到";
4})();
5
6// 现在真正还会用到的场合:需要一个异步作用域
7useEffect(() => {
8 (async () => {
9 const res = await fetch(url);
10 // ...
11 })();
12 return () => { /* 清理 */ };
13}, [url]);
14
15// 为什么要括号
16function () {}(); // ✗ SyntaxError —— 被当成函数声明
17(function () {})(); // ✓ 括号让它变成表达式
18!function () {}(); // ✓ 一元运算符也行
19void function () {}();//
1// The classic form
2(function () {
3 var private = "not visible from outside";
4})();
5
6// Where it is still genuinely used: when you need an async scope
7useEffect(() => {
8 (async () => {
9 const res = await fetch(url);
10 // ...
11 })();
12 return () => { /* cleanup */ };
13}, [url]);
14
15// Why the parentheses are needed
16function () {}(); // ✗ SyntaxError —— read as a function declaration
17(function () {})(); // ✓ the parentheses turn it into an expression
18!function () {}(); // ✓ a unary operator works too
19void function () {}();//
JavaScriptJavaScript#302

什么是面向对象编程

What is Object-Oriented Programming (OOP)

看答案Show answer

一句话:把数据和操作数据的方法打包在一起,用对象来组织程序。

四个特征(背下来):

  • 封装—— 内部细节藏起来, 只暴露必要的接口。JS 里用闭包或#private 字段实现。
  • 继承—— 子类复用父类的能力。
  • 多态—— 同一个方法名, 不同对象有不同行为。
  • 抽象—— 只关心「能做什么」, 不关心「怎么做的」。

JS 的特别之处(这才是考点):它是基于原型(prototype)的, 不是基于类的。class 是 ES6 加的语法糖, 底下还是原型链 ——class A extends B 编译后就是设置A.prototype.__proto__ = B.prototype

原型链一句话:访问一个属性时,对象自己没有就去__proto__ 上找, 一层层往上直到 null和作用域链是一个套路, 只是一个查变量、一个查属性。

会追问:「React 为什么从 class 转向函数组件?」—— 因为 UI 更适合用「输入 → 输出」来描述, 而不是「一个有生命周期的对象」; 而且 class 里 this的绑定问题、逻辑按生命周期而不是按关注点拆分, 都是实际痛点(见 #322)。

In one line: bundle data together with the methods that act on it, and organise the program around objects.

Four pillars — memorise these:

  • Encapsulation — hide the internals, expose only the interface callers need. In JS you get it from closures or #private fields.
  • Inheritance — a subclass reuses what the parent can already do.
  • Polymorphism — same method name, different behaviour per object.
  • Abstraction — you care what it can do, not how it does it.

What makes JS different, and this is the real question: it is prototype-based, not class-based. class is syntax sugar added in ES6; the prototype chain is still underneath — class A extends B compiles down to setting A.prototype.__proto__ = B.prototype.

The prototype chain in one line: read a property, and if the object does not have it the lookup walks up __proto__ one level at a time until null. Same idea as the scope chain — one looks up variables, the other looks up properties.

Follow-up: “Why did React move from classes to function components?” — because UI is easier to describe as “input → output” than as an object with a lifecycle. On top of that, this binding and code split by lifecycle instead of by concern were real, daily pain (see #322).

JavaScript四个特征与原型链The four traits and the prototype chain示意Illustrative
1class Animal {
2 #secret = "私有字段,外部访问不到"; // 封装
3 constructor(name) { this.name = name; }
4 speak() { return `${this.name} 发出声音`; }
5}
6
7class Dog extends Animal {
8 speak() { return `${this.name} 汪汪`; } // 多态:覆盖父类方法
9}
10
11new Dog("旺财").speak(); // "旺财 汪汪"
12
13// class 只是语法糖,底下是原型链
14Object.getPrototypeOf(Dog.prototype) === Animal.prototype; // true
15new Dog("x") instanceof Animal; // true
1class Animal {
2 #secret = "a private field, unreachable from outside"; // encapsulation
3 constructor(name) { this.name = name; }
4 speak() { return `${this.name} makes a sound`; } // encapsulation
5}
6
7class Dog extends Animal {
8 speak() { return `${this.name} woof`; } // polymorphism: overrides the parent
9}
10
11new Dog("旺财").speak(); // the name is data, so it stays: <name> woof
12
13// class is only syntax sugar; a prototype chain sits underneath
14Object.getPrototypeOf(Dog.prototype) === Animal.prototype; // true
15new Dog("x") instanceof Animal; // true

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