🌐 深入理解 JavaScript 的作用域与执行上下文:从变量提升到闭包
本文将带你系统性地解析 JavaScript 中最核心的机制之一------作用域与执行上下文。我们将通过一个具体的代码案例,结合执行栈、词法环境、变量环境和调用链,一步步揭示 JS 引擎如何"思考"变量查找与函数执行的过程。
🔍 一、引言:为什么我们要理解作用域?
在日常开发中,我们经常遇到这样的问题:
ini
var myName = "极客时间";
function foo() {
var myName = "极客邦";
bar();
}
function bar() {
console.log(myName); // 输出什么?
}
foo();
结果是 "极客时间",而不是 "极客邦"。
这背后到底发生了什么?
答案藏在 JavaScript 的作用域规则 和 执行上下文机制 中。
本文将以一段复杂的代码为例,带你层层剖析 JS 引擎的运行逻辑。
🧩 二、代码案例分析
我们来分析这段代码:
js
function bar() {
var myName = "极客世界";
let test1 = 100;
if (1) {
let myName = "Chrome 浏览器";
console.log(test);
}
}
function foo() {
var myName = "极客邦";
let test = 2;
{
let test = 3;
bar();
}
}
var myName = "极客时间";
let myAge = 10;
let test = 1;
foo();
❓ 提问:这段代码会输出什么?为什么会这样?
提示 :
console.log(test)在bar()内部执行,而test并未在bar()中声明!答案是:1
这是词法作用域的查找结果,bar 函数是定义在全局作用域中,在它的执行这个函数的执行上下文的变量环境中会有一个outer 指针,这个指针指向函数书写位置所在作用域的执行上下文。所以它会查找到test = 1。这是作用域链的查找规则。

📚 三、关键概念回顾(可作为章节标题)
1. 执行上下文(Execution Context)
每次函数调用时,JS 引擎都会创建一个执行上下文,包含以下三个部分:
- 变量环境(Variable Environment) :存储
var声明的变量和函数声明。 - 词法环境(Lexical Environment) :存储
let/const声明的变量,支持块级作用域。 - 外层环境引用(Outer Environment Reference) :指向父级执行上下文,构成作用域链。
💡 执行上下文分为三种类型:
- 全局执行上下文
- 函数执行上下文
- eval 执行上下文(不常用)
2. 调用栈(Call Stack)
当函数被调用时,其执行上下文会被压入调用栈,函数执行完毕后弹出。
css
[全局执行上下文]
↓
[foo函数执行上下文]
↓
[bar函数执行上下文]
图示说明:每层都包含自己的变量环境和词法环境。
3. 变量提升(Hoisting)
var声明的变量会在执行上下文创建阶段被提升到顶部,并初始化为undefined。- 函数声明也会被提升,且可以先调用再定义。
let/const不会被提升,但存在暂时性死区(TDZ) ,在声明前访问会报错。
js
console.log(a); // undefined(提升)
var a = 1;
console.log(b); // ReferenceError(TDZ)
let b = 2;
4. 作用域链(Scope Chain)
作用域链决定了变量查找的路径。查找顺序如下:
- 当前执行上下文的词法环境 -》 变量环境
- 外层执行上下文的词法环境 -》 变量环境
- ... 直到全局执行上下文 (全部执行上下文变量环境中的 outer = null)

