☰
函数对象与具名函数表达式(NFE):深入 JavaScript 中“函数即对象“的核心机制
2026/10/4 1:55:54 网站建设 项目流程
  • 文档/教程
  • 前端

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

Modern JavaScript Tutorial

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

本文围绕 Modern JavaScript Tutorial(en.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 Expression(NFE):具名函数表达式

NFE 是指带有名字的函数表达式(Function Expression)。普通函数表达式长这样:

let sayHi = function(who) { alert(`Hello, ${who}`); };

给它加上一个名字func:

let sayHi = function func(who) { alert(`Hello, ${who}`); };

加上这个名字我们得到了什么?首先需要注意:它依然是函数表达式。在function后面加名字并没有把它变成函数声明(Function Declaration),因为它仍然是赋值表达式的一部分。同时,加名字也没有破坏任何东西,函数照常可以以sayHi()的方式调用:

let sayHi = function func(who) { alert(`Hello, ${who}`); }; sayHi("John"); // Hello, John

内部名的两大特性

名字func之所以存在,是因为它有两个特殊之处:

  1. 允许函数在内部引用自身(例如递归调用);
  2. 在函数外部不可见。

看一个例子——没有传入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与decrease(2-counter-inc-dec)

任务说明 要求改造makeCounter(),让计数器支持:

  • counter()照旧返回下一个数字;
  • counter.set(value)把计数设置为value;
  • counter.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。

任务二:任意数量括号的sum(5-sum-many-brackets)

任务说明 要求写出这样的sum:

sum(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

解答 的思路层层递进:

  1. 要让整个链条工作,sum的返回值必须是函数;
  2. 这个函数必须在多次调用之间记住当前累加值(这里用闭包变量currentSum);
  3. 根据题目,函数在==比较时必须能变成数字——函数是对象,转换按 对象原始值转换 章节描述的规则进行,我们可以提供自己的转换方法返回数字。

仓库中的参考实现(见 _js.view/solution.js):

function 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.info

Modern JavaScript Tutorial

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

相关推荐

上一篇:告别盲目调优:VizTracer自动分析功能让Python性能问题无所遁形
下一篇:Selene vs Selenium:为什么选择 Python 版本的 Selenide 进行 Web 测试

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

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

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

立即咨询