一、变量提升:代码执行前的 “隐形操作”
先看一段看似 “逻辑颠倒” 的代码,感受变量提升的存在:
showName();
console.log(username); // undefined
// 全局变量声明
var username = "张三";
function showName (){
console.log('函数showName 执行了')
}
执行结果
函数showName 执行了
undefined
为什么函数能在定义前调用?变量未赋值却不报错?核心原因是:
JavaScript 引擎在代码实际执行前,会进行 “预编译”——
var声明的变量和function声明的函数,会被 “悄悄提升” 到当前作用域的顶部。这就是代码顺序颠倒却能正常运行的关键。
预编译后的 “真实执行顺序”
原始代码经过引擎处理后,会变成以下逻辑(相当于引擎的 “内部版本”):
// 1. 函数声明提升:完整提升(函数名+函数体)到作用域顶部
function showName (){
console.log('函数showName 执行了')
}
// 2. var 变量提升:仅提升“声明”,不提升“赋值”,默认值为 undefined
var username;
// -------------- 预编译结束,开始执行代码 --------------
// 3. 执行函数调用
showName();
// 4. 打印未赋值的变量(此时为默认值 undefined)
console.log(username);
// 5. 给提升后的变量赋值
username = "张三";
关键细节:提升优先级与函数表达式差异
1. 为什么函数提升在 var 前面?
function 声明的提升优先级高于 var 变量:
- 函数会被完整提升(包含函数体),直接成为可执行状态;
var仅提升 “变量声明”,赋值逻辑仍留在原地,提升后默认值为undefined。
2. 函数声明 vs 函数表达式(关键区别!)
只有 function 声明式 会完整提升,而 var 函数表达式(如 var fn = function(){})本质是普通 var 变量,仅提升变量名,不提升函数体:
// 先调用函数(定义在后面)
showName(); // 报错:showName is not a function(此时 showName 是 undefined)
// 函数表达式定义(var 变量 + 匿名函数赋值)
var showName = function() {
console.log("函数showName 执行了");
};
其真实执行顺序:
// 仅提升变量名,值为 undefined
var showName;
// 调用时 showName 是 undefined,不是函数 → 报错
showName();
// 赋值逻辑留在原地执行(此时才给变量绑定函数)
showName = function() {
console.log("函数showName 执行了");
};
变量提升带来的问题与解决方法
问题 1:变量声明前可访问(值为 undefined),逻辑混乱;
问题 2:var 无块级作用域,变量易穿透污染;
问题 3:允许重复声明,覆盖原有变量引发 bug。
解决方法:
- 避免使用
var,优先用let/const(无提升、支持块级作用域); - 若需兼容旧代码,遵循 “变量声明置顶” 原则,手动将
var变量和函数声明移至作用域顶部。
二、块级作用域:{} 圈起来的 “独立变量空间”
什么是块级作用域?
块级作用域指:变量 / 函数的作用范围被限制在一对花括号 {} 内,仅在该代码块(如 if、for、while、try/catch 或直接用 {} 包裹的代码)中有效,外部无法访问。
简单理解:块级作用域 = 用 {} 圈起来的 “独立房间”,变量出了这个房间就失效。
块级作用域的核心特点
- 变量仅在当前代码块内可见;
- 避免 “变量提升穿透”,防止变量污染;
- 支持嵌套(内部块可访问外部块变量,反之不行)。
经典误区:var 的 “函数作用域” vs 块级作用域
先看这段代码,为什么最后打印 localVar 会报错?
var globalVar = "全局变量";
function myFunction(){
var localVar = "局部变量";
console.log(globalVar); // 输出:全局变量
console.log(localVar); // 输出:局部变量
}
myFunction();
console.log(globalVar); // 输出:全局变量
console.log(localVar); // 报错:ReferenceError: localVar is not defined
很多人会疑惑:var 不支持块级作用域,为什么 localVar 不能在外部打印?核心是要分清 函数作用域 和 块级作用域 的区别:
var不支持块级作用域 →if/for等{}块内的var会 “穿透” 块,影响外部(比如if块内的var会成为函数作用域变量);- 但
var支持函数作用域 → 函数内用var声明的变量,会被限制在函数内部,外部完全访问不到(这就是localVar报错的原因)。
用两个例子直观对比:
// 例子1:var 不支持块级作用域(if 块内的 var 穿透到函数作用域)
function testBlock() {
if (true) {
var x = 10; // 无块级作用域,x 属于函数作用域
}
console.log(x); // 输出:10(x 穿透了 if 块,函数内可访问)
}
testBlock();
// 例子2:var 支持函数作用域(函数内的 var 无法穿透到外部)
function testFunction() {
var y = 20; // 函数作用域变量
}
testFunction();
console.log(y); // 报错:y is not defined(外部访问不到函数内的 var 变量)
实战对比:var vs let 的块级作用域差异
第一段代码(var + if (true))
var name = "张三";
function showName() {
console.log(name); // undefined(函数内 var name 提升,遮蔽全局 var,未赋值)
if (true) {
var name = "李四"; // var 无块级作用域,穿透到函数作用域,给提升后的变量赋值
}
console.log(name); // 输出:李四(函数内变量已赋值)
}
showName();
核心逻辑:var 有变量提升、无块级作用域,函数内的 var name 提升后遮蔽全局变量,if (true) 执行后赋值生效,最终输出 undefined → 李四。
第二段代码(let + if (false))
let name = "张三";
function showName() {
console.log(name); // 输出:张三(let 无提升,函数内无同名变量,直接访问全局 let)
if (false) {
let name = "李四"; // let 有块级作用域,但 if 不执行,变量未声明
}
}
showName();
核心逻辑:let 无变量提升、支持块级作用域,if (false) 导致块内变量未执行,函数内无同名变量遮蔽全局,最终仅输出 张三。
一句话总结
var提升 + 无块级作用域 → 函数内变量遮蔽全局,先undefined后赋值;let无提升 + 有块级作用域 → 未执行的块内变量不生效,直接访问全局值。
三、升华:JS 如何让变量提升与块级作用域和谐共存?(执行上下文视角)
ES6 引入块级作用域后,并未废除 ES5 的变量提升,而是通过执行上下文的 “双环境设计” (变量环境 + 词法环境)实现了二者的 “一国两制”—— 既兼容旧代码的 var 变量提升逻辑,又支持 let/const 的块级作用域特性,核心是通过分离存储、独立规则、协同查询,让新旧特性互不干扰、和谐工作。
1. 核心设计:执行上下文的 “双环境分工”
执行上下文是 JS 代码运行的 “环境容器”,ES6 对其扩展后,核心包含两大环境,各司其职:
| 环境类型 | 存储内容 | 核心规则(对应特性) | 数据结构 |
|---|---|---|---|
| 变量环境(VE) | var 声明的变量、function 声明 | 支持变量提升(预编译提升声明)、函数 / 全局作用域(无块级隔离) | 普通对象(键值对) |
| 词法环境(LE) | let/const 声明的变量 | 支持块级作用域、暂时性死区(无变量提升) | 栈结构(后进先出) |
核心思想:用 “变量环境” 保留 ES5 旧规则(兼容),用 “词法环境” 实现 ES6 新特性(升级),双环境独立存储但协同查询。
2. 第一步:编译阶段 —— 创建执行上下文,初始化双环境
JS 引擎执行代码前,会先编译代码并创建对应执行上下文(全局 / 函数),此时双环境会完成初始化,分别对应变量提升和暂时性死区:
(1)变量环境初始化(对应变量提升)
-
编译时,扫描当前作用域内所有
var变量和function声明:- 先提升
function声明:完整提升(函数名 + 函数体)到变量环境顶部(优先级高于var); - 再提升
var变量:仅提升声明(不提升赋值),默认值设为undefined,存入变量环境;
- 先提升
-
赋值语句不提升,留在原地等待执行阶段触发。
示例(全局作用域编译) :
var name = "张三";
function showInfo() {}
let age = 20;
编译后变量环境(全局):
{
showInfo: 函数体(完整提升),
name: undefined(仅声明提升)
}
(2)词法环境初始化(对应暂时性死区 + 块级作用域准备)
-
编译时,扫描当前作用域内所有
let/const变量:- 仅 “注册” 变量名,不赋值(状态为
uninitialized,区别于undefined); - 此时变量进入 “暂时性死区”:声明前访问会直接报错,而非
undefined; - 词法环境的核心是 栈结构:当前作用域(如全局、函数)的
let/const变量存入栈底,后续进入块级作用域(if/for等)时,会创建新的块级词法环境压入栈顶。
- 仅 “注册” 变量名,不赋值(状态为
示例(全局作用域编译) :词法环境(全局)栈初始化:
[
{ age: uninitialized } // 栈底:全局块级作用域的 let 变量
]
编译阶段总结:
- 变量环境的初始化 = 变量提升的实现(
var/function按规则提升并初始化); - 词法环境的初始化 = 暂时性死区的实现(
let/const注册但未赋值,不可访问); - 双环境初始化完成后,执行上下文进入 “执行阶段”。
3. 第二步:执行阶段 —— 块级作用域的实现(词法环境栈的操作)
执行阶段按代码顺序执行,当遇到 {} 块级作用域(如 if、for)时,词法环境的栈结构会发生 “压栈”“查询”“出栈” 操作,这正是 ES6 实现块级作用域的核心:
(1)进入块级作用域:压栈(创建块级词法环境)
当执行到 {} 块内,且块内有 let/const 声明时:
- 创建一个新的 “块级词法环境”(独立键值对对象);
- 将块内
let/const变量注册到该环境中(状态uninitialized),执行赋值语句后更新为实际值; - 将这个新环境压入词法环境栈顶(栈结构 “后进先出”,当前块环境为栈顶)。
示例(进入 if 块级作用域) :
function showInfo() {
if (true) {
let age = 25; // 块内 let 变量
}
}
showInfo();
进入 if 块后,词法环境栈(函数内):
[
{ age: 25 }, // 栈顶:if 块级词法环境(赋值后)
{ 无变量 }, // 中间:函数级词法环境
Outer: 全局词法环境(age: 20) // 栈底:外部引用
]
(2)块内变量查询:栈顶优先
执行块内代码时,访问变量会遵循 “栈顶优先” 规则:
- 先查询词法环境栈顶(当前块级环境):找到则直接使用;
- 未找到则向上回溯外层词法环境(函数级→全局级);
- 词法环境中未找到,再查询变量环境(
var/function); - 仍未找到则报错
xxx is not defined。
示例(块内查询 age) :块内 console.log(age) → 直接查询栈顶(if 块级环境)的 age: 25 → 输出 25。
(3)退出块级作用域:出栈(销毁块级词法环境)
当块内代码执行完毕:
- 栈顶的块级词法环境会被弹出(销毁);
- 块内
let/const变量随环境销毁,外部无法访问(实现块级隔离); - 词法环境栈恢复到进入块前的状态,后续查询会回溯到外层环境。
示例(退出 if 块后) :词法环境栈(函数内)恢复为:
[
{ 无变量 }, // 栈顶:函数级词法环境
Outer: 全局词法环境(age: 20) // 栈底
]
此时查询 age → 回溯到全局词法环境,输出 20。
4. ES6 支持块级作用域的本质:词法环境的栈结构
从执行上下文角度看,ES6 实现块级作用域的核心是 “词法环境的栈结构设计” :
- 块级作用域 = 词法环境栈中的一个 “栈帧”(块级环境);
- 进入块 → 压栈(新增栈帧,隔离块内变量);
- 退出块 → 出栈(销毁栈帧,释放变量,外部不可访问);
- 变量查询 → 栈顶优先(确保块内变量优先使用,不污染外层)。
而变量提升则通过 “变量环境的普通对象结构” 保留:var/function 存入普通对象,无块级隔离,遵循函数 / 全局作用域,实现旧规则兼容。
5. 核心总结:双环境协同的 “和谐之道”
- 分离存储:
var/function存入变量环境(旧规则),let/const存入词法环境(新规则),互不干扰; - 独立规则:变量环境实现变量提升,词法环境通过栈结构实现块级作用域 + 暂时性死区;
- 协同查询:查询时词法环境栈优先(新规则优先),变量环境兜底(兼容旧规则);
- 最终实现:ES5 代码按旧规则运行(变量提升、无块级),ES6 代码按新规则运行(块级作用域、无提升),二者和谐共存。
一句话概括:ES6 用 “栈结构的词法环境” 实现块级作用域,用 “普通对象的变量环境” 保留变量提升,双环境协同查询,既兼容历史又满足新需求。
总结
JavaScript 执行机制的核心演进,本质是「兼容历史」与「优化特性」的平衡:ES5 时代的变量提升虽简化了早期引擎实现,却带来了变量污染、逻辑混乱等问题;ES6 并未废除旧规则,而是通过执行上下文的「双环境设计」,让变量提升与块级作用域实现了和谐共存。
变量环境承载 var 与函数声明的提升逻辑,延续函数 / 全局作用域规则,保障旧代码兼容;词法环境以栈结构为核心,实现 let/const 的块级隔离与暂时性死区,解决了变量提升的缺陷。二者通过「栈顶优先、逐级回溯」的查询规则协同工作,既让 ES5 代码正常运行,又让 ES6 代码具备更严谨的作用域控制。
理解这一机制,能从底层解释「变量为何提前可访问」「块内变量为何不污染外部」等核心问题,也为后续学习闭包、执行上下文栈等知识点奠定基础。实际开发中,优先使用 let/const 遵循块级作用域规则,可有效减少变量污染与逻辑 bug,同时理解变量提升的底层逻辑,能更精准地处理旧代码兼容问题。