
本文解析为何在函数体内对函数名重新赋值会改变其行为:`fun1()` 通过首次调用时重定义全局函数引用并捕获闭包中的数组,实现“单例数组复用”;而 `fun2()` 每次调用都新建数组,返回独立副本。二者本质差异在于作用域绑定、执行时机与内存生命周期。
在 JavaScript 中,函数名在非严格模式下(尤其作为函数声明时)会作为可写属性被注入到当前词法环境的外层作用域(通常是全局或模块顶层)中。这一特性常被用于实现“惰性初始化”(lazy initialization)或“函数自替换”(function self-overwriting)模式——fun1 正是典型应用。
我们来逐步拆解 fun1 的执行逻辑:
function fun1() {
const arr = ["a", "b", "c", "d", "e"];
fun1 = function () { // ⚠️ 关键:直接赋值给标识符 fun1
return arr;
};
return fun1(); // 此处调用的是刚重定义的新函数
}-
首次调用 fun1():
- 创建新数组 arr(地址记为 0x100);
- 执行 fun1 = function() { return arr; } —— 此操作修改了外层作用域中 fun1 变量的引用,使其指向一个新函数;
- 该新函数形成了闭包,持久持有对 arr(0x100)的引用;
- return fun1() 实际调用这个新函数,返回 arr 的引用(而非拷贝)。
后续调用 fun1():
原始函数体不再执行,直接进入闭包函数,始终返回同一个 arr 实例(0x100)。因此对返回值的修改(如 .pop())会真实反映在后续调用结果中。
相比之下,fun2 是纯粹的“无状态工厂函数”:
立即学习“Java免费学习笔记(深入)”;
function fun2() {
const arr = ["a", "b", "c", "d", "e"]; // 每次调用都新建数组(新内存地址)
return arr; // 返回新数组的引用
}每次调用 fun2() 都会创建一个全新数组实例,彼此内存隔离,互不影响。
✅ 验证关键点:检查函数身份与数组引用
可通过以下代码直观验证两者的差异:
console.log(fun1.toString() !== fun2.toString()); // true —— fun1 已被重定义 console.log(fun1() === fun1()); // true —— 同一数组引用 console.log(fun2() === fun2()); // false —— 不同数组实例
⚠️ 注意事项与陷阱
- 仅在非严格模式/函数声明下生效:若 fun1 被定义为 const fun1 = function() {...} 或在严格模式中,对 fun1 的赋值将抛出 TypeError(不可写绑定)。
- 非线程安全:在并发调用场景下,首次竞争可能导致未定义行为(尽管实际中极少发生)。
- 调试难度高:函数行为在运行时动态变更,违背“纯函数”直觉,增加维护成本。
- 替代方案更推荐:现代代码应优先使用模块级私有变量 + 闭包,语义更清晰:
// 推荐:显式封装,意图明确
const fun1 = (() => {
const arr = ["a", "b", "c", "d", "e"];
return () => arr;
})();总结
fun1 的“魔法”并非来自语法黑科技,而是 JavaScript 函数声明提升(hoisting)与变量可写性共同作用的结果:它利用首次调用完成一次性初始化 + 闭包固化 + 函数重绑定三重效果,实现了轻量级单例数组缓存;而 fun2 则遵循常规函数语义,保证每次调用的独立性。理解这一机制,有助于识别遗留代码中的隐式状态管理,并在必要时安全复用该模式(建议辅以注释说明其惰性初始化意图)。