✅ 作用域查找规则由函数定义的位置决定(词法作用域),而非调用位置。 (词法如何理解?编译阶段会进行词法分析,词法作用域是静态的,它在代码书写的时候就决定了。)
5. 块级作用域与词法环境栈
ES6 引入了 let/const,使得 {} 块内可以创建独立的作用域。
- 在词法环境内部维护了一个小型栈结构。
- 每个块级作用域对应一个词法环境对象 ,存放在词法环境的"栈"中。
- 块执行结束后,该词法环境出栈,变量不可访问(除非被闭包捕获)。
图示说明:嵌套块中
let test = 3;创建了一个新的词法环境,覆盖了外层的test。
6. 闭包(Closure)
闭包是词法作用域链书写代码时产生的自然结果。
在函数A中定义了一个函数B,函数A 的返回结果是函数B ,并在函数A 作用域之外调用了函数B。这就是一个闭包
函数B 记忆记住并访问它书写时所在的词法作用域(函数A 形成的作用域),这就产生了闭包。
scss
function foo() {
var a = 2;
funtion bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 这就是闭包的效果
闭包的本质是:函数可以记住并访问所在的词法作用域。
🔍 四、逐步解析代码执行流程
我们回到原始代码,逐行分析:
第一步:全局执行上下文创建
ini
var myName = "极客时间";
let myAge = 10;
let test = 1;
- 全局变量环境:
myName = "极客时间",test = 1 - 全局词法环境:
myAge = 10,test = 1 - 函数声明:
foo,bar被提升并绑定到变量环境中
⚠️ 注意:
let test = 1;是在全局词法环境中创建的。
第二步:调用 foo(),进入 foo 执行上下文
ini
function foo() {
var myName = "极客邦";
let test = 2;
{
let test = 3;
bar();
}
}
执行上下文结构:
| 组件 | 内容 |
|---|---|
| 变量环境 | myName = "极客邦", test = undefined(待赋值) |
| 词法环境 | test = 2(初始值) |
| 外层引用 | 指向全局执行上下文 |
var myName被提升,let test在词法环境中创建。
第三步:进入块级作用域 {}
ini
{
let test = 3;
bar();
}
- 创建新的词法环境,
test = 3 - 此时
test的查找优先级最高 → 使用3
第四步:调用 bar(),进入 bar 执行上下文
ini
function bar() {
var myName = "极客世界";
let test1 = 100;
if (1) {
let myName = "Chrome 浏览器";
console.log(test);
}
}
执行上下文结构:
| 组件 | 内容 |
|---|---|
| 变量环境 | myName = "极客世界" |
| 词法环境 | test1 = 100 |
| 外层引用 | 指向 foo 的执行上下文 |
✅
bar()中没有test的声明,因此查找作用域链。
第五步:执行 console.log(test)
arduino
console.log(test);
-
查找顺序:
bar的词法环境 → 无testbar的外层 →foo的词法环境 →test = 3(当前块中的值)- 如果没找到,继续向上
✅ 最终找到
test = 3,输出3
⚠️ 但注意:if块中let myName是局部变量,不影响外层myName。
🎯 五、重点结论总结
| 问题 | 答案 |
|---|---|
console.log(test) 输出什么? |
3 |
为什么不是 1 或 2? |
因为 test 在 foo 的块级作用域中被重新定义为 3,且 bar() 能访问该作用域 |
myName 在 bar() 中取哪个值? |
"极客世界",因为 bar() 自己声明了 var myName |
if 块中的 myName 影响外层吗? |
不影响,let 是块级作用域 |
test 是否会被提升? |
let test = 2 不会被提升,但存在 TDZ;var test 会被提升 |
🧠 六、图解执行过程
图1:调用栈结构

图2:作用域链

图3:块级作用域栈



💡 七、总结
- 作用域是变量查找规则,同时还管理者变量的生命周期,块级作用域出栈就应该销毁变量(除非存在闭包),但是变量提升(hositing) 污染了环境,本应该销毁的变量没有被销毁。
- 为什么es6之前不支持块级作用域(其实在es3 中的catch 和 with 是块级作用域,但我们通常说es6 之后才支持块级作用域),没有了块级作用域,可以把作用域内部的变量统一提升到作用域顶部(执行执行上下文的作用域),可以统一管理变量,这是最快,最简单的设计。
- es6 是如何支持块级作用域的? 从执行上下文的角度分析,是栈结构的词法环境,将变量分离开了,执行完就出栈。
- 作用域链是变量的查找路径。在执行上下文的变量环境中存在一个outer 指针,它指向代码编写位置时的执行上下文。outer 指针指向的才是变量查找的路径,而不是看代码运行时的位置。
📚 八、参考资料与延伸阅读
- MDN: Scope
- MDN: Hoisting
- 《JavaScript高级程序设计》第4章
- 《你不知道的JavaScript》上卷
✅ 九、结语
JavaScript 的作用域和执行上下文机制看似复杂,但一旦理解了执行上下文、调用栈、作用域链、变量提升和词法环境这几个核心概念,就能轻松应对各种"诡异"的行为。
记住一句话 :
"变量在哪里声明,就在哪里查找。"------ 词法作用域的本质