今天我们只聊聊js闭包

一. 啥叫闭包?

  1. 先来看一下经典的示例:
javascript 复制代码
function createCounter() {  
    let count = 0; // 外部函数变量  
    return function() {  
        count++;    // 内部函数访问外部变量  
        return count;  
    }; 
} 
const counter = createCounter(); 
console.log(counter()); 
// 1 console.log(counter()); 
// 2 (保留对count的引用)
createCounter 执行过程解析:
  1. createCounter() 执行完毕,但返回的匿名函数仍持有对 count 的引用

  2. count 变量不会被销毁,形成"私有状态"

  3. 每次调用 counter() 都修改同一个 count

这里,createCounter返回的方法就形成了闭包,保留了对 count变量的引用。count变量不会被垃圾回收,因为它被闭包"记住"了

那么就可以简单的引出闭包了

当一个函数(内部函数)在其词法作用域之外被调用时,它仍然能访问原作用域中的变量------这种"跨作用域的记忆能力"就是闭包。

下面找一个闭包的定义哈

一个函数与其词法环境中所有变量的引用共同组成的"闭包对象"​

简单点就是 闭包 = 函数 + 其创建时的词法作用域环境

那么下面的闭包关键点以及形成条件就是进一步理解闭包的关键啦。

闭包的关键点解析:

  1. 词法作用域(Lexical Scope)

    JavaScript 函数的作用域在定义时确定(而非执行时),它能访问定义位置的外层作用域变量。

  2. 闭包的形成条件

    • 嵌套函数(内部函数访问外部函数变量)

    • 内部函数被导出到外部作用域(如通过返回值、事件回调等)

  3. 生命周期特性

    即使外部函数已执行完毕,闭包仍能保留其词法作用域中的变量(不会被垃圾回收)。

闭包的形成条件​​:

要形成闭包,需满足两个关键条件:

  1. ​存在嵌套函数​​:外层函数内部定义了一个内层函数;

  2. ​内层函数被外部引用​​:内层函数被返回或传递到外层函数之外执行

二、闭包的实际应用场景:

  1. 数据封装(模拟私有变量)
javascript 复制代码
function createBankAccount(initialBalance) {
  let balance = initialBalance; // 私有变量
  
  return {
    deposit: (amount) => balance += amount,
    withdraw: (amount) => balance -= amount,
    getBalance: () => balance
  };
}

const account = createBankAccount(100);
account.withdraw(30);
console.log(account.getBalance()); // 70 (外部无法直接访问balance)

2. 函数工厂

javascript 复制代码
function powerFactory(exponent) {
  return function(base) {
    return base ** exponent;
  };
}

const square = powerFactory(2);
console.log(square(5)); // 25
  1. 事件处理(保留上下文)
javascript 复制代码
function setupButton() {
  const button = document.getElementById('myBtn');
  let clickCount = 0;
  
  button.addEventListener('click', () => {
    clickCount++;
    console.log(`Clicked ${clickCount} times`);
  });
}
// 每次点击都访问同一个clickCount

4. 模块模式(Modern JS 已被 class 替代)

ini 复制代码
const Calculator = (() => {
  let memory = 0;
  
  return {
    add: (x) => memory += x,
    getMemory: () => memory
  };
})();

5. 函数柯里化(Currying)

scss 复制代码
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn(...args);
    } else {
      return (...nextArgs) => curried(...args, ...nextArgs);
    }
  };
}

const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 输出 6

三、 闭包的工作原理

JavaScript 引擎通过​​作用域链(Scope Chain)​​管理变量的访问。当函数执行时,会创建一个执行上下文(Execution Context),其中包含:

  • ​词法环境(Lexical Environment)​​:存储当前作用域的变量和函数声明;

  • ​变量环境(Variable Environment)​ ​:存储 var声明的变量(ES6 后与词法环境合并);

  • ​this 绑定​ ​:函数的 this指向。

当内层函数被外部调用时,它的作用域链不会被销毁,而是​​保留了外层函数作用域的引用​​。因此,即使外层函数已执行完毕,内层函数仍能通过作用域链访问外层函数的变量。

