DrillLab
第 04 / 25 节LESSON 04 / 25约 26 分钟~26 min

函数与作用域十二问12 questions on functions and scope

定义方式、一等/一阶/高阶函数、纯函数、use strict、作用域、hoisting、作用域链、闭包、柯里化、IIFE。Ways to define a function, first class / first order / higher order functions, pure functions, use strict, scope, hoisting, the scope chain, closure, currying, IIFE.

面试 · 第 2 部分Interview · Part 2
这一页有什么On this page13
学完这节你会After this lesson you can
  • 说清闭包是什么、为什么会「记住」外层变量,并举出两个真实用途Explain what a closure is, why it keeps the outer variables, and give two real uses
  • 画出一段代码的作用域链Draw the scope chain for a piece of code
  • 区分函数声明和函数表达式在提升上的差别Tell a function declaration from a function expression by the way each one is hoisted
  • 说明纯函数的两个条件,并解释它为什么让代码好测State the two conditions for a pure function, and explain why it makes code easy to test
这在考试里考什么What the exam does with this

闭包是 JS 面试出现频率第一的题,而且它不是背概念就能过 —— 会让你解释循环里的 setTimeout、或者写一个计数器。hoisting 和作用域链是它的前置知识。纯函数那道会直接连到 React(为什么组件要写成纯的、为什么不能改 props)。Closure is the most frequent question in a JavaScript interview, and reciting the definition is not enough — you will be asked to explain a setTimeout inside a loop, or to write a counter. hoisting and the scope chain come before it. The pure function question leads straight into React: why a component has to be pure, and why you must not change props.

§01

有几种定义函数的方式How many ways are there to define a function?

#285 How many ways to define a function

一句话:五种 —— 函数声明、函数表达式、箭头函数、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};
§02

什么是一等函数What is a first class function?

#290 What is a first class function

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

这是语言的性质,不是某个函数的性质。说「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.

§03

什么是一阶函数What is a first order function?

#291 What is a first order function

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

(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.

§04

什么是高阶函数What is a higher order function?

#292 What is a higher order function

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

你天天在用: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};
§05

什么是纯函数What is a pure function?

#293 What is a pure function

一句话:两个条件。同样的输入永远给同样的输出; ② 没有副作用(不改外部变量、 不改参数、不发请求、不写 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;
§06

"use strict" 是干什么的What does "use strict" do?

#294 What is "use strict"

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

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

  • 禁止隐式全局变量。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.

§07

作用域有哪几种What kinds of scope are there?

#295 What are the different type of scopes

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

  • 全局—— 最外层。浏览器里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}
§08

什么是变量提升What is hoisting?

#296 What is hoisting

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

四种情况分清就够答:

声明前访问的结果
函数声明整个函数都能用(可以直接调用)
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 () {};
§09

什么是作用域链What is the scope chain?

#297 What is the scope chain?

一句话:找一个变量时,先在当前作用域找,找不到就往外一层, 一直找到全局,还没有就报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();
§10

什么是闭包What is a closure?

#298 What is a closure

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

为什么会这样?因为作用域链在定义时就绑好了(#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]
§11

什么是柯里化What is currying?

#299 What is currying

一句话:把「一次收 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.
§12

什么是 IIFEWhat is an IIFE?

#300 What is an IIFE

一句话:立即执行函数表达式 (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 () {}();//
迁移Transfer

换一道题也能用Works on other problems too

考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.

问「解释一下闭包」Asked to explain closure
函数记住定义时的作用域 + 举私有状态和防抖两个用途A function keeps the scope it was defined in; give two uses, private state and debounce
循环里的回调都拿到最后一个值Every callback in a loop sees the last value
闭包共享同一个 var 绑定,换 letThe closures share one var binding; use let
「React 里数值卡住不动」A value in React never changes
过期闭包 —— 闭包捕获的是那次渲染的值A stale closure — it captured the value from that one render
要手写防抖/节流Asked to write debounce or throttle by hand
返回函数 + 闭包存 timer + function 转发 thisReturn a function, keep the timer in the closure, and use a function expression so this is forwarded
声明前调用报 TypeErrorCalling it before the declaration gives a TypeError
函数表达式,只提升了变量名It is a function expression; only the variable name was hoisted
声明前调用报 ReferenceErrorUsing it before the declaration gives a ReferenceError
let/const/class 的 TDZThe temporal dead zone of let, const and class
问纯函数有什么用Asked what a pure function is good for
好测、可缓存、可并发;接到 React 渲染和 reducerEasy to test, safe to cache, safe to run in parallel; connect it to React rendering and to a reducer
对象方法里 this 是 undefinedthis is undefined inside an object method
别用箭头函数写方法Do not write a method as an arrow function
这节的要点What to take away
  1. 函数声明整体提升,函数表达式只提升变量名;箭头函数没有自己的 this、arguments,不能 new。A function declaration is hoisted whole; for a function expression only the variable name is hoisted; an arrow function has no this and no arguments of its own, and cannot be called with new.
  2. 一等(函数能当值)→ 一阶(不碰函数)→ 高阶(收或返函数),这三题是一组。First class (a function can be a value) → first order (does not touch functions) → higher order (takes or returns a function): these three questions belong together.
  3. 纯函数两条件:同输入同输出 + 无副作用;React 渲染函数和 Redux reducer 都必须纯。A pure function has two conditions: the same input gives the same output, and there are no side effects; a React render function and a Redux reducer both have to be pure.
  4. 严格模式主要价值是禁隐式全局;ES 模块和 class 内部自动严格,不用手写。The main value of strict mode is that it forbids accidental globals; ES modules and class bodies are already strict, so you do not write it there.
  5. 四种作用域:全局/函数/块/模块;块作用域只约束 let、const、class。There are four scopes: global, function, block and module; block scope only applies to let, const and class.
  6. let 也会提升,只是处于 TDZ 访问就抛错 —— 说「let 不提升」是错的。let is hoisted too, it just sits in the temporal dead zone where reading it throws — saying that let is not hoisted is wrong.
  7. 作用域链由内到外,且在定义时确定(词法作用域);this 相反,是调用时确定。The scope chain goes from inside out and is fixed where the function is written (lexical scope); this is the opposite, it is decided when the function is called.
  8. 闭包 = 函数记住定义时的作用域;用途是私有状态、防抖、柯里化,以及 React Hooks 的全部基础。A closure is a function that keeps the scope it was defined in; it is used for private state, debounce and currying, and it is the basis of every React hook.
  9. IIFE 当年是唯一的隔离手段,现在只剩「需要异步作用域」这一个真实场合。An IIFE used to be the only way to isolate variables; today the one real use left is when you need an async scope.

接下来What next

  1. 接着看下一节Continue to the next lessonthis 与面向对象三问3 questions on this and object-oriented programming
    下一节Next lesson
  2. 可选:再巩固一下Optional: reinforce it这一节的 12 道八股12 questions from this lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: 引擎与类型十问10 questions on the engine and types