手写 curry(部分应用可复用)Write curry yourself (reusable partial application)
题面The problem
先把要求读完,再动手。Read every requirement before you start.
fn.length 就执行,可任意分组, 部分应用复用互不污染。Grow this return-fn-directly version into a real curry: run as soon as fn.length arguments have arrived, accept them in any grouping, and let a partial application be reused without one call affecting another.- 攒够 fn.length 个参数就执行Runs as soon as fn.length arguments have arrived
- 参数可以任意分组:c(1)(2)(3) / c(1, 2)(3) / c(1)(2, 3)Arguments may come in any grouping: c(1)(2)(3), c(1, 2)(3), c(1)(2, 3)
- 部分应用可复用:const add1 = c(1) 之后多次调用互不污染A partial application is reusable: after const add1 = c(1), calling add1 many times gives independent results
预计 15 分钟。这个数字是照「读完题就开始写、不查资料」估的 —— 第一次超时很正常,第二次要压进去。Budget 15 minutes. Overrunning on the first pass is normal; the second pass should fit.
工作区Workspace
工作区是一个真的浏览器沙箱:左边写代码,右边实时预览,下面一个「跑测试」按钮。测试和本机那套是同一批断言,转写成了浏览器里能跑的写法。The workspace is a real in-browser sandbox: edit on the left, live preview on the right, one Run button below. The assertions are the same ones that pass on a real machine, rewritten for the browser runner.
需要联网。Requires an internet connection. 打包器和 npm 依赖都在 CodeSandbox 的远程服务上(评估过程见 docs/sandpack-evaluation.md),断网这块就起不来 —— 那就照下面的命令在本机跑。The bundler and the npm packages come from CodeSandbox's remote service, so this panel needs network access.
展开讲解Walkthrough
下面是《数据与函数:deepClone、flatten、curry》那一节的原文 —— 和课程里是同一份内容,不是另写的摘要。卡住了再展开。Below is the actual text of the lesson “数据与函数:deepClone、flatten、curry” — the same content as in the course, not a rewritten summary. Expand it when you stall.
展开《数据与函数:deepClone、flatten、curry》(3 段 · 约 35 分钟)Expand “数据与函数:deepClone、flatten、curry” (3 sections · ~35 min)
完整那一节(含练习、常见错误、迁移模式)在The full lesson (with exercises, common mistakes and transfer patterns) is at 数据与函数:deepClone、flatten、curry。
deepClone:先登记,再递归deepClone: record it first, then recurse
Write a deepClone that survives circular references
一句话:按类型分支递归克隆;每造出一个新容器,立刻在 WeakMap 里登记 「原对象 → 它的克隆」,循环引用一进来就直接还登记过的克隆 —— 这就是不爆栈的全部原理。
分支顺序:原始值和 null 原样返回 → 查 seen → Date → Map /Set → 数组 → 普通对象。「先登记再递归子节点」的次序不能反: 反了,a.self = a 这种结构会在登记之前就递归回自己。
会追问(必问):「为什么不用JSON.parse(JSON.stringify(x))?」—— 丢 undefined 和函数、Date 变字符串、Map / Set 变空对象、循环引用直接抛TypeError。「那 structuredClone 呢?」—— 生产代码优先用它(原生、支持循环引用),但它克隆不了函数和 DOM 节点,而且这道题考的就是你自己会不会写。
In one line: recurse by type, and the moment you create a new container, register “original → its clone” in a WeakMap — when a circular reference comes back around, you hand out the registered clone instead of recursing forever. That is the entire trick.
Branch order: primitives and null pass through → check seen → Date → Map / Set → arrays → plain objects. “Register first, recurse into children second” must not be flipped: flipped, a structure like a.self = a recurses back into itself before the registration exists.
Follow-up (always asked): “Why not JSON.parse(JSON.stringify(x))?” — it drops undefined and functions, turns Date into a string, turns Map / Set into empty objects, and throws a TypeError on circular references. “And structuredClone?” — prefer it in production code (native, handles cycles), but it cannot clone functions or DOM nodes, and this question exists to see whether you can write the thing yourself.
flatten:递归的出口就是 depthflatten: depth is what stops the recursion
Write a flatten with a depth parameter
一句话:遍历数组,遇到「是数组且 depth > 0」 就递归展开(depth 减一),否则原样收进结果 ——depth 本身就是递归的出口。
两个语义细节要和原生 Array.prototype.flat 对齐, 面试官就在这两处等你:默认 depth 是 1(不是 Infinity),depth 0 返回浅拷贝(不是原数组的引用 —— 不改输入是底线)。
会追问:「不用递归写一遍」—— 用栈:[...arr.map(v => [v, depth])] 形式的工作栈, 弹出时是数组且层数没用完就把子项带着层数压回去。 递归版清晰、迭代版不吃调用栈,说得出取舍就够了。
In one line: walk the array; when an item “is an array and depth > 0”, recurse into it with depth minus one, otherwise push it as-is — depth itself is the recursion exit.
Two semantic details must match the native Array.prototype.flat, and this is exactly where the interviewer waits for you: the default depth is 1 (not Infinity), and depth 0 returns a shallow copy (not the original reference — never mutate the input).
Follow-up: “Now without recursion” — use a work stack of [value, remainingDepth] pairs; when you pop an array with depth left, push its children back with depth minus one. Recursion reads better, iteration does not consume the call stack — naming that trade-off is all they want.
curry:攒参数必须造新数组curry: collecting arguments means building a new array each time
Write a curry; why must partial applications not share state
一句话:攒到的参数够 fn.length就执行,不够就返回一个「接着攒」的新函数 —— 攒的动作必须是 (...more) => curried(...args, ...more)这种拼新数组,不能往共享数组上 push。
为什么这么严:const add1 = curried(1) 之后,add1(2, 3) 和 add1(10, 20)必须都从 [1] 出发。push 版第一次调用后共享数组变成[1, 2, 3],第二次 add1(10, 20)实际拿到五个参数 —— 部分应用被污染了。 测试里专门有一条抓这个。
会追问:「fn.length 有什么坑?」—— 它数不到默认参数和 rest 参数((a, b = 1) => 的 length 是 1),所以带默认参数的函数 curry 不动;说得出这一句, 这道题就答干净了。
In one line: once the collected arguments reach fn.length, run; otherwise return a new collector — and collecting must build a fresh array, (...more) => curried(...args, ...more), never a push onto something shared.
Why so strict: after const add1 = curried(1), both add1(2, 3) and add1(10, 20) must start from [1]. With push, the shared array becomes [1, 2, 3] after the first call, so the second call really receives five arguments — the partial application is polluted. One test exists specifically to catch this.
Follow-up: “What is the catch with fn.length?” — it does not count default or rest parameters ((a, b = 1) => {} has length 1), so functions with defaults do not curry cleanly. Saying that one sentence closes the question.
参考答案Reference solution
提示是一级一级放的。四级看完还写不出来,再开答案门。The hints come one level at a time. If all four leave you stuck, open the answer.
这份答案在本机真跑过测试。但先确认你自己动手写过一遍 —— 读懂答案和写出答案是两种能力,考场上考的是后一种。This answer really was run here and its tests passed. But write it yourself first — reading an answer and producing one are two different skills, and the exam tests the second.