JavaScript 词法作用域与闭包:从底层原理到实战理解
JS运行机制
词法作用域
“词法”这个词听起来有点抽象,其实它的意思很简单: “词法” = “和你写代码的位置有关” 。
换句话说,JavaScript 中很多行为在你写代码的时候就已经确定了,而不是等到程序运行时才决定。这种特性也叫静态作用域(static scoping)。
你可以这样理解:
代码怎么写的,它就怎么执行——这非常符合我们的直觉。
比如,let 和 const 声明的变量之所以不能在声明前使用(会报“暂时性死区”错误),就是因为它们属于词法环境的一部分。而词法环境正是由你在源代码中的书写位置决定的。你把变量写在哪里,它就在哪里生效,不能“穿越”到还没写到的地方去用——这很合理,也很直观。
所以,“词法”本质上就是:看代码结构,而不是看运行过程。
看一段关于词法作用域的代码
function bar() {
console.log(myName);
}
function foo() {
var myName = '极客邦'
bar()// 运行时
}
var myName = '极客时间'
foo();
这里输出的是
极客时间
为什么输出的不是 "极客邦"?
因为 bar 函数是在全局作用域中声明的,所以它的词法作用域链在定义时就已经固定为:自身作用域 → 全局作用域。
JavaScript 查找变量时,遵循的是词法作用域规则——也就是说,它只关心函数在哪里被定义,而不关心函数在哪里被调用。
当 bar 内部访问变量(比如 test 或 myName)时,引擎会先在 bar 自己的执行上下文中查找;如果找不到,就沿着词法作用域链向外层查找,也就是直接跳到全局作用域,而不会进入 foo 的作用域——尽管 bar 是在 foo 里面被调用的。
因此,bar 根本“看不见” foo 中的 myName = "极客邦",自然也就无法输出它。
![]()
总结:
JavaScript 使用 词法作用域(Lexical Scoping) ,也就是说,函数在定义时就决定了它能访问哪些变量,而不是在调用时。
词法作用域链:变量查找的路径
当 JavaScript 引擎执行代码时,会为每一段可执行代码创建一个 执行上下文(Execution Context) 。
每个执行上下文都包含一个 词法环境(Lexical Environment) ,它不仅保存了当前作用域中声明的变量,还持有一个指向外层词法环境的引用。这些嵌套的词法环境连接起来,就形成了 作用域链(Scope Chain) 。
- 全局执行上下文位于调用栈的底部,是程序启动时创建的。
- 每当调用一个函数,就会创建一个新的函数执行上下文,并将其压入调用栈。
- 当需要查找某个变量时,JavaScript 会从当前作用域开始,沿着作用域链由内向外逐层查找,直到找到该变量,或最终到达全局作用域为止。
这种机制确保了变量访问遵循词法作用域规则——即“在哪里定义,就看哪里的变量”,而不是“在哪里调用”。
看看这段关于作用域链和块级作用域的代码:
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();
这段代码的执行结果不会报错,而是会正常输出:
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 词法作用域机制的自然结果。
二、闭包形成的两个必要条件(缺一不可)
- 函数嵌套:内部函数引用了外部函数的变量;
-
内部函数被暴露到外部:比如通过
return返回、赋值给全局变量、作为回调传递等,并在外部被调用。
只有同时满足这两点,闭包才会真正“生效”。
三、经典示例:直观感受闭包
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 和 "极客邦"
✅ 输出结果:
1
1
极客邦
四、关键问题:为什么 foo 的变量没被垃圾回收?
-
通常情况下,函数执行结束后,其局部变量会被垃圾回收。
-
但在闭包场景中,只要内部函数仍被外部引用,JavaScript 引擎就会保留该函数所依赖的外部变量。
-
在本例中:
-
getName和setName引用了myName和test1→ 这两个变量被“捕获”并保留在内存中; -
test2没有被任何函数使用 → 被正常回收。
-
📌 重点:闭包不会阻止整个函数上下文销毁,只保留“被引用”的变量。
这既保证了功能,又避免了内存浪费。
![]()
五、闭包的本质与词法作用域的关系
1. 闭包的本质
闭包不是某种特殊语法,而是一种运行时行为:
函数 + 它出生时的词法环境 = 闭包
你可以把它想象成:函数随身带了一个“背包”,里面装着它定义时能访问的所有外部变量。无论它走到哪里(哪怕在全局调用),都能从背包里取用或修改这些数据。
2. 与词法作用域的关联
- 词法作用域:变量的作用域由代码书写位置决定(静态的、编译期确定)。
- 闭包:正是词法作用域在函数被传递到外部后依然生效的体现。
✅ 所以说:闭包不是额外特性,而是词法作用域 + 函数作为一等公民 的必然产物。
💡 记住一句话:
闭包不是“不让变量销毁”,而是“还有人用,所以不能销毁”。
它让 JavaScript 实现了私有状态、模块封装、回调记忆等强大能力。
理解闭包,你就真正迈入了 JavaScript 高阶编程的大门。