四、常见误区与注意事项:内存与性能​​

闭包虽然强大,但需注意以下问题:

1. ​​内存泄漏风险​​

闭包会长期保留对外部作用域变量的引用,导致这些变量无法被垃圾回收。如果闭包中引用了大对象(如 DOM 元素),可能导致内存占用过高。

javascript 复制代码
function heavyProcess() {
  const bigData = new Array(1000000); // 大对象
  
  return function() {
    // 即使不再需要bigData,它仍被保留在内存中!
  };
}

​解决方法​​:

  • 在闭包不再需要时,手动解除引用(如将闭包变量置为 null);

  • 避免在循环中不必要地创建闭包;

  • 使用块级作用域(let/const)限制变量作用范围。

2. 循环中的闭包陷阱

css 复制代码
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); 
}
// 输出:3, 3, 3 (不是预期的0,1,2)

解决方案

  • 使用 let(块级作用域)

    for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); // 0,1,2 }

  • 立即执行函数(IIFE)

    for (var i = 0; i < 3; i++) { (function(j) { setTimeout(() => console.log(j), 100); // 0,1,2 })(i); }

3. ​​变量共享问题​​

闭包中引用的外部变量是​​引用绑定​​(而非值拷贝),多个闭包可能共享同一个变量,导致意外行为。

javascript 复制代码
// 错误示例:所有按钮点击都输出 3(闭包共享同一个 i)
for (var i = 0; i < 3; i++) {
  document.querySelectorAll('button')[i].addEventListener('click', function() {
    console.log('按钮 ' + i + ' 被点击'); // i 是循环结束后的值 3
  });
}

// 正确示例:用闭包保留当前 i 的值(ES5 方案)
for (var i = 0; i < 3; i++) {
  (function(j) { // 立即执行函数创建闭包,保存当前的 j=i
    document.querySelectorAll('button')[j].addEventListener('click', function() {
      console.log('按钮 ' + j + ' 被点击'); // 输出 0、1、2
    });
  })(i);
}

// ES6 方案:用 let 声明 i(块级作用域自动形成闭包)
for (let i = 0; i < 3; i++) { 
  document.querySelectorAll('button')[i].addEventListener('click', function() {
    console.log('按钮 ' + i + ' 被点击'); // 输出 0、1、2
  });
}

​​五、总结:闭包的本质​​

闭包的本质是​​函数对其词法环境的"记忆"能力​​。它让函数突破了执行时的作用域限制,能够访问定义时的环境。这一特性是 JavaScript 实现模块化、私有变量、函数式编程(如柯里化)的核心基础。

理解闭包的关键是掌握:

  • 词法作用域决定了闭包的"记忆范围";

  • 闭包通过保留作用域链实现跨作用域访问;

  • 合理使用闭包能提升代码灵活性,但需注意内存管理。

相关推荐
摇滚侠31 分钟前
JavaScript 浮点数计算精度错误示例
开发语言·javascript·ecmascript
天蓝色的鱼鱼1 小时前
JavaScript垃圾回收:你不知道的内存管理秘密
javascript·面试
waillyer2 小时前
taro跳转路由取值
前端·javascript·taro
yume_sibai2 小时前
Vue 生命周期
前端·javascript·vue.js
讨厌吃蛋黄酥3 小时前
🌟 React Router Dom 终极指南:二级路由与 Outlet 的魔法之旅
前端·javascript
轻语呢喃4 小时前
useMemo & useCallback :React 函数组件中的性能优化利器
前端·javascript·react.js
_未完待续4 小时前
Web 基础知识:CSS - 基础知识
前端·javascript·css
掘金014 小时前
初学者 WebRTC 视频连接教程:脚本逻辑深度解析
javascript·面试
GISer_Jing5 小时前
Node.js的Transform 流
前端·javascript·node.js
在钱塘江5 小时前
《你不知道的JavaScript-中卷》第一部分-类型和语法-笔记-4-强制类型转换
前端·javascript