JavaScript 词法作用域与闭包:从底层原理到实战理解

JS运行机制

词法作用域

"词法"这个词听起来有点抽象,其实它的意思很简单: "词法" = "和你写代码的位置有关"

换句话说,JavaScript 中很多行为在你写代码的时候就已经确定了 ,而不是等到程序运行时才决定。这种特性也叫静态作用域(static scoping)。

你可以这样理解:

代码怎么写的,它就怎么执行------这非常符合我们的直觉。

比如,letconst 声明的变量之所以不能在声明前使用(会报"暂时性死区"错误),就是因为它们属于词法环境的一部分。而词法环境正是由你在源代码中的书写位置决定的。你把变量写在哪里,它就在哪里生效,不能"穿越"到还没写到的地方去用------这很合理,也很直观。

所以,"词法"本质上就是:看代码结构,而不是看运行过程

看一段关于词法作用域的代码

js 复制代码
function bar() {
    console.log(myName);
}
function foo() {
    var myName = '极客邦'
    bar()// 运行时
}
var myName = '极客时间'
foo();

这里输出的是

极客时间 为什么输出的不是 "极客邦"

因为 bar 函数是在全局作用域中声明的 ,所以它的词法作用域链在定义时就已经固定为:自身作用域 → 全局作用域

JavaScript 查找变量时,遵循的是词法作用域规则 ------也就是说,它只关心函数在哪里被定义 ,而不关心函数在哪里被调用

bar 内部访问变量(比如 testmyName)时,引擎会先在 bar 自己的执行上下文中查找;如果找不到,就沿着词法作用域链向外层查找,也就是直接跳到全局作用域 ,而不会进入 foo 的作用域 ------尽管 bar 是在 foo 里面被调用的。

因此,bar 根本"看不见" foo 中的 myName = "极客邦",自然也就无法输出它。

总结:

JavaScript 使用 词法作用域(Lexical Scoping) ,也就是说,函数在定义时 就决定了它能访问哪些变量,而不是在调用时

词法作用域链:变量查找的路径

当 JavaScript 引擎执行代码时,会为每一段可执行代码创建一个 执行上下文(Execution Context)

每个执行上下文都包含一个 词法环境(Lexical Environment) ,它不仅保存了当前作用域中声明的变量,还持有一个指向外层词法环境 的引用。这些嵌套的词法环境连接起来,就形成了 作用域链(Scope Chain)

  • 全局执行上下文位于调用栈的底部,是程序启动时创建的。
  • 每当调用一个函数,就会创建一个新的函数执行上下文,并将其压入调用栈。
  • 当需要查找某个变量时,JavaScript 会从当前作用域开始,沿着作用域链由内向外逐层查找,直到找到该变量,或最终到达全局作用域为止。

这种机制确保了变量访问遵循词法作用域规则------即"在哪里定义,就看哪里的变量",而不是"在哪里调用"。

看看这段关于作用域链和块级作用域的代码:

js 复制代码
function bar () {
  var myName = "极客世界";
  let test1 = 100;
  if (1) {
    let myName = "Chrome 浏览器"  // 1.先在词法环境查找一下
    console.log(test)
  }
}
function foo() {
  var myName = "极客邦";
  let test = 2;
  {
    let test = 3;
    bar()
  }
}
var myName = "极客时间";
let myAge = 10;
let test = 1;
foo();

这段代码的执行结果不会报错,而是会正常输出:

text 复制代码
1

原因正是基于 JavaScript 的 词法作用域(Lexical Scoping) 机制。

虽然 bar() 是在 foo() 内部被调用的,但它的声明位置在全局作用域 。因此,当 bar 内部引用变量 test 时,JavaScript 引擎会从 bar 自身的作用域开始查找;找不到时,就沿着词法作用域链 向外层查找------也就是直接跳到全局作用域 ,而不会进入 foo 的作用域。

由于全局作用域中存在 let test = 1;,所以 console.log(test) 最终输出的是 1

换句话说:变量查找看的是函数"在哪里定义",而不是"在哪里调用" 。这条由内向外的查找路径,就是我们所说的 作用域链

在 JavaScript 的设计中,每个函数的执行上下文 都包含一个内部指针(通常称为 [[Outer]] 或 "outer 引用"),它指向该函数定义时所在的作用域 ------也就是它的词法外层环境

当你在代码中嵌套定义多个函数时,每个函数都会通过这个 outer 指针,链接到它上一层的词法环境。这样一层套一层,就形成了一条静态的、由代码结构决定的链式结构 ,我们称之为 词法作用域链(Lexical Scope Chain)

正是这条链,决定了变量查找的路径:从当前作用域开始,沿着 outer 指针逐级向外搜索,直到全局作用域为止。

