变量声明(Variable Declaration)

JavaScript 中的变量声明:varletconst 的使用和底层原理


📖 入门:基础使用

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;

步骤详解

  1. 词法分析(Lexical Analysis)

    • 将代码分解为 tokens:varx=10
  2. 语法分析(Parsing)

    • 构建抽象语法树(AST)
    • 识别为变量声明语句
  3. 作用域分析(Scope Analysis)

    • 确定变量属于全局作用域或函数作用域
    • 对于全局 var,确定添加到全局对象
  4. 创建变量绑定(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):存储 letconstclass
  • 对象环境记录(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 及之前:只有 varfunction,使用变量环境
  • ES6 引入let/const/class,需要新的机制
  • 向后兼容:保留变量环境,新增词法环境

7.2 技术原因

分离的好处

  1. 精确控制:变量环境和词法环境可以独立管理
  2. 作用域区分:函数作用域 vs 块级作用域
  3. 性能优化:可以针对不同环境进行优化
  4. 内存管理:可以更精确地控制变量的生命周期

📝 深入学习


📝 最佳实践

  1. 优先使用 const:不变的值用 const
  2. 需要重新赋值用 let:可变的值用 let
  3. 避免使用 var:使用 let/const 替代
  4. 声明在作用域顶部:提高代码可读性

🔗 相关链接


javascript 变量声明 作用域 变量提升 tdz