函数对象与具名函数表达式(NFE):深入 JavaScript 中“函数即对象“的核心机制

发布时间:2026/10/4 1:55:56
函数对象与具名函数表达式(NFE):深入 JavaScript 中“函数即对象“的核心机制 文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载本文围绕 Modern JavaScript Tutorialen.javascript.info中高级函数章节的 Function object, NFE 展开系统讲解函数作为对象的本质name、length内置属性、自定义属性如计数器、以及具名函数表达式Named Function Expression在可靠递归自引用中的关键作用。读完本文你将掌握利用函数属性替代闭包存储状态的两种思路、通过length实现参数多态分派以及 NFE 内部名在函数被重新赋值后依然稳定可用的实战技巧并能在仓库配套任务与测试中验证这些结论。函数是可调用的动作对象在 JavaScript 中函数是一种值而每一个值都有类型——函数的类型是对象。这一点是理解本章全部内容的前提函数可以被看作可调用的动作对象callable action objects。我们不仅能调用它还能把它当作普通对象对待添加/删除属性、按引用传递等等。这意味着函数同时具备两重身份作为代码可以被调用()运算符作为对象可以承载属性、方法、状态。本章就是围绕这双重身份逐一展开函数对象上内置与自定义的各种属性。name属性函数名从哪来函数对象自带一些可用属性name是其中之一直接返回函数的名称function sayHi() { alert(Hi); } alert(sayHi.name); // sayHi上下文命名contextual name有趣的是命名逻辑相当智能。即使函数在创建时没有名字只要它紧接着被赋值给变量引擎也能推断出正确的名称let sayHi function() { alert(Hi); }; alert(sayHi.name); // sayHi居然有名字这种推断同样适用于通过默认参数赋值的情况function f(sayHi function() {}) { alert(sayHi.name); // sayHi依然有效 } f();在 ECMAScript 规范中这一特性被称为上下文命名contextual name如果函数没有显式提供名称那么在赋值场景下引擎会根据上下文推断出名字。对象方法也有名字对象的方法同样具备name无论是简洁方法语法还是函数表达式赋值let user { sayHi() { // ... }, sayBye: function() { // ... } }; alert(user.sayHi.name); // sayHi alert(user.sayBye.name); // sayBye无法推断时名字为空当然这里没有魔法。当引擎确实无法推断名字时name就是空字符串。例如函数被创建在数组内部没有任何赋值上下文// 创建在数组里的函数 let arr [function() {}]; alert( arr[0].name ); // 空字符串 // 引擎无从推断合适的名字所以没有名字不过在实践中绝大多数函数都拥有名字。length属性参数个数与多态分派length是另一个内置属性返回函数定义中声明的参数个数function f1(a) {} function f2(a, b) {} function many(a, b, ...more) {} alert(f1.length); // 1 alert(f2.length); // 2 alert(many.length); // 2注意上面的例子rest 参数...more不会被计入length。利用length做内省与多态length属性经常被用在操作其他函数的函数中做内省introspection。原文档给出了一个经典的ask示例function ask(question, ...handlers) { let isYes confirm(question); for(let handler of handlers) { if (handler.length 0) { if (isYes) handler(); // 无参处理器仅在肯定回答时调用 } else { handler(isYes); // 带参处理器任何情况下都调用并传入结果 } } } // 肯定回答时两个处理器都会被调用否定回答时只有第二个被调用 ask(Question?, () alert(You said yes), result alert(result));这里ask接收一个问题question和任意数量的处理器handler。通过检查handler.length决定调用方式零参数函数只在用户给出肯定回答时调用最常见、最简洁的写法带参数函数无论结果如何都会被调用并接收回答结果。这是一种典型的多态polymorphism用法——根据参数的类型或length以不同方式处理参数。这种思路在 JavaScript 库中确实有实际应用。自定义属性给函数挂上自己的状态既然函数是对象我们当然可以添加自己的属性。比如用counter属性统计函数的调用总次数function sayHi() { alert(Hi); // 记录调用了多少次 sayHi.counter; } sayHi.counter 0; // 初始值 sayHi(); // Hi sayHi(); // Hi alert( Called ${sayHi.counter} times ); // Called 2 times注意属性不是变量像 sayHi.counter 0 这样赋给函数的属性并**不会**在函数内部定义一个局部变量 counter。换句话说属性 counter 和变量 let counter 是两件互不相干的事。 我们可以把函数当作对象、在它身上存储属性但这不会影响函数本身的执行。变量不是函数的属性反之亦然——它们是两个平行世界。函数属性 vs 闭包状态存储的两种路径函数属性有时可以替代闭包。例如可以把 闭包章节 中的计数器例子改写为使用函数属性function makeCounter() { // 不再需要 // let count 0 function counter() { return counter.count; }; counter.count 0; return counter; } let counter makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1此时count直接存储在函数上而不是外层词法环境Lexical Environment中。那么它比闭包更好还是更差两者最大的区别在于可访问性如果count生活在外层变量中外部代码无法访问它只有嵌套函数才能修改如果count绑定在函数上外部代码就可以直接读写function makeCounter() { function counter() { return counter.count; }; counter.count 0; return counter; } let counter makeCounter(); counter.count 10; alert( counter() ); // 10所以选哪种实现取决于我们的目标需要对外封闭的状态用闭包允许外部读写/扩展的状态用函数属性。Named Function ExpressionNFE具名函数表达式NFE 是指带有名字的函数表达式Function Expression。普通函数表达式长这样let sayHi function(who) { alert(Hello, ${who}); };给它加上一个名字funclet sayHi function func(who) { alert(Hello, ${who}); };加上这个名字我们得到了什么首先需要注意它依然是函数表达式。在function后面加名字并没有把它变成函数声明Function Declaration因为它仍然是赋值表达式的一部分。同时加名字也没有破坏任何东西函数照常可以以sayHi()的方式调用let sayHi function func(who) { alert(Hello, ${who}); }; sayHi(John); // Hello, John内部名的两大特性名字func之所以存在是因为它有两个特殊之处允许函数在内部引用自身例如递归调用在函数外部不可见。看一个例子——没有传入who时函数用func以Guest再次调用自己let sayHi function func(who) { if (who) { alert(Hello, ${who}); } else { func(Guest); // 用 func 重新调用自身 } }; sayHi(); // Hello, Guest // 但下面这样不行 func(); // Error: func is not defined函数外部不可见为什么不用外层的sayHi递归既然函数在外面有个变量sayHi为什么嵌套调用不直接用sayHi呢大多数情况下确实可以let sayHi function(who) { if (who) { alert(Hello, ${who}); } else { sayHi(Guest); } };问题在于sayHi可能会被外层代码改变。一旦函数被赋值给另一个变量旧代码就会出错let sayHi function(who) { if (who) { alert(Hello, ${who}); } else { sayHi(Guest); // Error: sayHi is not a function } }; let welcome sayHi; sayHi null; welcome(); // 报错嵌套的 sayHi 调用不再工作了原因在于函数是从外层词法环境取sayHi的函数内部没有局部sayHi所以引用的是外层变量——而调用发生时那个外层sayHi已经是null了。NFE 修复内部名始终指向当前函数函数表达式中可选的名字正是为解决这类问题而生。用它修复上面的代码let sayHi function func(who) { if (who) { alert(Hello, ${who}); } else { func(Guest); // 现在完全正常 } }; let welcome sayHi; sayHi null; welcome(); // Hello, Guest嵌套调用依然工作现在一切正常因为名字func是函数局部的它不从外部获取外部也不可见。规范保证它永远引用当前这个函数。外层代码仍然持有自己的变量sayHi或welcome而func是一个内部函数名internal function name是函数可靠调用自身的方式。函数声明没有这种内部名这里介绍的内部名特性只对函数表达式可用函数声明没有添加内部名的语法。 当我们需要一个可靠的内部名时这往往是把函数声明改写成具名函数表达式形式的原因。小结函数是对象本章核心结论函数是对象。我们覆盖了它们的属性name—— 函数名。通常取自函数定义若没有JavaScript 会尝试从上下文如赋值推断length—— 函数定义中参数的个数rest 参数不计入自定义属性 —— 函数可以携带任意额外属性许多知名 JavaScript 库充分利用了这一特性。如果函数以函数表达式形式声明不在主代码流中并且带名字它就叫具名函数表达式NFE。这个名字可以在函数内部用于自引用递归调用等。库中的经典实践主函数挂载辅助函数很多著名 JavaScript 库会创建一个主函数再把许多辅助函数挂到它上面。例如 jQuery 库创建了函数$lodash 库创建了函数_然后给它添加_.clone、_.keyBy等属性。这样做的目的是减少对全局空间的污染——一个库只暴露一个全局变量从而降低命名冲突的可能性。所以一个函数既能独立完成有用的工作又能在属性中承载一整套其他功能。仓库实战验证配套任务与测试本仓库在该章节下附带两个配套任务位于 06-function-object 目录正好用来实战检验函数属性与函数即对象的威力。任务一计数器的set与decrease2-counter-inc-dec任务说明 要求改造makeCounter()让计数器支持counter()照旧返回下一个数字counter.set(value)把计数设置为valuecounter.decrease()把计数减 1。题目还提示既可以用闭包也可以用函数属性来保存当前计数甚至可以两种都写。仓库给出的参考解答采用闭包保存count同时把方法直接写进counter函数对象上function makeCounter() { let count 0; function counter() { return count; } counter.set value count value; counter.decrease () count--; return counter; }方法set和decrease与counter共享同一个外层词法环境因此都能访问当前的count。这正是本章函数是对象、可承载方法思想的应用闭包负责隐藏状态函数属性负责暴露操作接口。配套的单元测试Mocha 风格完整验证了三种行为连续调用递增、set(10)后计数变为 10 再递增为 11、set(10)后decrease()回到 10。完整的可运行骨架见 _js.view/source.js。任务二任意数量括号的sum5-sum-many-brackets任务说明 要求写出这样的sumsum(1)(2) 3; // 1 2 sum(1)(2)(3) 6; // 1 2 3 sum(5)(-1)(2) 6 sum(6)(-1)(-2)(-3) 0 sum(0)(1)(2)(3)(4)(5) 15解答 的思路层层递进要让整个链条工作sum的返回值必须是函数这个函数必须在多次调用之间记住当前累加值这里用闭包变量currentSum根据题目函数在比较时必须能变成数字——函数是对象转换按 对象原始值转换 章节描述的规则进行我们可以提供自己的转换方法返回数字。仓库中的参考实现见 _js.view/solution.jsfunction sum(a) { let currentSum a; function f(b) { currentSum b; return f; } f.toString function() { return currentSum; }; return f; } alert( sum(1)(2) ); // 3 alert( sum(5)(-1)(2) ); // 6 alert( sum(6)(-1)(-2)(-3) ); // 0 alert( sum(0)(1)(2)(3)(4)(5) ); // 15请注意sum本身只被调用了一次返回函数f。之后每次调用f把自己的参数累加到currentSum上并返回自身供下一次调用继续链式使用。最后一行没有递归递归长这样function f(b) { currentSum b; return f(); // -- 递归调用 }而我们的实现只是返回函数本身并没有调用它function f(b) { currentSum b; return f; // -- 不调用自身返回自身 }这个f会在下一次调用中被使用再返回自己如此反复。当它被当作数字或字符串使用时toString返回currentSum。这里同样可以使用Symbol.toPrimitive或valueOf来做转换——它们与toString都属于函数作为对象自定义属性的范畴。延伸阅读变量作用域与闭包理解函数属性与闭包各自的状态存储机制是本章属性 vs 变量对比的理论基础对象原始值转换sum(1)(2) 3中函数对象转数字的规则依据函数对象章节配套任务2-counter-inc-dec与5-sum-many-brackets的完整任务、解答与测试代码可在本地结合_js.view中的沙箱代码直接运行验证。赞分享文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载相关推荐终极指南如何利用C AMP实现GPU并行计算的3种核心方法终极指南如何利用C AMP实现GPU并行计算的3种核心方法 C AMP加速大规模并行计算是微软推出的强大并行编程模型能够显著提升C代码在GP文档/教程ChatGPT-ShortcutAiShort首页界面实战指南标签筛选、智能搜索与提示词卡片交互ChatGPT ShortcutAiShort首页界面实战指南标签筛选、智能搜索与提示词卡片交互 本文基于 ChatGPT ShortcutAiShorAI 应用提示工程人工智能前端TinyIssue项目管理实战如何用看板视图追踪团队任务进度TinyIssue项目管理实战如何用看板视图追踪团队任务进度 在当今快节奏的团队协作环境中高效的 任务追踪工具 已成为项目管理的关键。TinyIssue作为后端上一篇告别盲目调优VizTracer自动分析功能让Python性能问题无所遁形下一篇Selene vs Selenium为什么选择 Python 版本的 Selenide 进行 Web 测试创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考