当前位置: 首页 > news >正文

深入理解JavaScript执行机制:从执行上下文到调用栈,八个代码示例彻底搞懂变量提升和作用域

你是否曾经被var的提升、let的暂时性死区、函数内部的变量覆盖等问题困扰?是否好奇 V8 引擎到底如何管理你的代码?本文将带你走进 JavaScript 的执行上下文、词法环境、调用栈等核心概念,并通过8 个完整的代码示例,逐步揭示 JS 代码从编译到执行的底层秘密。

核心概念速览

在分析代码之前,我们先快速梳理一下 JavaScript 的执行机制:

  • 执行上下文(Execution Context):每当执行全局代码或调用一个函数时,V8 都会创建一个执行上下文对象。它包含三部分:

    • 变量环境(Variable Environment):存储var声明和function声明的绑定。

    • 词法环境(Lexical Environment):存储letconst声明的绑定,并支持块级作用域。

    • 可执行代码:按顺序执行的语句。

  • 编译阶段(Creation Phase):在执行代码之前,V8 会先“编译”当前执行上下文:

    1. 创建执行上下文对象。

    2. 扫描形参和变量声明(var),在变量环境中添加键,初始值为undefined

    3. 统一形参与实参的值(仅在函数上下文中)。

    4. 扫描函数声明(function),在变量环境中添加键,并将值指向函数体(会覆盖同名的变量声明)。

  • 执行阶段(Execution Phase):从上到下逐行执行代码,修改变量的值、调用函数等。

  • 调用栈(Call Stack):V8 用来管理函数调用关系的栈结构。每个函数被调用时,它的执行上下文被压入栈;函数执行完毕后,弹出并销毁。

  • 暂时性死区(TDZ):从进入作用域到let/const声明语句之前,这段区域内不能访问该变量,否则报错ReferenceError


示例1:全局变量提升与函数提升

代码:example1.js

