JavaScript 中的作用域链与闭包详解:从词法作用域看原理

键盘旁的猫
键盘旁的猫
发布时间:2026-10-15| 3人浏览过
原创

本文深入解析 JavaScript 中的三大核心概念:作用域链、词法作用域与闭包。通过具体代码案例分析作用域的查找顺序,阐明词法作用域(静态作用域)的决定性因素,并揭示闭包在函数执行结束后仍保持对内部变量引用的底层机制。

结果或目标

理解 JavaScript 引擎在运行代码时,如何通过作用域链定位变量标识符,以及为何某些变量能在函数返回后依然保存在内存中。

准备条件

需要了解 JavaScript 的基本语法,熟悉函数的定义与调用,以及 `var`、`let`、`const` 等变量声明方式的区别。

真实操作顺序

第1步:理解基本的作用域链查找逻辑

在分析复杂情况前,先回顾常规的作用域表现。当代码访问变量时,JavaScript 引擎遵循“由内向外”的查找规则。

var a = '喜羊羊';
function A(){
    console.log(a); // 1. 打印 '喜羊羊'
    a = '美羊羊';   // 2. 修改全局变量 a
    function B(){
        console.log(a); // 3. 打印 '美羊羊'
    }
    B();
}
A();

在这个例子中,B 函数位于 A 函数内部,A 函数位于全局作用域。当 B 需要查找变量 a 时:

  1. 首先在 B 自身的局部作用域中查找,未找到。
  2. 向上查找至外层 A 作用域,仍未找到。
  3. 继续向上查找到全局作用域,找到 var a。

这就是作用域链的基本体现:当前层找不到,就去找上一级。

第2步:剖析词法作用域对外部引用的影响

很多时候,开发者会误以为函数内部的变量查找取决于函数的“调用位置”,但这正是常见的误区。JavaScript 采用的是词法作用域(Lexical Scope),也称为静态作用域。

词法作用域意味着作用域是由代码编写时的物理位置决定的,而不是运行时动态决定的。每个执行上下文的变量环境中都包含一个 outer 引用(通常指向 所示的内部结构),用于指向定义该函数时的外部执行上下文。

来看一个容易掉坑的例子:

function bar() {
    console.log(myName) // 这里的 myName 是多少?
}

function foo() {
    var myName = "极客邦"
    bar() // 注意这里是调用 bar,而非 bar 被定义在 foo 内
}

var myName = "极客时间"
foo()

直觉上可能会认为打印出 “极客邦”,但实际结果是 “极客时间”。

原因解析:

bar 函数是在全局作用域下定义的,而不是在 foo 函数内部定义的。根据词法作用域规则,bar 创建时的 outer 引用指向的是全局环境。因此,无论 bar 在哪里被调用(即使是在 foo 内部),它查找变量的起点始终是其定义时的外部环境——即全局作用域。在全局作用域中,myName 的值为 “极客时间”。

从上图可以看出,整个词法作用域链的顺序是固定不变的:bar 函数作用域 → 全局作用域。它不包含 foo 函数的作用域,因为 bar 在代码结构中并没有嵌套在 foo 内部。

结论: 作用域链在代码编译阶段就已经确定,只与代码的物理位置有关,与函数调用时机无关。

第3步:掌握闭包的运行机制

闭包(Closure)是 JavaScript 中最强大也最常被讨论的概念。从上面的例子延伸,如果内部函数引用了外部函数的变量,那么当外部函数执行完毕后,这些变量会被回收吗?

观察以下代码:

function foo() {
    var myName = "极客时间"
    let test1 = 1
    const test2 = 2
    
    var innerBar = {
        getName: function(){
            console.log(test1)
            return myName
        },
        setName: function(newName){
            myName = newName
        }
    }
    return innerBar
}

var bar = foo()
bar.setName("极客邦")
console.log(bar.getName()) // 输出:1, "极客邦"

这里存在一个关键疑问:foo 函数执行完毕后,其执行上下文从调用栈中弹出,按照常规逻辑,局部变量应当被垃圾回收。为什么 bar.getName() 依然能访问到 myName 和 test1?

原理分析:

当 foo 返回对象 innerBar 时,由于 innerBar 中的 getName 和 setName 方法(闭包)引用了 foo 内部的变量 myName 和 test1,JavaScript 引擎检测到这些外部变量仍有活跃的外部引用,因此不会将它们从内存中清除。

可以将这理解为给 setName 和 getName 配备了一个专属背包,里面装着它们所依赖的外部变量快照。只要这两个方法还存在(如赋值给 bar),这个背包就会一直存在于内存中。

完成判断

闭包的定义: 能够访问其他函数内部变量的函数,被称为闭包。更严谨地说,在 JavaScript 中,内部函数总是可以访问其外部函数中声明的变量;当外部函数返回内部函数或其方法后,即使外部函数已结束,内部函数仍能引用这些变量,这些变量的集合即为闭包。

判断是否形成闭包的关键标准:
1. 函数嵌套。
2. 内部函数引用了外部函数的变量。
3. 外部函数返回了内部函数(或被其他方式持有引用)。

原文有依据的排错

常见错误在于混淆“调用位置”与“定义位置”。许多开发者认为函数在哪里被调用,它的词法环境就在哪里。请务必记住:词法作用域只看定义位置,不看调用位置。这也是调试跨模块作用域问题时最容易产生的误区。

简要总结

JavaScript 的作用域链由词法作用域静态决定,决定了变量查找的路径;而闭包则是这一机制带来的副作用与应用,允许内部函数在外部生命周期结束后依然访问私有变量。理解这两者,是写出健壮、可维护前端代码的基础。

以上就是 JavaScript 中的作用域链与闭包原理的详细解读,更多关于 JavaScript 进阶知识的资料请关注本站其它相关文章!

相关标签:

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

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 算法库。