JS核心知识-闭包

41 阅读5分钟

对于 JavaScript 开发者来说,闭包是一个既核心又容易困惑的概念。本文将从底层机制入手,帮你彻底掌握闭包的本质,让它从神秘的"黑魔法"变成清晰的编程工具。

函数作用域

在理解闭包之前,我们需要先掌握函数作用域的基本特性。函数作用域就像一个"数据隔离区",具有以下特点:

  • 内部隐私性:函数内部变量对外部不可见
  • 外部可访问性:函数内部可以访问外部变量
  • 生命周期限制:函数执行完毕后,内部变量通常会被销毁
// 全局变量 - 如同地球上的居民
const globalValue = 'globalValue';

// 函数就像一座孤岛上的城堡
function fn() {
  const localValue = 'localValue';
  console.log(globalValue); // ✅ 城堡内可以访问外部世界
  console.log(localValue);  // ✅ 当然也能访问城堡内部
}

fn();
console.log(localValue); // ❌ 外部无法访问城堡内的居民

闭包:打破作用域限制的魔法

闭包(Closure)  是一个函数与其被记忆的词法环境的组合。它打破了函数作用域的常规限制,让内部函数能够"记住"并访问创建时的环境。

function outer() {
  var num = 0;
  return function inner() {
    num++;
    return num;
  };
}
const fn = outer();
console.log(fn());
console.log(fn());

闭包的形成机制详解

让我们一步步分析上述代码中闭包的生命周期:

1. 全局上下文创建阶段


全局内存:
- outer: 函数对象
  - [[Environment]]: 全局环境
- fn: undefined
  1. 执行到var fn = outer(),调用outer函数
调用栈:
[全局执行上下文] → [outer执行上下文]

outer执行上下文:
- 变量环境:
  - num: 0
  - inner: 函数对象
    - [[Environment]]: outer的变量环境(当前环境)
  1. 返回inner函数,outer执行上下文出栈,由于inner函数已经于outer的词法环境捆绑在一起,形成了闭包。虽然outer函数已经执行完毕,但由于inner[[Environment]]引用到了inner的词法环境,并不会被垃圾回收。
调用栈:
[全局执行上下文] ← outer已弹出

堆内存:
- inner函数对象
  - [[Environment]]: → outer的变量环境(闭包)
    - num: 0

全局变量:
- fn: → inner函数对象
  1. 第一次执行console.log(fn())
调用栈:
[全局执行上下文] → [inner执行上下文]

inner执行上下文:
- 变量环境: {} (空的,没有局部变量)
- 作用域链: [inner变量环境, outer闭包环境, 全局环境]

根据作用域链找到num变量,num++后由 0 变成 1。 5. inner返回结果,弹出调用栈,输出结果为1

调用栈:
[全局执行上下文]

堆内存中的闭包:
- num: 1 (值已更新)
  1. 执行第二次console.log(fn())
调用栈:
[全局执行上下文] → [新的inner执行上下文]

新的inner执行上下文:
- 变量环境: {} 
- 作用域链: [新的inner变量环境, outer闭包环境, 全局环境]

再次根据作用域链找到num变量,num++后由 1 变成 2。最后的内存状态:

堆内存:
├── outer闭包环境
│   └── num: 2
│
├── inner函数对象
│   ├── [[Environment]]: → outer闭包环境
│   └── 函数代码
│
└── 全局环境
    ├── outer: 函数对象
    └── fn: → inner函数对象

调用栈:
└── 全局执行上下文(空闲)

⚠️ 重要提醒

闭包会延长变量的生命周期,如果闭包持有大量数据或DOM引用,可能导致内存泄漏。使用时需注意适时释放资源。

由上述的执行流程不难发现闭包的形成过程以及将函数outer的变量寿命延长

闭包的实战应用场景

闭包在现代JavaScript开发中无处不在,是构建模块化、可复用代码的基石。

1. 数据封装与私有变量

实现信息的隐藏和保护,创建具有私有状态的组件:

function createCounter(initialValue = 0) {
  let count = initialValue;
  return {
    increment() {
      return ++count;
    },
    decrement() {
      return --count;
    },
    reset() {
      count = initialValue;
    },
    get() {
      return count;
    },
  };
}
const selfCounter = createCounter(10);
console.log(selfCounter.get()); // 10
console.log(selfCounter.increment()); // 11

2. 函数记忆化(Memoization)

优化性能,避免重复计算:

function power() {
  var map = new Map();
  return function (x) {
    if (map.has(x)) {
      return map.get(x);
    } else {
      var result = x ** 2;
      map.set(x, result);
      return result;
    }
  };
}
var pow = power();
console.log(pow(2));
console.log(pow(3));
console.log(pow(2));

3. 实现柯里化

创建可配置、可复用的函数工厂:

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function (...nextArgs) {
        return curried.apply(this, args.concat(nextArgs));
      };
    }
  };
}
function sum(a, b, c) {
  return a + b + c;
}
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3));
console.log(curriedSum(1, 2)(3));
console.log(curriedSum(1, 2, 3));

核心要点总结

通过本文的深入分析,我们可以得出以下关键结论:

🎯 闭包的本质

函数 + 外层词法环境 = 闭包。这种组合让内部函数能够突破传统作用域的限制,实现状态的持久化保存。

🔧 形成机制

当内部函数被创建时,它的 [[Environment]] 隐藏属性会自动引用当前的词法环境。这个引用关系就是闭包形成的技术基础。

💾 内存管理原理

JavaScript 的垃圾回收机制基于"引用计数"。由于内部函数持有着外层环境的引用,即使外层函数执行完毕,相关变量也不会被回收,从而实现了状态的保持。

🚀 实践价值

闭包不是抽象的学术概念,而是现代 JavaScript 开发的实用工具。从模块封装到性能优化,从函数式编程到异步处理,闭包都发挥着不可替代的作用。

进阶学习建议

虽然本文介绍了闭包的核心机制和典型应用,但要真正掌握闭包的艺术,还需要:

  1. 阅读优秀源码:学习 Lodash、Redux 等库中闭包的高级用法
  2. 动手实践:在项目中尝试实现自定义的闭包应用
  3. 深入理解:结合事件循环、this 绑定等概念,形成完整的知识体系

闭包是 JavaScript 这门语言的精髓之一,掌握它将为你的编程能力开启新的维度。Happy Coding! 🎉