【JavaScript】🎒 闭包揭密:JS 的魔法小背包

228 阅读3分钟

一. 前言

在之前的文章中,我们知道了作用域链的查找规则(内层作用域能访问外层变量)和执行上下文的生命周期(函数执行完会被销毁)。那么问题来了:当函数的执行上下文被销毁后,我们还能访问它内部的变量吗? 🤔

听起来似乎不可能?但答案是可以的!今天我们就来揭秘实现这个“魔法”的技巧——闭包

二. 作用域链 vs 执行上下文

先快速回顾两个关键概念:

  1. 作用域链 📜
    • 一个由当前作用域 + 所有外层作用域组成的“查找链”
    • 变量查找规则:由内向外(内部能访问外部)
  2. 执行上下文 🚀
    • 代码执行时的临时环境
    • 函数执行时创建,执行完立刻销毁

🚨 容易混淆的陷阱!

看这段代码:

var a = 10;
function foo2() {
  console.log(a); // 输出什么?
}

function foo1() {
  var a = 20;
  console.log(a); // 20
  foo2(); // 这里调用了foo2!
}
foo1();

错误分析

“当 foo2 执行时,它在 foo1 内部调用,所以 console.log(a) 应该找到 foo1 里的 a=20 吧?”

实际输出

20
10 // 咦?!

💡 真相时刻

问题出在作用域是静态的

关键区别:

特性作用域链 📜执行上下文 🚀
确定时机函数定义时决定函数调用时创建
是否可变永不改变动态创建/销毁
查找依据函数定义的位置函数调用的位置

✨ 记住:函数的作用域由它出生地决定,和在哪里调用无关!

三. 闭包:突破作用域的“魔法” ✨

终于到主角了!先看这段代码:

function foo() {
  var a = 1;
  return function bar() { 
    console.log(a); // 神奇地访问了foo的变量!
  };
}

var baz = foo(); // foo执行完,上下文应该销毁了?
baz(); // 输出 1 (居然成功了!)

🤔 矛盾点:

  1. 按作用域规则:bar 能访问 foo 的变量 ✅
  2. 按执行上下文:foo 执行完,变量 a 该被销毁了 ❌

JS 的解决方案:闭包诞生了!

1. 闭包是什么?

当内部函数使用了外部函数的变量,并被拿到外部执行时,JS 会创建一个“秘密背包”🎒,把内部函数和它用到的外部变量打包在一起。这个“背包”就是闭包。


即使 foo 的执行上下文销毁了,闭包(图中的 Closure)依然保存着变量 a,让 bar 能正常访问它!

2. 闭包的作用 🛠️

  • 访问外部函数作用域 🔍
  • 封装私有变量(避免全局污染) 🛡️
// 用闭包实现计数器
function createCounter() {
  let count = 0; // 外部无法直接修改
  return function() {
    return ++count; 
  };
}
const counter = createCounter();
counter(); // 1
counter(); // 2 (count被安全保存着)

3. 闭包的缺点 ⚠️

  • 内存泄漏风险:闭包中的变量会一直存在内存中,直到闭包被销毁。滥用可能导致内存占用过高!

四. 总结

🎉 闭包的核心:内部函数 + 它捕获的外部变量 = 闭包

回到最初的问题:除了闭包,还有哪些方法能在外部访问函数内部的变量?
(欢迎在评论区分享你的答案~ 💬)

关键理解:

  • 作用域是静态的(函数定义时确定)
  • 闭包是 JS 为解决“作用域存活”和“上下文销毁”矛盾的方案
  • 合理使用闭包 = 强大 ✨,滥用闭包 = 内存压力 💥