函数与作用域十二问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.
这一页有什么On this page13
- 01 有几种定义函数的方式How many ways are there to define a function?
- 02 什么是一等函数What is a first class function?
- 03 什么是一阶函数What is a first order function?
- 04 什么是高阶函数What is a higher order function?
- 05 什么是纯函数What is a pure function?
- 06 "use strict" 是干什么的What does "use strict" do?
- 07 作用域有哪几种What kinds of scope are there?
- 08 什么是变量提升What is hoisting?
- 09 什么是作用域链What is the scope chain?
- 10 什么是闭包What is a closure?
- 11 什么是柯里化What is currying?
- 12 什么是 IIFEWhat is an IIFE?
- 迁移模式Transfer
- 说清闭包是什么、为什么会「记住」外层变量,并举出两个真实用途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
闭包是 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.
有几种定义函数的方式How many ways are there to define a function?
#285 How many ways to define a function
一句话:五种 —— 函数声明、函数表达式、箭头函数、Function 构造器、以及对象/类里的方法简写。
但面试真正想听的是它们的差别, 尤其是前三种:
| 提升 | this | arguments | 能否 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:
| Hoisting | this | arguments | Can you new it | |
|---|---|---|---|---|
| Function declaration | Hoisted whole, callable before its line | Decided at call time | Yes | Yes |
| Function expression | Only the variable name is hoisted | Decided at call time | Yes | Yes |
| Arrow function | Only the variable name is hoisted | The enclosing this where it was written | No | No |
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.
什么是一等函数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.
什么是一阶函数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.
什么是高阶函数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.
什么是纯函数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.
"use strict" 是干什么的What does "use strict" do?
#294 What is "use strict"
一句话:开启严格模式 —— 把一批「静默出错」的写法变成直接抛错, 并禁掉一些历史包袱。
具体管四件事(记两三条就够答):
- 禁止隐式全局变量。
x = 1忘了let, 非严格下会悄悄挂到window, 严格下抛ReferenceError。这是它最大的价值。 - 函数里的
this是undefined, 而不是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 = 1with a forgottenletquietly lands onwindowin sloppy mode, and throwsReferenceErrorin strict mode. This is its biggest single win. thisinside a plain function isundefinedinstead ofwindow— 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.
withis banned, duplicate parameter names are banned, andargumentsno 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.
作用域有哪几种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也有自己的作用域。
会追问:「函数作用域和块作用域差在哪,举个例子?」——if 里 var 声明的变量出了 if 还能访问,let 就不能。 这是把老代码从 var改成 let 时最常见的破坏点。
In one line: four — global, function, block and module.
- Global — the outermost level. In a browser, a
vardeclared here lands onwindow. - Function — the territory of
varand the parameters, visible anywhere in the function body. - Block — any pair of
{}. It only bindslet/const/class;varignores 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.
什么是变量提升What is hoisting?
#296 What is hoisting
一句话:编译阶段引擎会先扫一遍, 把声明登记到作用域里, 所以「在声明之前引用」不一定报错 ——但只提升声明,不提升赋值。
四种情况分清就够答:
| 声明前访问的结果 | |
|---|---|
| 函数声明 | 整个函数都能用(可以直接调用) |
var | undefined |
let / const | 抛 ReferenceError(TDZ) |
class | 抛 ReferenceError(也有 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 declaration | Usable throughout (you can call it) |
var | undefined |
let / const | throws ReferenceError (TDZ) |
class | throws 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.
什么是作用域链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.
什么是闭包What is a closure?
#298 What is a closure
一句话:函数记住了它定义时所在的作用域—— 即使外层函数已经执行完了, 里面的变量也还活着,因为这个函数还在引用它们。
为什么会这样?因为作用域链在定义时就绑好了(#297)。 外层函数返回后, 它的变量本该被回收, 但只要还有函数引用着, 垃圾回收就不会动它。
四个真实用途(面试要的是用途,不是定义):
- 私有状态—— 计数器、缓存。 外部拿不到那个变量,只能通过你暴露的方法改。
- 防抖 / 节流—— 用闭包存 timer。
- 柯里化 / 偏函数—— 记住已经传进来的参数(#299)。
- React 的 Hooks 全靠它——
useState返回的setState、useEffect里的回调, 都是闭包捕获了那一次渲染的值。「过期闭包」这个 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
setStatereturned byuseState, and the callback insideuseEffect, 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.
什么是柯里化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}`), thenconst 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.
什么是 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.
换一道题也能用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.
- 函数声明整体提升,函数表达式只提升变量名;箭头函数没有自己的 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.
- 一等(函数能当值)→ 一阶(不碰函数)→ 高阶(收或返函数),这三题是一组。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.
- 纯函数两条件:同输入同输出 + 无副作用;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.
- 严格模式主要价值是禁隐式全局;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.
- 四种作用域:全局/函数/块/模块;块作用域只约束 let、const、class。There are four scopes: global, function, block and module; block scope only applies to let, const and class.
- 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.
- 作用域链由内到外,且在定义时确定(词法作用域);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.
- 闭包 = 函数记住定义时的作用域;用途是私有状态、防抖、柯里化,以及 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.
- 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.