JavaScript 中函数内重赋值自身名称的原理与闭包效应

4次阅读

JavaScript 中函数内重赋值自身名称的原理与闭包效应

本文解析为何在函数体内对函数名重新赋值会改变其行为:`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()

    1. 创建新数组 arr(地址记为 0x100);
    2. 执行 fun1 = function() { return arr; } —— 此操作修改了外层作用域中 fun1 变量的引用,使其指向一个新函数;
    3. 该新函数形成了闭包,持久持有对 arr(0x100)的引用
    4. 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 则遵循常规函数语义,保证每次调用的独立性。理解这一机制,有助于识别遗留代码中的隐式状态管理,并在必要时安全复用该模式(建议辅以注释说明其惰性初始化意图)。

text=ZqhQzanResources