深入理解 JavaScript 作用域链与闭包:原理、绑定与实战解析

今天早睡
今天早睡
发布时间:2026-10-15| 5人浏览过
原创

JavaScript 中的作用域链与闭包是理解异步编程和内存管理的关键。本文将基于 ECMAScript 规范,从[[Environment]]内部属性出发,拆解词法环境、变量环境的构建过程,并深入探讨闭包的本质、变量绑定机制以及 var 与 let 在循环闭包中的不同表现。

深入作用域链与闭包的核心关联

作用域链与闭包紧密相连,理解前者是掌握后者的基础。在 JavaScript 中,如果内部函数使用了外部变量,这些变量是通过词法环境(由各类环境记录组成)串联起来的,最终在浏览器引擎中体现为作用域链 [[Scope]]。

闭包的触发需要一个独立的空间来管理从外部获取的变量。而这个变量的获取与绑定过程,完全依赖于作用域链。因此,只有理清作用域链的形成原理,才能深入理解闭包的工作机制。

完善的环境记录结构

仅仅将 VariableEnvironment.[[outerEnv]] 指向外部是不完备的。考虑到 JavaScript 中万物皆对象,若每次执行函数都动态解析全局词法来获取外部环境,将严重消耗性能。为此,ECMAScript 规范引入了更高效的机制。

[[Environment]] 内部属性

当函数被声明时,它就被附加了一个内部属性 [[Environment]]。根据 ECMAScript Function Objects 的内部槽定义,该属性是一个环境记录(Environment Record),其 [[outerEnv]] 直接指向声明该函数的词法环境。

内部槽 (Internal Slot)类型 (Type)描述 (Description)
[[Environment]]an Environment Record函数封闭过的环境记录。在评估函数代码时,用作外层环境。

执行上下文中的完整结构

在函数执行时创建的词法环境和变量环境,均存储在该函数的 [[Environment]] 中。以以下代码为例:

function foo() {
    var a = 1;
    let b = 2;
}
foo();

在函数执行前创建上下文时,较为完备的结构解释如下:

ExecutionContext: {
   [[Environment]](0x00): {
      LexicalEnvironment(0x01): {
         b -> nothing
         [[outerEnv]]: 0x02
      }
      VariableEnvironment(0x02): {
         a -> undefined
         [[outerEnv]]: 0x00
      }
      ...
      [[outerEnv]]: global
   }
}

闭包的本质与实现

函数实例化的概念

为了更好地解释闭包,首先需要明确函数实例化的概念。虽然函数声明和表达式在语法上有所不同,但它们在运行时都会创建函数对象的实例:

  • 函数声明:可以视为创建了原型 Function 的一个实例。
  • 函数表达式:则是创建函数的实例。

以下代码展示了同一函数代码块如何生成多个独立的函数实例:

function foo() {
    return function myFun() {}
}
const fun1 = foo()
const fun2 = foo()
console.log(fun1 === fun2) // false

这里 myFun 是函数表达式,而 fun1 和 fun2 是两个不同的函数实例。

什么是闭包

基于函数实例的概念,闭包可以理解为与函数实例的作用域链绑定的一个映像。每生成一个函数实例,其内部都会有一条由环境记录串成的作用域链。

在具体实践(如 V8 引擎)中,函数在预编译阶段会解析内部词法。无论嵌套深度如何,也无论子函数是否被调用,只要内部引用了外部变量,这些变量就会被存到同一个闭包上。由于这些变量是通过作用域链获取且绑定的,可以说闭包在一定程度上是作用域链的副本或映射。

这个闭包可视为父函数的一个属性,且同一个实例中的所有子函数共用同一个闭包。

变量绑定的关键性

为什么要强调“绑定”?

  • 当外部变量发生变化时,闭包中对应的变量也会随之变化。
  • 在闭包中修改外部变量,其绑定的环境记录中的变量也会同步更新。

验证代码如下:

let a = 1;
let b = 2;
function foo() {
  return function () {
    a += 1;
    b += 10;
    console.log(a, b);
  };
}
const bar = foo();
bar();  // 输出: 2 12
bar();  // 输出: 3 22
a += 10;
bar();  // 输出: 14 32
a = 0;
bar();  // 输出: 1 42
console.log(a) // 输出: 1

这种绑定机制解释了经典的面试题:为什么 var 声明的循环中 setTimeout 输出全是 6?

