- 文档
- 教程
- 前端
【免费下载链接】zh.javascript.info
现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。
本篇实战文章基于《现代 JavaScript 教程》(zh.javascript.info)中「函数对象,NFE」章节的进阶练习题「为 counter 添加 set 和 decrease 方法」,围绕官方解答源码,深入拆解如何在同一份计数状态上,同时挂载"调用即自增"的计数函数与set/decrease两个操作方法。读完本文,你将掌握闭包与函数对象属性两种状态保持方案的取舍,理解嵌套函数与箭头函数如何共享同一个外部词法环境,并学会用 Mocha 风格的单元测试验证计数器的完整行为。
任务回顾:让计数器"能加能减能设置"
本练习位于仓库 1-js/06-advanced-functions/06-function-object/2-counter-inc-dec/task.md,重要程度标记为importance: 5(最高级)。题目要求修改经典的makeCounter()工厂函数,使返回的计数器在原有"每次调用返回下一个数字"的基础上,额外支持两种操作:
counter()—— 返回下一个数字(与原逻辑一致);counter.set(value)—— 将当前计数count设置为value;counter.decrease()—— 将count减 1。
题目还特别给出提示(P.S.):可以使用闭包或函数属性来保持当前计数,也可以两种方式混用。这实际上暗示了本练习有两条不同的实现路线,正是本篇文章要展开对比的核心。
练习附带的可运行沙箱初始代码见 _js.view/source.js,它展示了期望的完整调用序列:
function makeCounter() { let count = 0; // ... your code ... } let counter = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 counter.set(10); // set the new count alert( counter() ); // 10 counter.decrease(); // decrease the count by 1 alert( counter() ); // 10 (instead of 11)注意最后一步:set(10)后调用一次得到10(内部已自增到11),随后decrease()将计数减回10,因此下一次调用返回的是10而非11。
官方解答:闭包变量 + 函数属性组合方案
官方解答全文见 solution.md,核心源码见 _js.view/solution.js:
function makeCounter() { let count = 0; function counter() { return count++; } counter.set = value => count = value; counter.decrease = () => count--; return counter; }解答原文这样解释其原理:
该解决方案在局部变量中使用
count,而进行加法操作的方法是直接写在counter中的。它们共享同一个外部词法环境,并且可以访问当前的count。
也就是说,这套方案把"状态存储"与"行为挂载"分开了:
- 状态存储:
count是makeCounter()调用内部的局部变量,靠闭包保存在词法环境中,外部代码无法直接触碰; - 行为挂载:
set与decrease以箭头函数的形式,作为属性直接写在函数对象counter上; - 状态共享:无论是普通函数
counter,还是两个箭头函数属性,它们都在makeCounter()内部创建,因此共享同一个外部词法环境,读写的是同一个count。
原理深挖:为什么三个函数能共享同一个 count
要真正理解这套解法的妙处,需要回到闭包章节的底层机制。仓库 1-js/06-advanced-functions/03-closure/article.md 详细阐述了 JavaScript 的词法环境(Lexical Environment)模型:
- 每个运行的函数、代码块以及整个脚本,都有一个隐藏的词法环境对象,它由"环境记录(Environment Record,存储局部变量)"和"对外部词法环境的引用"两部分组成;
- 所有函数在"诞生"时都会通过隐藏属性
[[Environment]]记住创建它们的词法环境,这个引用在函数创建时被设置并永久保存; - 当代码访问一个变量时,会先搜索内部词法环境,再逐级向外搜索,直到全局词法环境;
- 变量在它所在的词法环境中被更新。
对照本练习的makeCounter()调用过程:
- 每次执行
makeCounter(),都会创建一个新的词法环境,其中存放局部变量count = 0; - 函数
counter、箭头函数counter.set、箭头函数counter.decrease都在这个环境中被创建,三个函数的[[Environment]]全部指向同一个词法环境; - 之后无论
counter被赋值给哪个变量、在哪里被调用,count的查找路径都是:自身词法环境(无局部变量)→ 外部makeCounter()的词法环境(找到count); counter()中的count++、set中的count = value、decrease中的count--操作的都是同一份count。
这里还有一个值得注意的细节:set和decrease使用的是箭头函数。箭头函数没有自己的this、arguments和super,它们完全从定义处的外层作用域捕获变量。在本例中,这意味着箭头函数体内的count直接绑定到makeCounter的词法环境中的那个count,count = value这条赋值语句会直接修改外层词法环境中count属性的值——这正是set(10)之后counter()返回10的根本原因。
同时,题目解答中用到的"方法直接写在counter上"这一写法,正是 函数对象,NFE 章节 中"自定义属性"一节的实战延伸:在 JavaScript 中函数也是对象,我们可以像给普通对象增删属性一样给函数挂载属性,而这些属性对函数的执行本身没有任何影响,只是作为额外携带的数据或方法存在。
对比方案:纯函数属性保持计数
题目 P.S. 中提到"也可以使用函数属性来保持当前的计数"。基于 函数对象,NFE 章节 中"函数属性有时会用来替代闭包"的思路,可以写出完全不使用局部变量的等价实现:
function makeCounter() { function counter() { return counter.count++; } counter.count = 0; counter.set = value => counter.count = value; counter.decrease = () => counter.count--; return counter; }这一版与官方解答的行为完全一致,但状态存放位置不同,两种路线的差异可以总结如下:
| 对比维度 | 闭包变量方案(官方解答) | 函数属性方案 |
|---|---|---|
| 状态存放 | count位于makeCounter的词法环境 | count直接挂在函数对象counter.count上 |
| 外部可访问性 | 外部代码无法访问count,只能通过三个方法间接操作 | 外部代码可直接读写counter.count |
| 状态封装性 | 强,状态对外完全隐藏 | 弱,状态是公开的函数属性 |
| 适用场景 | 需要严格封装内部状态、防止外部篡改 | 需要外部能直接查看/干预状态、或利用函数属性做内省 |
函数对象,NFE 章节 中也明确指出,闭包变量方案下"外部的代码无法访问到它,只有嵌套的那些函数可以修改它";而函数属性方案下外部代码可以直接执行counter.count = 10之类的赋值。因此,"选择哪种实现方式取决于我们的需求是什么"——如果需要对外隐藏内部状态,就用闭包变量;如果希望状态可被外部直接读写,就用函数属性。
用单元测试验证计数器行为
本练习在 _js.view/test.js 中附带了一组 Mocha 风格的单元测试,覆盖了三种行为,可以作为实现是否正确的判定标准:
describe("counter", function() { it("increases from call to call", function() { let counter = makeCounter(); assert.equal( counter(), 0 ); assert.equal( counter(), 1 ); assert.equal( counter(), 2 ); }); describe("counter.set", function() { it("sets the count", function() { let counter = makeCounter(); counter.set(10); assert.equal( counter(), 10 ); assert.equal( counter(), 11 ); }); }); describe("counter.decrease", function() { it("decreases the count", function() { let counter = makeCounter(); counter.set(10); assert.equal( counter(), 10 ); counter.decrease(); assert.equal( counter(), 10 ); }); }); });测试要点解读:
- 递增行为:
counter()连续调用依次返回0、1、2,验证count++返回旧值并自增的后置递增语义; - set 行为:
counter.set(10)后,第一次调用返回10(同时内部自增到11),第二次返回11,验证set确实把count改成了目标值且没有破坏自增逻辑; - decrease 行为:
set(10)后调用一次(内部变为11),decrease()减回10,下一次调用返回10,验证decrease与自增逻辑共享同一份状态。
注意每个测试用例都调用一次makeCounter()创建独立的计数器,这也印证了闭包章节的结论:每次makeCounter()调用都会创建独立的词法环境,多个计数器实例互不影响。
小结
本练习看似简单,实则同时覆盖了本教程两个重要章节的核心知识点:
- 闭包与词法环境(见 闭包章节):
count被保存在makeCounter的词法环境中,三个函数通过[[Environment]]共享同一份状态; - 函数对象与自定义属性(见 函数对象章节):
set和decrease作为属性直接挂在函数对象counter上,函数既是可调用的行为,也是可携带方法与数据的对象。
官方解答将两者结合:用闭包变量保护状态,用函数属性暴露操作,既保持了状态封装性,又让计数器拥有完整的"自增 / 设置 / 减一"接口。理解这一组合思路后,你便掌握了在 JavaScript 中为"行为对象"附加状态与方法的标准范式,这在函数式工具库、装饰器、计数器、缓存等场景中都非常常见。
相关文件索引:
- 任务描述
- 官方解答说明
- 解答源码
- 沙箱初始代码
- 单元测试
- 函数对象,NFE 章节
- 变量作用域,闭包章节
- 文档
- 教程
- 前端
【免费下载链接】zh.javascript.info
现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。
相关推荐
JavaScript 函数对象实战:为闭包计数器扩展 set 与 decrease 方法(en.javascript.info 示例精讲)
JavaScript 函数对象实战:为闭包计数器扩展 set 与 decrease 方法(en.javascript.info 示例精讲) 闭包与函数对象属性是
文档/教程前端JavaScript 函数对象实战:为计数器添加 set 与 decrease 方法
JavaScript 函数对象实战:为计数器添加 set 与 decrease 方法 本篇文章以《现代 JavaScript 教程》中「函数对象,NFE」章节的
文档教程前端JavaScript 计数器进阶:为 makeCounter 添加 set 与 decrease——闭包与函数属性的双方案实战
JavaScript 计数器进阶:为 makeCounter 添加 set 与 decrease——闭包与函数属性的双方案实战 导读 本篇文章基于 Modern
文档/教程前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考