这种机制是 JavaScript 闭包、变量访问和作用域行为的核心基础。理解了 outer 指针如何连接各个词法环境,你就真正掌握了词法作用域链的本质。

闭包 ------前面内容的优雅升华

闭包(Closure)是 JavaScript 中一个基于词法作用域的核心机制。掌握它,不仅能写出更灵活、模块化的代码,还能轻松应对面试中的高频问题。下面用通俗易懂的方式,带你彻底搞懂闭包。


一、什么是闭包?

闭包 = 一个函数 + 它定义时所处的词法环境。

换句话说:

当一个函数即使在自己原始作用域之外被调用 ,仍然能够访问并操作其定义时所在作用域中的变量,这个函数就形成了闭包。

这并不是魔法,而是 JavaScript 词法作用域机制的自然结果


二、闭包形成的两个必要条件(缺一不可)

  1. 函数嵌套:内部函数引用了外部函数的变量;
  2. 内部函数被暴露到外部 :比如通过 return 返回、赋值给全局变量、作为回调传递等,并在外部被调用。

只有同时满足这两点,闭包才会真正"生效"。


三、经典示例:直观感受闭包

javascript 复制代码
function foo() {
  var myName = "极客时间";
  let test1 = 1;
  const test2 = 2; // 注意:test2 未被内部函数使用

  var innerBar = {
    getName: function () {
      console.log(test1); // 引用了外部变量 test1
      return myName;      // 引用了外部变量 myName
    },
    setName: function (newName) {
      myName = newName;   // 修改外部变量 myName
    }
  };

  return innerBar; // 将内部对象返回,使内部函数可在外部调用
}

// 执行 foo,获取返回的对象
var bar = foo(); // 此时 foo 已执行完毕,上下文出栈

// 在外部调用内部函数 ------ 闭包开始工作!
bar.setName("极客邦");
bar.getName();               // 输出:1
console.log(bar.getName());  // 输出:1 和 "极客邦"

输出结果:

text 复制代码
1
1
极客邦

四、关键问题:为什么 foo 的变量没被垃圾回收?

  • 通常情况下,函数执行结束后,其局部变量会被垃圾回收。

  • 但在闭包场景中,只要内部函数仍被外部引用 ,JavaScript 引擎就会保留该函数所依赖的外部变量

  • 在本例中:

    • getNamesetName 引用了 myNametest1 → 这两个变量被"捕获"并保留在内存中;
    • test2 没有被任何函数使用 → 被正常回收。

📌 重点:闭包不会阻止整个函数上下文销毁,只保留"被引用"的变量

这既保证了功能,又避免了内存浪费。


五、闭包的本质与词法作用域的关系

1. 闭包的本质

闭包不是某种特殊语法,而是一种运行时行为

函数 + 它出生时的词法环境 = 闭包

你可以把它想象成:函数随身带了一个"背包",里面装着它定义时能访问的所有外部变量。无论它走到哪里(哪怕在全局调用),都能从背包里取用或修改这些数据。

2. 与词法作用域的关联

  • 词法作用域 :变量的作用域由代码书写位置决定(静态的、编译期确定)。
  • 闭包 :正是词法作用域在函数被传递到外部后依然生效的体现。

✅ 所以说:闭包不是额外特性,而是词法作用域 + 函数作为一等公民 的必然产物。


💡 记住一句话
闭包不是"不让变量销毁",而是"还有人用,所以不能销毁"。

它让 JavaScript 实现了私有状态、模块封装、回调记忆等强大能力。

理解闭包,你就真正迈入了 JavaScript 高阶编程的大门。

相关推荐
GYY_y1 小时前
封装一个支持动态表头与权限控制的通用 VxeTable 组件
前端
某只天落1 小时前
Vue2 通用文件在线预览下载组件:一站式解决多类型文件处理需求(支持视频、文档、图片、Office)
前端
AY呀1 小时前
黑马喽大闹天宫与JavaScript的寻亲记:作用域与作用域链全解析
前端·javascript·面试
金融数据出海1 小时前
日本股票市场渲染 KlineCharts K 线图
前端·后端
是Yu欸1 小时前
DevUI MateChat 技术演进:UI 与逻辑解耦的声明式 AI 交互架构
前端·人工智能·ui·ai·前端框架·devui·metachat
梦想CAD控件1 小时前
AI生成CAD图纸(云原生CAD+AI让设计像聊天一样简单)
前端·javascript·vue.js
栀秋6661 小时前
JavaScript 中的 简单数据类型:Symbol——是JavaScript成熟的标志
前端
最爱老虎头1 小时前
Konvajs实现虚拟表格
javascript
Nayana1 小时前
前端控制批量请求并发
前端