// var 版本
function foo() {
  for (var i = 0; i < 6; i++) {
    setTimeout(() => {
      console.log(i);
    }, i * 100);
  }
}
foo();  // 输出: 6 6 6 6 6

因为 i 使用 var 声明,它会“逸出”保存到 foo 的变量环境中。所有 setTimeout 匿名函数闭包中的 i 都与 foo 的环境记录绑定。循环结束后 i++ 使 i 变为 6,此时执行回调,所有闭包读取的是同一个 i,故输出均为 6。

对比 let 的表现:

// let 版本
function foo() {
  for (let i = 0; i < 6; i++) {
    setTimeout(() => {
      console.log(i);
    }, i * 100);
  }
}
foo();  // 输出: 0 1 2 3 4 5

使用 let 声明时,i 保存在最近的词法环境(即块级作用域)中。每次循环迭代都会形成一个新的块级作用域,因此每个 setTimeout 闭包绑定了不同的环境记录,从而能独立维护各自的值。

闭包的独立性与验证

在同一函数实例中,所有子函数共用一个闭包空间;但不同实例间的闭包空间是独立的。

同实例共用闭包

以下代码演示了同一实例中,即使某些子函数未被调用,其引用的变量依然存在於闭包中:

function foo() {
  let a = 1;
  const b = 2;
  let c = 3;
  let d = 4;
  
  function bar() {  // 验证未调用的情况
    console.log(a);
    function barSon() { 
      console.log(b);
    }
  }
  
  return function () {
    console.log(d);
    return {
      addNum() {
        d = "new" + d;
      },
    };
  };
}
const fun1 = foo();
fun1().addNum();    // 验证不同实例的闭包空间独立
fun1();

const fun2 = foo();
fun2();

上图显示,包含变量 a, b 的子函数并未被调用,但它们依然存在于闭包中,证明了闭包捕获的是引用而非当前值。

返回的匿名函数和 bar 使用的变量均在同一个闭包 foo.Closure 中。当我们通过 fun1.addNum() 修改 d 的值后,查看 fun2 的闭包,发现其中的 d 仍然是初始值 4。这证实了两个独立实例的闭包空间是完全隔离的。

总结

与静态的函数实例不同,闭包是一个运行期概念。它在函数执行过程中处于激活状态,并在函数实例调用结束后保持数据的最终状态,直到闭包被销毁。例如,反复调用同一函数实例生成的闭包,会看到数据状态的累积变化,这些值来源于上次调用的最终结果。

实际上,当函数执行并引用外部变量时,会新建一个环境记录作为闭包,存储在 [[Environment]].[[outerEnv]] 的可变绑定中。在浏览器环境中,只要函数内部引用外部变量,其内部属性 [[Scope]] 中就会产生名为 Closure 的对象。

最后,通过下图的形象说明闭包在作用域链中的位置。假设每个空间都有堆内存支持:

function foo() {
  let a = 1;
  const b = 2;
  let c = 3;
  let d = 4;
  function bar() {  
    console.log(a);
    function barSon() { 
      console.log(b);
    }
  }
  return function myFun() {
    console.log(d);
  };
}
const fun1 = foo();
// 词法编译时结构示意
ExecutionContext(foo): {
   [[outerEnv]]: global
   ...
   [[Environment]](0x00): {
       LexicalEnvironment(0x01): {
           [[outerEnv]]: 0x00
           ...
            a → nothing, b -> nothing, c -> nothing, d -> nothing
           bar: {
               LexicalEnvironment(0x02): {...}
               [[Environment]] : {
                   [[outerEnv]] : 0x10 // 指向闭包
                   ...
               }
               barSon: {
                   [[Environment]] : {
                       // 指向上一层函数的词法环境/闭包
                       [[outerEnv]] : 0x02
                       ...
                   }
               }
           }
           myFun: {
               [[Environment]] : {
                   [[outerEnv]] : 0x10 // 指向闭包
                   ...
               }
           }
       }
       // 即所谓的闭包,来源於外部的环境记录映射
       EnvironmentRecord(0x10): {
           a -> nothing, b -> nothing, d -> nothing
           [[outerEnv]]: 0x01  // 指向外部词法环境
       }
   }
   ...
}

以上就是 JavaScript 深入理解作用域链与闭包详情的详细内容,更多关于 JS 作用域链与闭包的文章资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。