JavaScript 作用域链与闭包深度解析

62 阅读5分钟

一、前置知识:执行上下文、词法环境与作用域

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


二、作用域链:变量查找的路径

作用域链 = 当前词法环境 + 外层词法环境 + ... + 全局环境

查找规则:

  1. 首先在当前词法环境(LE)  中查找(如块级 let/const);
  2. 若未找到,再在当前变量环境(VE)  中查找(如函数内 var);
  3. 若仍未找到,沿 outer 指针进入外层词法环境,重复步骤 1–2;
  4. 直到全局环境,若仍未找到,则抛出 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说明
1if 块LE只有 myName,无 text
2if 块VE块级作用域无 VE(var 不在此提升)
3bar 函数LE有 text1let),但无 text
4bar 函数VE有 myNamevar),但无 text
5全局LE找到 let text = 1

💡 关键点:bar 是在全局作用域中声明的,因此它的 outer 指向全局词法环境(LE)。
尽管它在 foo 内部被调用,调用位置不影响作用域链,只影响执行时机。

🚫 常见误解:认为“在 foo 里调用 bar 就能访问 foo 的变量”——这是错误的!
只有嵌套函数(即 bar 定义在 foo 内部)才能形成闭包并访问外层变量。


三、闭包(Closure):跨越生命周期的变量捕获

什么是闭包?

闭包 = 函数 + 其声明时的词法环境(对自由变量的引用)

当一个内部函数被返回并在外部调用时,它仍能访问其外层函数的变量,即使外层函数已执行完毕(执行上下文出栈)。

闭包形成条件:

  1. 函数嵌套函数;
  2. 内部函数引用了外层变量(称为自由变量);
  3. 内部函数在外部被访问(如通过 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 中被引用的变量(myNametext1);
  • 变量查找路径为: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)。