你是否曾经被var的提升、let的暂时性死区、函数内部的变量覆盖等问题困扰是否好奇 V8 引擎到底如何管理你的代码本文将带你走进 JavaScript 的执行上下文、词法环境、调用栈等核心概念并通过8 个完整的代码示例逐步揭示 JS 代码从编译到执行的底层秘密。核心概念速览在分析代码之前我们先快速梳理一下 JavaScript 的执行机制执行上下文Execution Context每当执行全局代码或调用一个函数时V8 都会创建一个执行上下文对象。它包含三部分变量环境Variable Environment存储var声明和function声明的绑定。词法环境Lexical Environment存储let、const声明的绑定并支持块级作用域。可执行代码按顺序执行的语句。编译阶段Creation Phase在执行代码之前V8 会先“编译”当前执行上下文创建执行上下文对象。扫描形参和变量声明var在变量环境中添加键初始值为undefined。统一形参与实参的值仅在函数上下文中。扫描函数声明function在变量环境中添加键并将值指向函数体会覆盖同名的变量声明。执行阶段Execution Phase从上到下逐行执行代码修改变量的值、调用函数等。调用栈Call StackV8 用来管理函数调用关系的栈结构。每个函数被调用时它的执行上下文被压入栈函数执行完毕后弹出并销毁。暂时性死区TDZ从进入作用域到let/const声明语句之前这段区域内不能访问该变量否则报错ReferenceError。示例1全局变量提升与函数提升代码example1.jsshowName(极客时间); 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.jsconsole.log(func); // [Function: func] function func() { console.log(func); } var func 123; console.log(func); // 123输出[Function: func] 123解析在全局编译阶段扫描函数声明function func() {...}在变量环境中添加func: 函数体。扫描变量声明var func发现变量环境中已经存在func键直接忽略不会覆盖成undefined。执行阶段第1行打印func值为函数。第4行赋值func 123覆盖了原来的函数。第5行打印func输出123。结论函数声明比var声明优先级更高且var不会覆盖已存在的函数声明但后续赋值语句可以改变其值。示例3函数形参、变量声明与函数声明同名代码example3.jsvar 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的编译 执行编译阶段创建执行上下文找形参a→ 变量环境中添加a: undefined。统一形参与实参实参3→a 3。找函数声明function a() {}→ 发现已有a覆盖为函数体。找变量声明var a→ 发现a已存在忽略var b→ 添加b: undefined。此时变量环境中的a最终值为函数function a() {}。执行阶段console.log(a)→ 打印函数[Function: a]。var a 2→ 将a赋值为2。function a() {}已经提升过执行阶段不再处理。var b a→ 此时a是2所以b被赋值为2。console.log(a)→ 打印2。结论形参、函数声明同名时函数声明会覆盖形参的值后续赋值会改变该变量。示例4var的函数级作用域代码example4.jsfunction 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无视块级结构容易造成意外的变量覆盖。示例5let的块级作用域与暂时性死区代码example5.jsfunction 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暂时性死区。结论let和const带来了真正的块级作用域并且强制要求先声明后使用。示例6变量环境与词法环境的混用实战代码example6.jsfunction 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 3、d 5。块内的console.log(a)在当前词法环境中找不到a就向外层的变量环境查找找到a 1。块内的console.log(b)当前词法环境中有b3直接输出。离开块后块级词法环境被销毁外层b恢复为2。c由于是var它属于函数变量环境不受块级影响所以可以访问到4。d是let仅在块内词法环境中存在块外无法访问报错。结论变量环境var/function和词法环境let/const协同工作词法环境支持嵌套和作用域链。示例7let/const不可重复声明代码example7.jslet 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/const或var/function标识符。如果发现重复立即抛出语法错误整个代码块都不会执行。注意var和function可以重复声明后者覆盖前者但let/const绝对不允许。这是 ES6 为 JS 修复的一个重要“bug”让变量声明更加严谨。示例8调用栈与执行上下文的压栈/出栈代码example8.jsfunction 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调用栈变化过程执行全局代码创建全局执行上下文压栈。调用first()→ 创建first执行上下文压栈。在first中调用second()→ 创建second执行上下文压栈。在second中调用third()→ 创建third执行上下文压栈。third执行完毕其上下文弹出栈回到second。second执行完毕弹出回到first。first执行完毕弹出回到全局。全局代码执行完毕最终清空栈。每一个执行上下文在编译阶段都会重复“形参/变量提升、函数声明提升”等操作。调用栈保证了函数的嵌套调用能够正确返回并维持作用域链。总结通过以上8 个示例我们可以清晰地看到编译阶段决定了提升行为执行阶段真正赋值。变量环境var/function与词法环境let/const分工明确词法环境支持块级作用域和暂时性死区。调用栈是管理函数执行上下文的核心数据结构每个函数调用都会创建新的上下文并压栈。let和const修复了var的诸多缺陷推荐在绝大多数场景下优先使用。理解这些机制后你将不再被变量提升、作用域嵌套等问题困扰也能写出更可预测、更健壮的 JavaScript 代码。如果你觉得本文对你有帮助欢迎点赞、评论、转发关注我持续分享 JavaScript 底层原理与前端进阶知识。
深入理解JavaScript执行机制:从执行上下文到调用栈,八个代码示例彻底搞懂变量提升和作用域
发布时间:2026/6/8 8:15:49
你是否曾经被var的提升、let的暂时性死区、函数内部的变量覆盖等问题困扰是否好奇 V8 引擎到底如何管理你的代码本文将带你走进 JavaScript 的执行上下文、词法环境、调用栈等核心概念并通过8 个完整的代码示例逐步揭示 JS 代码从编译到执行的底层秘密。核心概念速览在分析代码之前我们先快速梳理一下 JavaScript 的执行机制执行上下文Execution Context每当执行全局代码或调用一个函数时V8 都会创建一个执行上下文对象。它包含三部分变量环境Variable Environment存储var声明和function声明的绑定。词法环境Lexical Environment存储let、const声明的绑定并支持块级作用域。可执行代码按顺序执行的语句。编译阶段Creation Phase在执行代码之前V8 会先“编译”当前执行上下文创建执行上下文对象。扫描形参和变量声明var在变量环境中添加键初始值为undefined。统一形参与实参的值仅在函数上下文中。扫描函数声明function在变量环境中添加键并将值指向函数体会覆盖同名的变量声明。执行阶段Execution Phase从上到下逐行执行代码修改变量的值、调用函数等。调用栈Call StackV8 用来管理函数调用关系的栈结构。每个函数被调用时它的执行上下文被压入栈函数执行完毕后弹出并销毁。暂时性死区TDZ从进入作用域到let/const声明语句之前这段区域内不能访问该变量否则报错ReferenceError。示例1全局变量提升与函数提升代码example1.jsshowName(极客时间); 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.jsconsole.log(func); // [Function: func] function func() { console.log(func); } var func 123; console.log(func); // 123输出[Function: func] 123解析在全局编译阶段扫描函数声明function func() {...}在变量环境中添加func: 函数体。扫描变量声明var func发现变量环境中已经存在func键直接忽略不会覆盖成undefined。执行阶段第1行打印func值为函数。第4行赋值func 123覆盖了原来的函数。第5行打印func输出123。结论函数声明比var声明优先级更高且var不会覆盖已存在的函数声明但后续赋值语句可以改变其值。示例3函数形参、变量声明与函数声明同名代码example3.jsvar 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的编译 执行编译阶段创建执行上下文找形参a→ 变量环境中添加a: undefined。统一形参与实参实参3→a 3。找函数声明function a() {}→ 发现已有a覆盖为函数体。找变量声明var a→ 发现a已存在忽略var b→ 添加b: undefined。此时变量环境中的a最终值为函数function a() {}。执行阶段console.log(a)→ 打印函数[Function: a]。var a 2→ 将a赋值为2。function a() {}已经提升过执行阶段不再处理。var b a→ 此时a是2所以b被赋值为2。console.log(a)→ 打印2。结论形参、函数声明同名时函数声明会覆盖形参的值后续赋值会改变该变量。示例4var的函数级作用域代码example4.jsfunction 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无视块级结构容易造成意外的变量覆盖。示例5let的块级作用域与暂时性死区代码example5.jsfunction 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暂时性死区。结论let和const带来了真正的块级作用域并且强制要求先声明后使用。示例6变量环境与词法环境的混用实战代码example6.jsfunction 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 3、d 5。块内的console.log(a)在当前词法环境中找不到a就向外层的变量环境查找找到a 1。块内的console.log(b)当前词法环境中有b3直接输出。离开块后块级词法环境被销毁外层b恢复为2。c由于是var它属于函数变量环境不受块级影响所以可以访问到4。d是let仅在块内词法环境中存在块外无法访问报错。结论变量环境var/function和词法环境let/const协同工作词法环境支持嵌套和作用域链。示例7let/const不可重复声明代码example7.jslet 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/const或var/function标识符。如果发现重复立即抛出语法错误整个代码块都不会执行。注意var和function可以重复声明后者覆盖前者但let/const绝对不允许。这是 ES6 为 JS 修复的一个重要“bug”让变量声明更加严谨。示例8调用栈与执行上下文的压栈/出栈代码example8.jsfunction 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调用栈变化过程执行全局代码创建全局执行上下文压栈。调用first()→ 创建first执行上下文压栈。在first中调用second()→ 创建second执行上下文压栈。在second中调用third()→ 创建third执行上下文压栈。third执行完毕其上下文弹出栈回到second。second执行完毕弹出回到first。first执行完毕弹出回到全局。全局代码执行完毕最终清空栈。每一个执行上下文在编译阶段都会重复“形参/变量提升、函数声明提升”等操作。调用栈保证了函数的嵌套调用能够正确返回并维持作用域链。总结通过以上8 个示例我们可以清晰地看到编译阶段决定了提升行为执行阶段真正赋值。变量环境var/function与词法环境let/const分工明确词法环境支持块级作用域和暂时性死区。调用栈是管理函数执行上下文的核心数据结构每个函数调用都会创建新的上下文并压栈。let和const修复了var的诸多缺陷推荐在绝大多数场景下优先使用。理解这些机制后你将不再被变量提升、作用域嵌套等问题困扰也能写出更可预测、更健壮的 JavaScript 代码。如果你觉得本文对你有帮助欢迎点赞、评论、转发关注我持续分享 JavaScript 底层原理与前端进阶知识。