《从一道“诡异”输出题,彻底搞懂 JavaScript 的作用域与执行上下文》

🌐 深入理解 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)

作用域链决定了变量查找的路径。查找顺序如下:

  1. 当前执行上下文的词法环境 -》 变量环境
  2. 外层执行上下文的词法环境 -》 变量环境
  3. ... 直到全局执行上下文 (全部执行上下文变量环境中的 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);
  • 查找顺序:

    1. bar 的词法环境 → 无 test
    2. bar 的外层 → foo 的词法环境 → test = 3(当前块中的值)
    3. 如果没找到,继续向上

✅ 最终找到 test = 3,输出 3
⚠️ 但注意:if 块中 let myName 是局部变量,不影响外层 myName


🎯 五、重点结论总结

问题 答案
console.log(test) 输出什么? 3
为什么不是 12 因为 testfoo 的块级作用域中被重新定义为 3,且 bar() 能访问该作用域
myNamebar() 中取哪个值? "极客世界",因为 bar() 自己声明了 var myName
if 块中的 myName 影响外层吗? 不影响,let 是块级作用域
test 是否会被提升? let test = 2 不会被提升,但存在 TDZ;var test 会被提升

🧠 六、图解执行过程

图1:调用栈结构

图2:作用域链

图3:块级作用域栈


💡 七、总结

  1. 作用域是变量查找规则,同时还管理者变量的生命周期,块级作用域出栈就应该销毁变量(除非存在闭包),但是变量提升(hositing) 污染了环境,本应该销毁的变量没有被销毁。
  2. 为什么es6之前不支持块级作用域(其实在es3 中的catch 和 with 是块级作用域,但我们通常说es6 之后才支持块级作用域),没有了块级作用域,可以把作用域内部的变量统一提升到作用域顶部(执行执行上下文的作用域),可以统一管理变量,这是最快,最简单的设计。
  3. es6 是如何支持块级作用域的? 从执行上下文的角度分析,是栈结构的词法环境,将变量分离开了,执行完就出栈。
  4. 作用域链是变量的查找路径。在执行上下文的变量环境中存在一个outer 指针,它指向代码编写位置时的执行上下文。outer 指针指向的才是变量查找的路径,而不是看代码运行时的位置。

📚 八、参考资料与延伸阅读


✅ 九、结语

JavaScript 的作用域和执行上下文机制看似复杂,但一旦理解了执行上下文、调用栈、作用域链、变量提升和词法环境这几个核心概念,就能轻松应对各种"诡异"的行为。

记住一句话
"变量在哪里声明,就在哪里查找。"

------ 词法作用域的本质

相关推荐
正在走向自律40 分钟前
企业微信消息推送全链路实战:Java后端与Vue前端集成指南
前端·vue.js·企业微信·企业微信消息推送·官方企业微信
lcc18744 分钟前
Vue3 CompositionAPI的优势
前端·vue.js
五号厂房1 小时前
聊一聊前端下载文件N种方式
前端
code_Bo1 小时前
使用micro-app 多层嵌套的问题
前端·javascript·架构
小灰1 小时前
VS Code 插件 Webview 热更新配置
前端·javascript
进击的明明1 小时前
前端监控与前端兜底:那些我们平常没注意,但真正决定用户体验的“小机关”
前端·面试
前端老宋Running1 小时前
我只改了个头像,为什么整个后台系统都闪了一下?
前端·react.js·面试
r***01381 小时前
SpringBoot3 集成 Shiro
android·前端·后端
八哥程序员1 小时前
深入理解 JavaScript 作用域与作用域链
前端·javascript