一. 前言
在之前的文章中,我们知道了作用域链的查找规则(内层作用域能访问外层变量)和执行上下文的生命周期(函数执行完会被销毁)。那么问题来了:当函数的执行上下文被销毁后,我们还能访问它内部的变量吗? 🤔
听起来似乎不可能?但答案是可以的!今天我们就来揭秘实现这个“魔法”的技巧——闭包。
二. 作用域链 vs 执行上下文
先快速回顾两个关键概念:
- 作用域链 📜
- 一个由当前作用域 + 所有外层作用域组成的“查找链”
- 变量查找规则:由内向外(内部能访问外部)
- 执行上下文 🚀
- 代码执行时的临时环境
- 函数执行时创建,执行完立刻销毁
🚨 容易混淆的陷阱!
看这段代码:
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 (居然成功了!)
🤔 矛盾点:
- 按作用域规则:
bar能访问foo的变量 ✅ - 按执行上下文:
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 为解决“作用域存活”和“上下文销毁”矛盾的方案
- 合理使用闭包 = 强大 ✨,滥用闭包 = 内存压力 💥