变量声明(Variable Declaration)
JavaScript 中的变量声明:
var、let、const的使用和底层原理
📖 入门:基础使用
1. var 声明
基本语法:
var x = 10;
var name = "Alice";特点:
- ✅ 函数作用域(function scope)
- ✅ 变量提升(hoisting)
- ⚠️ 可以重复声明
- ⚠️ 可以在声明前访问(值为
undefined)
示例:
function test() {
console.log(a); // undefined(变量提升)
var a = 5;
console.log(a); // 5
}
test();
// 函数作用域示例
if (true) {
var x = 10;
}
console.log(x); // 10(函数作用域,可以访问)2. let 声明
基本语法:
let y = 20;
let age = 25;特点:
- ✅ 块级作用域(block scope)
- ✅ 不存在变量提升(存在 TDZ)
- ✅ 不能重复声明
- ✅ 必须在声明后使用
示例:
function test() {
// console.log(b); // 报错:Cannot access 'b' before initialization
let b = 10;
console.log(b); // 10
}
test();
// 块级作用域示例
if (true) {
let y = 20;
}
// console.log(y); // 报错:y is not defined(块级作用域)3. const 声明
基本语法:
const z = 30;
const PI = 3.14159;特点:
- ✅ 块级作用域(block scope)
- ✅ 必须初始化
- ✅ 值不可重新赋值(但对象属性可修改)
- ✅ 不存在变量提升(存在 TDZ)
示例:
const z = 30;
// z = 40; // 报错:Assignment to constant variable
// 对象属性可以修改
const obj = { name: "Alice" };
obj.name = "Bob"; // ✅ 允许
obj.age = 25; // ✅ 允许
// obj = {}; // ❌ 报错:不能重新赋值
// 数组元素可以修改
const arr = [1, 2, 3];
arr[0] = 10; // ✅ 允许
arr.push(4); // ✅ 允许🚀 提高:底层原理
深入学习:建议先阅读 执行上下文 了解 JavaScript 引擎的执行机制
1. JavaScript 引擎处理变量声明的完整流程
1.1 编译阶段(Compilation Phase)
引擎处理 var 声明的步骤:
var x = 10;步骤详解:
-
词法分析(Lexical Analysis)
- 将代码分解为 tokens:
var、x、=、10
- 将代码分解为 tokens:
-
语法分析(Parsing)
- 构建抽象语法树(AST)
- 识别为变量声明语句
-
作用域分析(Scope Analysis)
- 确定变量属于全局作用域或函数作用域
- 对于全局
var,确定添加到全局对象
-
创建变量绑定(Variable Binding)
- 在变量环境(Variable Environment)中创建绑定
- 初始化为
undefined
引擎内部实现(简化伪代码):
// 编译阶段
if (isGlobalScope) {
// 全局 var 声明
GlobalExecutionContext.VariableEnvironment.x = undefined;
// 因为变量环境 = 全局对象
globalThis.x = undefined; // window.x = undefined (浏览器)
} else {
// 函数内的 var 声明
FunctionExecutionContext.VariableEnvironment.x = undefined;
}1.2 执行阶段(Execution Phase)
变量赋值过程:
// 执行阶段
x = 10; // 在变量环境中查找 x,执行赋值引擎内部实现(简化伪代码):
// 执行阶段
if (isGlobalScope) {
globalThis.x = 10; // window.x = 10
} else {
FunctionExecutionContext.VariableEnvironment.x = 10;
}2. var 与全局对象的关系
2.1 全局执行上下文的变量环境
关键机制:
- 全局执行上下文的变量环境(Variable Environment)就是全局对象本身
- 这是 ECMAScript 规范的定义
ECMAScript 规范规定:
GlobalExecutionContext.VariableEnvironment = Global Object
实际效果:
var globalVar = 10;
// 以下三种方式访问的是同一个值
console.log(globalVar); // 10
console.log(window.globalVar); // 10 (浏览器)
console.log(this.globalVar); // 10 (全局 this 指向 window)引擎内部结构:
GlobalExecutionContext = {
VariableEnvironment: globalThis, // = window (浏览器) 或 global (Node.js)
LexicalEnvironment: {
EnvironmentRecord: {
// let/const 声明存储在这里
},
OuterLexicalEnvironment: null
},
ThisBinding: globalThis
};
// var 声明时
globalThis.globalVar = undefined; // 创建阶段
globalThis.globalVar = 10; // 执行阶段2.2 let/const 为什么不会添加到全局对象
关键区别:
var存储在变量环境(Variable Environment) = 全局对象let/const存储在词法环境(Lexical Environment) ≠ 全局对象
引擎内部结构:
var varVar = 1;
let letVar = 2;
const constVar = 3;
GlobalExecutionContext = {
// 变量环境 = 全局对象
VariableEnvironment: globalThis, // window (浏览器)
// globalThis.varVar = 1 ✅ 添加到这里
// 词法环境 ≠ 全局对象
LexicalEnvironment: {
EnvironmentRecord: {
letVar: 2, // ✅ 存储在这里
constVar: 3 // ✅ 存储在这里
},
OuterLexicalEnvironment: null
}
// ❌ 不会添加到 globalThis
};验证:
console.log(window.varVar); // 1 ✅ (变量环境 = window)
console.log(window.letVar); // undefined ❌ (词法环境 ≠ window)
console.log(window.constVar); // undefined ❌ (词法环境 ≠ window)3. 词法环境(Lexical Environment)详解
3.1 词法环境的结构
ECMAScript 规范定义:
LexicalEnvironment {
EnvironmentRecord: {
// 环境记录
// 存储 let、const、class、function 等绑定
},
OuterLexicalEnvironment: <reference> // 外部词法环境引用
}
环境记录类型:
- 声明式环境记录(Declarative Environment Record):存储
let、const、class等 - 对象环境记录(Object Environment Record):关联对象(如全局对象、
with语句) - 函数环境记录(Function Environment Record):函数执行上下文使用
- 模块环境记录(Module Environment Record):ES6 模块使用
3.2 let/const 在词法环境中的存储
代码:
let x = 10;
const y = 20;引擎内部实现:
GlobalExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
x: <uninitialized>, // 创建阶段:TDZ 状态
y: <uninitialized> // 创建阶段:TDZ 状态
},
OuterLexicalEnvironment: null
}
};
// 执行阶段
LexicalEnvironment.EnvironmentRecord.x = 10;
LexicalEnvironment.EnvironmentRecord.y = 20;3.3 TDZ(暂时性死区)的机制
TDZ 定义:
- 从词法环境创建到变量初始化之间的时间区域
- 在这个区域内访问变量会抛出
ReferenceError
引擎实现:
// 创建阶段
LexicalEnvironment.EnvironmentRecord.x = <uninitialized>; // TDZ 状态
// TDZ 检查
function GetValue(x) {
if (LexicalEnvironment.EnvironmentRecord.x === <uninitialized>) {
throw new ReferenceError("Cannot access 'x' before initialization");
}
return LexicalEnvironment.EnvironmentRecord.x;
}
// 执行阶段(初始化)
LexicalEnvironment.EnvironmentRecord.x = 10; // TDZ 结束4. 变量提升的底层机制
4.1 var 的提升:变量环境的初始化
代码:
console.log(x);
var x = 10;引擎处理流程:
编译阶段:
// 1. 扫描代码,发现 var 声明
// 2. 在变量环境中创建绑定
VariableEnvironment.x = undefined; // 初始化为 undefined执行阶段:
// 1. 执行 console.log(x)
console.log(VariableEnvironment.x); // undefined ✅
// 2. 执行 x = 10
VariableEnvironment.x = 10;为什么可以访问:因为变量环境在创建阶段就已经创建了绑定,初始化为 undefined。
4.2 let/const 的提升:词法环境的 TDZ
代码:
console.log(y);
let y = 20;引擎处理流程:
编译阶段:
// 1. 扫描代码,发现 let 声明
// 2. 在词法环境中创建绑定
LexicalEnvironment.EnvironmentRecord.y = <uninitialized>; // TDZ 状态执行阶段:
// 1. 执行 console.log(y)
// 2. 查找变量 y
function GetValue(y) {
if (LexicalEnvironment.EnvironmentRecord.y === <uninitialized>) {
throw new ReferenceError("Cannot access 'y' before initialization"); // ❌
}
return LexicalEnvironment.EnvironmentRecord.y;
}
// 3. 执行 y = 20(如果到达这里)
LexicalEnvironment.EnvironmentRecord.y = 20; // TDZ 结束为什么报错:虽然 let/const 也会被”提升”(在词法环境中创建绑定),但状态是 <uninitialized>,访问时会检查并抛出错误。
5. 函数作用域 vs 块级作用域的底层实现
5.1 函数作用域:变量环境
代码:
function test() {
if (true) {
var x = 10;
}
console.log(x); // 10
}引擎实现:
FunctionExecutionContext = {
VariableEnvironment: {
x: undefined // var 声明,整个函数作用域可见
},
LexicalEnvironment: {
// 空
}
};
// 执行阶段
if (true) {
VariableEnvironment.x = 10; // 在变量环境中赋值
}
console.log(VariableEnvironment.x); // 10 ✅关键:var 在整个函数作用域内共享同一个变量环境。
5.2 块级作用域:词法环境的嵌套
代码:
function test() {
if (true) {
let y = 20;
}
console.log(y); // ❌ ReferenceError
}引擎实现:
FunctionExecutionContext = {
VariableEnvironment: {},
LexicalEnvironment: {
EnvironmentRecord: {},
OuterLexicalEnvironment: null
}
};
// if 块执行时,创建新的词法环境
BlockLexicalEnvironment = {
EnvironmentRecord: {
y: 20
},
OuterLexicalEnvironment: <FunctionLexicalEnvironment>
};
// if 块结束后,BlockLexicalEnvironment 可能被销毁
// 因此外部无法访问 y关键:每个块级作用域创建新的词法环境,形成嵌套结构。
6. 重复声明的检查机制
6.1 var 的重复声明
代码:
var x = 1;
var x = 2; // ✅ 允许引擎处理:
// 第一次声明
VariableEnvironment.x = undefined;
VariableEnvironment.x = 1;
// 第二次声明
// 检查变量环境,发现 x 已存在
// 不创建新的绑定,直接使用现有的
// 因此允许重复声明6.2 let/const 的重复声明检查
代码:
let y = 1;
let y = 2; // ❌ SyntaxError引擎处理:
// 第一次声明
LexicalEnvironment.EnvironmentRecord.y = <uninitialized>;
LexicalEnvironment.EnvironmentRecord.y = 1;
// 第二次声明
// 检查词法环境,发现 y 已存在
// 抛出 SyntaxError: Identifier 'y' has already been declared关键区别:var 允许重复声明(覆盖),let/const 不允许(严格检查)。
7. 深入理解:为什么需要两个环境
7.1 历史原因
- ES5 及之前:只有
var和function,使用变量环境 - ES6 引入:
let/const/class,需要新的机制 - 向后兼容:保留变量环境,新增词法环境
7.2 技术原因
分离的好处:
- 精确控制:变量环境和词法环境可以独立管理
- 作用域区分:函数作用域 vs 块级作用域
- 性能优化:可以针对不同环境进行优化
- 内存管理:可以更精确地控制变量的生命周期
📝 深入学习
- 执行上下文详解 — 执行上下文的完整机制
- 作用域和闭包 — 作用域链和闭包原理
- ECMAScript 变量声明规范
- ECMAScript 词法环境规范
📝 最佳实践
- 优先使用
const:不变的值用const - 需要重新赋值用
let:可变的值用let - 避免使用
var:使用let/const替代 - 声明在作用域顶部:提高代码可读性
🔗 相关链接
- 作用域和闭包 — 深入理解作用域机制
- 数据类型 — 变量的数据类型
- ECMAScript 变量声明规范