showName('极客时间'); console.log(myName); var myName = 'fl'; function showName(name) { // 注意:形参 name 已存在,下方 let 会导致重复声明错误 let name = '时间'; // SyntaxError: Identifier 'name' has already been declared console.log(name); var b = 1; console.log('函数showName执行', name); }

输出与解析:

实际运行会抛出SyntaxError: Identifier 'name' has already been declared,因为函数参数name已经绑定到当前函数作用域,而let name试图重复声明。

但是,我们先忽略这个错误,看看 V8 在编译阶段做了什么:

  • 全局编译阶段

    • 变量环境:添加myName: undefined,添加showName: function(...)(函数提升优先)。

  • 函数showName编译阶段(假设没有let name错误):

    • 变量环境:形参name初始为undefined,然后被实参'极客时间'覆盖;接着var b被提升为b: undefined

    • 函数体内没有其他函数声明。

这个例子告诉我们:函数声明整体提升,var变量提升但值为undefined,而let在词法环境中不允许在声明前访问,也不允许与形参或其它let重名。

为了让机制更清晰,下面提供一个修正版(展示正常的提升效果):

// 修正版示例 showName('极客时间'); console.log(myName); // undefined var myName = 'fl'; function showName(name) { console.log('函数showName执行', name); // 函数showName执行 极客时间 var b = 1; }

输出:

函数showName执行 极客时间 undefined
  • 第1行调用showName正常,因为函数提升。

  • 第2行打印myName,此时变量提升但未赋值,输出undefined

  • 第4行执行赋值myName = 'fl',但代码中未再打印。


示例2:函数声明与变量声明的优先级

代码:example2.js

console.log(func); // [Function: func] function func() { console.log('func'); } var func = '123'; console.log(func); // '123'

输出:

[Function: func] 123

解析:

在全局编译阶段:

  1. 扫描函数声明function func() {...},在变量环境中添加func: 函数体

  2. 扫描变量声明var func,发现变量环境中已经存在func键,直接忽略(不会覆盖成undefined)。

  3. 执行阶段:

    • 第1行:打印func,值为函数。

    • 第4行:赋值func = '123',覆盖了原来的函数。

    • 第5行:打印func,输出'123'

结论:函数声明比var声明优先级更高,且var不会覆盖已存在的函数声明,但后续赋值语句可以改变其值。


示例3:函数形参、变量声明与函数声明同名

代码:example3.js

var a = 1; function fn(a) { console.log(a); // 输出什么? var a = 2; function a() {} var b = a; console.log(a); // 输出什么? } fn(3);

输出:

[Function: a] 2

详细解析(函数fn的编译 + 执行):

  1. 编译阶段(创建执行上下文):

    • 找形参:a→ 变量环境中添加a: undefined

    • 统一形参与实参:实参3a = 3

    • 找函数声明:function a() {}→ 发现已有a覆盖为函数体。

    • 找变量声明:var a→ 发现a已存在,忽略;var b→ 添加b: undefined

    此时变量环境中的a最终值为函数function a() {}

  2. 执行阶段

    • console.log(a)→ 打印函数[Function: a]

    • var a = 2→ 将a赋值为2

    • function a() {}已经提升过,执行阶段不再处理。

    • var b = a→ 此时a2,所以b被赋值为2

    • console.log(a)→ 打印2

结论:形参、函数声明同名时,函数声明会覆盖形参的值;后续赋值会改变该变量。


示例4:var的函数级作用域

代码:example4.js

function varTest() { var x = 1; if (true) { var x = 2; console.log(x); // 2 } console.log(x); // 2 } varTest();

输出:

2 2

解析:

  • var声明的变量没有块级作用域,只有函数作用域(或全局作用域)。

  • 在编译阶段,var x被提升到函数顶部,初始为undefined

  • 执行第2行:x = 1

  • 进入if块,var x = 2并不会创建新变量,而是修改同一个函数作用域中的x

  • 因此两次打印都是2

结论var无视块级结构,容易造成意外的变量覆盖。


示例5:let的块级作用域与暂时性死区

代码:example5.js

function varTest() { var x = 1; if (true) { let x = 2; // 块级作用域内的新变量 let b = 3; console.log(x); // 2 } // console.log(b); // 报错:b is not defined console.log(x); // 1 } varTest();

输出:

2 1

解析:

  • 外层var x属于函数作用域。

  • 进入if块时,词法环境会为块级作用域创建一个新的记录。

  • let x = 2只在块内有效,不影响外层的x

  • 块内的b在外部无法访问。

  • 如果在let x之前访问x(例如在块开头写console.log(x)),会报ReferenceError(暂时性死区)。

结论letconst带来了真正的块级作用域,并且强制要求先声明后使用。


示例6:变量环境与词法环境的混用实战

代码:example6.js

function foo() { var a = 1; let b = 2; { // 块级作用域开始 let b = 3; var c = 4; let d = 5; console.log(a); // 1 console.log(b); // 3 } console.log(b); // 2 console.log(c); // 4 console.log(d); // ReferenceError: d is not defined } foo();

输出:

1 3 2 4 ReferenceError: d is not defined

解析(V8 内部视角):

  • 函数foo变量环境包含:avar)、cvar被提升,但c的赋值在块内,执行后才变为4)。

  • 词法环境外层包含b = 2

  • 当进入{ ... }块时,V8 会创建一个新的词法环境子环境,其中包含b = 3d = 5

    • 块内的console.log(a):在当前词法环境中找不到a,就向外层的变量环境查找,找到a = 1

    • 块内的console.log(b):当前词法环境中有b=3,直接输出。

  • 离开块后,块级词法环境被销毁,外层b恢复为2

  • c由于是var,它属于函数变量环境,不受块级影响,所以可以访问到4

  • dlet,仅在块内词法环境中存在,块外无法访问,报错。

结论:变量环境(var/function)和词法环境(let/const)协同工作,词法环境支持嵌套和作用域链。


示例7:let/const不可重复声明

代码:example7.js

let a = 1; let a = 2; // SyntaxError: Identifier 'a' has already been declared

或者:

var a = 1; let a = 2; // SyntaxError: Identifier 'a' has already been declared

输出:上述两种情况都会在编译阶段抛出SyntaxError,代码不会执行。

解析:

  • 在词法环境中,V8 会检查当前作用域是否已经存在同名的let/constvar/function标识符。

  • 如果发现重复,立即抛出语法错误,整个代码块都不会执行。

  • 注意:varfunction可以重复声明(后者覆盖前者),但let/const绝对不允许。

这是 ES6 为 JS 修复的一个重要“bug”,让变量声明更加严谨。


示例8:调用栈与执行上下文的压栈/出栈

代码:example8.js

function first() { console.log('first start'); second(); console.log('first end'); } function second() { console.log('second start'); third(); console.log('second end'); } function third() { console.log('third start'); console.log('third end'); } first();

输出:

first start second start third start third end second end first end

调用栈变化过程:

  1. 执行全局代码,创建全局执行上下文(压栈)。

  2. 调用first()→ 创建first执行上下文,压栈。

  3. first中调用second()→ 创建second执行上下文,压栈。

  4. second中调用third()→ 创建third执行上下文,压栈。

  5. third执行完毕,其上下文弹出栈,回到second

  6. second执行完毕,弹出,回到first

  7. first执行完毕,弹出,回到全局。

  8. 全局代码执行完毕,最终清空栈。

每一个执行上下文在编译阶段都会重复“形参/变量提升、函数声明提升”等操作。调用栈保证了函数的嵌套调用能够正确返回,并维持作用域链。


总结

通过以上8 个示例,我们可以清晰地看到:

  • 编译阶段决定了提升行为,执行阶段真正赋值。

  • 变量环境var/function)与词法环境let/const)分工明确,词法环境支持块级作用域和暂时性死区。

  • 调用栈是管理函数执行上下文的核心数据结构,每个函数调用都会创建新的上下文并压栈。

  • letconst修复了var的诸多缺陷,推荐在绝大多数场景下优先使用。

理解这些机制后,你将不再被变量提升、作用域嵌套等问题困扰,也能写出更可预测、更健壮的 JavaScript 代码。

如果你觉得本文对你有帮助,欢迎点赞、评论、转发!关注我,持续分享 JavaScript 底层原理与前端进阶知识。

http://www.jsqmd.com/news/973433/

相关文章:

  • 哪家钢格板厂家专业?2026年6月推荐TOP5对比项目防腐蚀评测案例适用场景 - 品牌推荐
  • AI幻觉不是Bug,而是智能体的预测性编码本能
  • GPT-4的1.8万亿参数与2%激活真相:MoE路由机制深度解析
  • Django安全检测实战包:自动爬取URL+多类型漏洞识别+MySQL注入验证
  • 2026年6月厨房用品供应链生产厂家推荐,小家电供应链/小家电尾货/日用百货供应链,厨房用品供应链直销厂家推荐 - 品牌推荐师
  • 2025-2026年上海搬家公司推荐:五大口碑产品评测大件搬运防磕碰市场份额价格 - 品牌推荐
  • 你的AR/机器人‘眼睛’准吗?手把手教你用手机和A4纸完成相机标定与精度验证
  • 不背单词里没有的单词
  • 玩转SSD1306的8种扫描模式:用Arduino实现OLED动画和特殊显示效果
  • 功耗管理与唤醒锁 (WakeLock) 架构文档
  • 第36章:AI辅助合约性能压测——使用loadtest、forge snapshot
  • MuleSoft+LLM企业级AI编排:构建可治理、可审计、可落地的认知流水线
  • 高州母婴除甲醛CMA甲醛检测治理公司深度测评:绿呼吸环保稳居榜首 - 一修哥咨询
  • 别再复制粘贴了!手把手教你理解CMSIS-DAP离线下载器里那串神秘代码(附ARM反汇编实战)
  • 广州母婴除甲醛CMA甲醛检测治理公司深度测评:绿呼吸环保稳居榜首 - 一修哥咨询
  • 藁城母婴除甲醛CMA甲醛检测治理公司深度测评:绿呼吸环保稳居榜首 - 一修哥咨询
  • Qt调用WPS导出Word报告踩坑记:管理员权限竟是罪魁祸首?
  • 从故障录波到数据分析:COMTRADE文件在继电保护调试中的完整工作流
  • AIGC】story_agent_loop架构初步探讨5
  • 鸿蒙Next实战开发(四):个人中心与系统设置页面开发
  • Win10老显卡焕新记:GTX 1660 SUPER安装最新TensorFlow/PyTorch前的CUDA踩坑实录
  • 避开这些坑!TMS320F280049 SDFM模块调试常见问题与解决方案汇总
  • 2026 安徽阜阳市彩钢瓦修缮 TOP4 权威推荐 + 避坑指南(全区域服务) - 本地便民网
  • AD9831输出不过零?一个电容或变压器就能搞定(附Multisim仿真验证)
  • 2026 安徽亳州市彩钢瓦修缮 TOP4 权威推荐 + 避坑指南(全区域服务) - 本地便民网
  • 51单片机+ADC0809测电压不准?可能是这些细节没做好(附校准方法与代码优化)
  • C#反编译工具横评:dotPeek、ILSpy、dnSpy到底怎么选?附.NET 8实战对比
  • 阜阳母婴除甲醛CMA甲醛检测治理公司深度测评:绿呼吸环保稳居榜首 - 一修哥咨询
  • Mythos推理能力解析:多跳因果链与反事实推演的工程化实现
  • Advanced Matplotlib:数据可视化中的信息架构与认知效率