一、前置知识:执行上下文、词法环境与作用域
1. 执行上下文(Execution Context)
每当 JS 引擎执行一段代码(全局或函数),就会创建一个执行上下文,包含:
- 变量环境(Variable Environment, VE) :存储
var声明的变量(存在变量提升)。 - 词法环境(Lexical Environment, LE) :存储
let/const、函数参数以及块级绑定。 - outer 指针:指向外层词法环境,构成作用域链。
✅ 注意:虽然 VE 和 LE 在 ECMAScript 规范中是两个结构,但它们共享同一个
outer指针。
变量查找时,引擎首先在当前 LE 中搜索;若未找到,再检查当前 VE(主要是var变量);若仍未找到,则通过outer指针进入外层 LE,重复此过程。
因此,outer 指针连接的是词法环境(LE) ,而变量查找路径是:当前 LE → 当前 VE → outer.LE → outer.VE → ... → 全局。
2. 词法作用域(Lexical Scope)
- 静态决定:函数在声明时的位置决定了它能访问哪些变量。
- 与调用位置无关:即使函数在别处被调用,它的作用域链仍由声明位置决定。
js
编辑
var myName = '极客时间';
function foo() {
var myName = '极客邦';
bar(); // 虽然在 foo 中调用,但 bar 的作用域链指向全局!
}
function bar() {
console.log(myName); // 输出 "极客时间",不是 "极客邦"
}
foo();
🔍 解析:
bar是在全局作用域中声明的,因此它的outer指向全局环境,无法访问foo内部的myName。
二、作用域链:变量查找的路径
作用域链 = 当前词法环境 + 外层词法环境 + ... + 全局环境
查找规则:
- 首先在当前词法环境(LE) 中查找(如块级
let/const); - 若未找到,再在当前变量环境(VE) 中查找(如函数内
var); - 若仍未找到,沿
outer指针进入外层词法环境,重复步骤 1–2; - 直到全局环境,若仍未找到,则抛出
ReferenceError。
💡 关键机制:outer 指针始终指向外层的词法环境(LE) ,而非变量环境(VE)。
这意味着作用域链本质上是由词法环境组成的链表,而 VE 仅作为当前作用域的“补充存储”。
✅ 正确示例分析
js
编辑
var myName = '极客时间';
let text = 1;
function foo() {
var myName = '极客邦';
let text = 2;
{
let text = 3;
bar(); // 调用 bar
}
}
function bar() {
var myName = '极客世界';
let text1 = 100;
if (1) {
let myName = 'Chrome 浏览器';
console.log(text); // 👈 关键行
}
}
foo();
执行结果:
✅ 输出:1
❌ 不会报错!
作用域链查找过程(当执行 console.log(text) 时):
| 步骤 | 作用域层级 | 查找位置 | 是否存在 text | 说明 |
|---|---|---|---|---|
| 1 | if 块 | LE | ❌ | 只有 myName,无 text |
| 2 | if 块 | VE | ❌ | 块级作用域无 VE(var 不在此提升) |
| 3 | bar 函数 | LE | ❌ | 有 text1(let),但无 text |
| 4 | bar 函数 | VE | ❌ | 有 myName(var),但无 text |
| 5 | 全局 | LE | ✅ | 找到 let text = 1 |
💡 关键点:
bar是在全局作用域中声明的,因此它的outer指向全局词法环境(LE)。
尽管它在foo内部被调用,调用位置不影响作用域链,只影响执行时机。
🚫 常见误解:认为“在
foo里调用bar就能访问foo的变量”——这是错误的!
只有嵌套函数(即bar定义在foo内部)才能形成闭包并访问外层变量。
三、闭包(Closure):跨越生命周期的变量捕获
什么是闭包?
闭包 = 函数 + 其声明时的词法环境(对自由变量的引用)
当一个内部函数被返回并在外部调用时,它仍能访问其外层函数的变量,即使外层函数已执行完毕(执行上下文出栈)。
闭包形成条件:
- 函数嵌套函数;
- 内部函数引用了外层变量(称为自由变量);
- 内部函数在外部被访问(如通过
return或赋值给全局变量)。
你的闭包示例解析
js
编辑
function foo() {
var myName = '极客时间';
let text1 = 1;
const text2 = 2;
var innerBar = {
getName: function () {
console.log(text1); // 引用了 text1(自由变量)
return myName; // 引用了 myName(自由变量)
},
setName: function (newName) {
myName = newName; // 修改闭包中的 myName
}
}
return innerBar; // 返回对象,其中方法形成闭包
}
var bar = foo(); // foo 执行完毕,上下文出栈
bar.setName("极客邦");
console.log(bar.getName()); // 输出 "极客邦"
关键机制:
getName和setName的[[Environment]](即 outer 指针)指向foo的词法环境(LE);- 即使
foo执行上下文出栈,V8 仍保留其 LE 中被引用的变量(myName,text1); - 变量查找路径为:
getName.LE → getName.VE → foo.LE(via outer)→ ...; - 因此,闭包函数能持续访问
foo中的自由变量。
💡 闭包的本质:通过 outer 指针维持对声明时词法环境的引用,从而延长局部变量的生命周期。
四、常见误区澄清
| 误区 | 正确理解 |
|---|---|
| “闭包是 return 的函数” | 闭包是函数 + 自由变量的组合。即使不 return,只要内部函数引用了外层变量,就存在闭包(只是可能被 GC) |
| “作用域链由调用位置决定” | 作用域链由函数声明位置决定(词法作用域),与调用栈无关 |
| “在 A 函数里调用 B,B 就能访问 A 的变量” | ❌ 错!除非 B 是在 A 内部定义的(嵌套),否则无法访问 |
“let/const 不会形成闭包” | 错!let/const 同样会被闭包捕获,且更安全(块级作用域) |
| “outer 指向变量环境” | ❌ 错!outer 指针始终指向外层的词法环境(LE) ,变量查找时才结合当前 VE |
五、总结要点
✅ 作用域链
- 静态结构,编译阶段确定;
- 由函数声明位置决定;
- 查找路径:当前 LE → 当前 VE → outer.LE → outer.VE → ... → 全局;
- outer 指针连接的是词法环境(LE) ,这是作用域链的核心。
✅ 闭包
- 形成条件:嵌套函数 + 引用外层变量 + 外部可访问;
- 本质:函数通过
outer指针对其声明时词法环境的持续引用; - 用途:数据封装、模块模式、防污染全局变量;
- 代价:可能造成内存泄漏(未释放的闭包变量)。
✅ 变量提升与块级作用域
var提升到函数顶部,存于 VE;let/const存在于块级 LE 中,不提升,有暂时性死区(TDZ)。