☰
现代 JavaScript 教程实战:用闭包与函数对象属性为 counter 扩展 set 和 decrease 方法
2026/10/7 16:09:43 网站建设 项目流程
  • 文档
  • 教程
  • 前端

【免费下载链接】zh.javascript.info

现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。

项目地址:https://gitcode.com/gh_mirrors/zh/zh.javascript.info
点击查看免费下载

本篇实战文章基于《现代 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)模型:

  1. 每个运行的函数、代码块以及整个脚本,都有一个隐藏的词法环境对象,它由"环境记录(Environment Record,存储局部变量)"和"对外部词法环境的引用"两部分组成;
  2. 所有函数在"诞生"时都会通过隐藏属性[[Environment]]记住创建它们的词法环境,这个引用在函数创建时被设置并永久保存;
  3. 当代码访问一个变量时,会先搜索内部词法环境,再逐级向外搜索,直到全局词法环境;
  4. 变量在它所在的词法环境中被更新。

对照本练习的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()调用都会创建独立的词法环境,多个计数器实例互不影响。

小结

本练习看似简单,实则同时覆盖了本教程两个重要章节的核心知识点:

  1. 闭包与词法环境(见 闭包章节):count被保存在makeCounter的词法环境中,三个函数通过[[Environment]]共享同一份状态;
  2. 函数对象与自定义属性(见 函数对象章节):set和decrease作为属性直接挂在函数对象counter上,函数既是可调用的行为,也是可携带方法与数据的对象。

官方解答将两者结合:用闭包变量保护状态,用函数属性暴露操作,既保持了状态封装性,又让计数器拥有完整的"自增 / 设置 / 减一"接口。理解这一组合思路后,你便掌握了在 JavaScript 中为"行为对象"附加状态与方法的标准范式,这在函数式工具库、装饰器、计数器、缓存等场景中都非常常见。

相关文件索引:

  • 任务描述
  • 官方解答说明
  • 解答源码
  • 沙箱初始代码
  • 单元测试
  • 函数对象,NFE 章节
  • 变量作用域,闭包章节
  • 文档
  • 教程
  • 前端

【免费下载链接】zh.javascript.info

现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。

项目地址:https://gitcode.com/gh_mirrors/zh/zh.javascript.info
点击查看免费下载

相关推荐

上一篇:Rust代码保护终极指南:如何安全保护Easy Rust项目知识产权
下一篇:Mortal麻将AI:如何用Rust和强化学习打造21%胜率提升的竞技智能

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询