- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
本文围绕 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之所以存在,是因为它有两个特殊之处:
- 允许函数在内部引用自身(例如递归调用);
- 在函数外部不可见。
看一个例子——没有传入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解答 的思路层层递进:
- 要让整个链条工作,
sum的返回值必须是函数; - 这个函数必须在多次调用之间记住当前累加值(这里用闭包变量
currentSum); - 根据题目,函数在
==比较时必须能变成数字——函数是对象,转换按 对象原始值转换 章节描述的规则进行,我们可以提供自己的转换方法返回数字。
仓库中的参考实现(见 _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
相关推荐
终极指南:如何利用C++ AMP实现GPU并行计算的3种核心方法
终极指南:如何利用C++ AMP实现GPU并行计算的3种核心方法 C++ AMP(加速大规模并行计算)是微软推出的强大并行编程模型,能够显著提升C++代码在GP
文档/教程ChatGPT-Shortcut(AiShort)首页界面实战指南:标签筛选、智能搜索与提示词卡片交互
ChatGPT Shortcut(AiShort)首页界面实战指南:标签筛选、智能搜索与提示词卡片交互 本文基于 ChatGPT Shortcut(AiShor
AI 应用提示工程人工智能前端TinyIssue项目管理实战:如何用看板视图追踪团队任务进度
TinyIssue项目管理实战:如何用看板视图追踪团队任务进度 在当今快节奏的团队协作环境中,高效的 任务追踪工具 已成为项目管理的关键。TinyIssue作为
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考