2.2 - let/const/var区别、暂时性死区
JavaScript 变量声明深度解析:let/const/var 与暂时性死区
深入理解变量声明的底层原理,从执行上下文到作用域链的完整解析
目录
核心概念速览
三者对比表
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 提升并初始化为 undefined | 提升但不初始化(TDZ) | 提升但不初始化(TDZ) |
| 重复声明 | 允许 | 报错 | 报错 |
| 重新赋值 | 允许 | 允许 | 不允许(对象属性可修改) |
| 全局对象属性 | 是 | 否 | 否 |
| 暂时性死区 | 无 | 有 | 有 |
var 的底层机制
1. 函数作用域特性
var 只认函数边界,不认块级边界
function testVarScope() {
console.log(a); // undefined (变量提升)
if (true) {
var a = 10;
console.log(a); // 10
}
console.log(a); // 10 (if 块外仍可访问)
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出三个 3 (i 在函数作用域内只有一个)
}
原理解析:
// 引擎实际执行顺序(变量提升后)
function testVarScope() {
var a; // 提升到函数顶部,初始化为 undefined
var i; // 提升
console.log(a); // undefined
if (true) {
a = 10; // 赋值操作
console.log(a);
}
console.log(a);
for (i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
}
2. 变量提升(Hoisting)机制
var 的提升分为两个阶段:
console.log(typeof x); // undefined (不报错)
console.log(x); // undefined (已声明但未赋值)
var x = 5;
console.log(x); // 5
执行上下文创建阶段的处理:
// 创建阶段(Creation Phase)
ExecutionContext = {
VariableEnvironment: {
x: undefined // var 声明的变量被提升并初始化为 undefined
}
}
// 执行阶段(Execution Phase)
// console.log(x) -> 访问 VariableEnvironment.x -> undefined
// x = 5 -> 赋值操作
3. 全局对象污染
var globalVar = 'I am global';
console.log(window.globalVar); // 'I am global' (浏览器环境)
let blockVar = 'I am block';
console.log(window.blockVar); // undefined
原理:
var在全局作用域声明的变量会成为全局对象的属性let/const声明的变量存储在声明式环境记录中,不会污染全局对象
4. 重复声明不报错
var a = 1;
var a = 2; // 不报错,后者覆盖前者
console.log(a); // 2
// 引擎处理
// 第一次: var a; a = 1;
// 第二次: (忽略 var a,因为已存在) a = 2;
let/const 的革命性改变
1. 块级作用域的实现原理
词法环境(Lexical Environment)的创建
function testBlockScope() {
let x = 1;
if (true) {
let x = 2; // 新的词法环境
console.log(x); // 2
}
console.log(x); // 1
}
内部结构:
// 函数执行上下文
FunctionExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
x: 1 // 外层 let x
},
outer: GlobalLexicalEnvironment
}
}
// if 块执行时创建新的词法环境
BlockExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
x: 2 // 内层 let x
},
outer: FunctionExecutionContext.LexicalEnvironment // 指向外层
}
}
2. 经典的 for 循环问题
var 版本的问题:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出: 3, 3, 3
// 原因: 只有一个 i 变量
FunctionScope = {
i: 3 // 循环结束后的值
}
// 三个回调函数都引用同一个 i
let 版本的解决:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出: 0, 1, 2
// 原因: 每次迭代创建新的词法环境
Iteration0: { i: 0 }
Iteration1: { i: 1 }
Iteration2: { i: 2 }
// 每个回调函数捕获各自迭代的 i
详细机制:
// for (let i = 0; i < 3; i++) { ... }
// 等价于:
{
let i = 0;
{
// 迭代 0
let i_iteration = i; // 创建迭代专用绑定
setTimeout(() => console.log(i_iteration), 100);
i++;
}
{
// 迭代 1
let i_iteration = i;
setTimeout(() => console.log(i_iteration), 100);
i++;
}
{
// 迭代 2
let i_iteration = i;
setTimeout(() => console.log(i_iteration), 100);
i++;
}
}
3. const 的不可变性
const 限制的是绑定,不是值
// ✅ 基本类型不可变
const num = 10;
num = 20; // TypeError: Assignment to constant variable
// ✅ 对象属性可变
const obj = { value: 10 };
obj.value = 20; // 允许
obj.newProp = 30; // 允许
console.log(obj); // { value: 20, newProp: 30 }
// ❌ 重新赋值不允许
obj = {}; // TypeError
// ✅ 数组元素可变
const arr = [1, 2, 3];
arr.push(4); // 允许
arr[0] = 10; // 允许
console.log(arr); // [10, 2, 3, 4]
// ❌ 重新赋值不允许
arr = []; // TypeError
内存结构:
// const obj = { value: 10 };
Stack Memory:
obj -> 0x1234 (常量引用,不可改变)
Heap Memory:
0x1234: { value: 10 } (对象内容,可以改变)
// obj.value = 20;
// 改变的是堆内存中的内容,栈中的引用不变
// obj = {};
// 尝试改变栈中的引用,const 不允许
真正的不可变对象:
// 使用 Object.freeze()
const obj = Object.freeze({ value: 10 });
obj.value = 20; // 严格模式下报错,非严格模式静默失败
console.log(obj.value); // 10
// 深度冻结
function deepFreeze(obj) {
Object.freeze(obj);
Object.values(obj).forEach(val => {
if (typeof val === 'object' && val !== null) {
deepFreeze(val);
}
});
return obj;
}
const nested = deepFreeze({
a: 1,
b: { c: 2 }
});
nested.a = 10; // 无效
nested.b.c = 20; // 无效
暂时性死区(TDZ)深度剖析
1. TDZ 的定义与本质
暂时性死区(Temporal Dead Zone): 从块级作用域开始到变量声明语句之间的区域,在此期间访问变量会报错。
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 10;
// TDZ 范围:
{
// TDZ 开始
console.log(x); // ReferenceError
let x = 5; // TDZ 结束
console.log(x); // 5
}
2. TDZ 的底层原理
执行上下文的三个阶段:
// 示例代码
function test() {
console.log(a); // ReferenceError
let a = 10;
}
// 阶段 1: 创建阶段(Creation Phase)
ExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
a: <uninitialized> // let 变量被"提升"但未初始化
}
}
}
// 阶段 2: 执行阶段 - TDZ
// 访问 a -> 状态为 <uninitialized> -> ReferenceError
// 阶段 3: 执行阶段 - 声明后
// let a = 10 执行 -> a 被初始化为 10
ExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
a: 10 // 现在可以正常访问
}
}
}
三种变量状态:
// 1. undeclared (未声明)
console.log(noExist); // ReferenceError: noExist is not defined
// 2. uninitialized (未初始化 - TDZ)
console.log(inTDZ); // ReferenceError: Cannot access 'inTDZ' before initialization
let inTDZ;
// 3. initialized (已初始化)
let initialized = 10;
console.log(initialized); // 10
3. TDZ 的复杂场景
场景 1: 函数参数默认值
function test(a = b, b = 2) {
console.log(a, b);
}
test(); // ReferenceError: Cannot access 'b' before initialization
// 原因:
// 参数从左到右初始化
// 1. a = b (此时 b 还在 TDZ 中)
// 2. ReferenceError
正确写法:
function test(b = 2, a = b) {
console.log(a, b);
}
test(); // 2 2
// 执行顺序:
// 1. b = 2 (b 初始化完成)
// 2. a = b (b 已经可以访问)
场景 2: typeof 操作符
// var 时代的安全检测
console.log(typeof undeclaredVar); // "undefined" (不报错)
// let/const 时代的 TDZ 陷阱
console.log(typeof x); // ReferenceError: Cannot access 'x' before initialization
let x = 10;
// 原因:
// typeof 在 TDZ 中访问 let/const 变量会报错
// 但访问完全未声明的变量返回 "undefined"
场景 3: 闭包中的 TDZ
function outer() {
return function inner() {
console.log(x); // ReferenceError
};
let x = 10;
}
outer()();
// 分析:
// inner 函数创建时,x 已经在外层作用域的环境记录中
// 但 inner 执行时,x 仍在 TDZ 中(声明语句还未执行)
场景 4: 类中的 TDZ
class Example {
// 静态字段
static field1 = console.log(this.field2); // undefined
static field2 = 10;
// 实例字段
instanceField1 = this.method(); // ReferenceError
method() {
return this.instanceField2;
}
instanceField2 = 20;
}
// 原因:
// 静态字段按顺序初始化,field2 在 field1 之后
// 实例字段初始化时,后面的字段还在 TDZ 中
4. TDZ 的实际意义
1. 防止变量提升带来的问题
// var 时代的隐蔽 bug
function getPrice(quantity) {
if (quantity > 0) {
var discount = 0.1;
}
return quantity * (1 - discount); // discount 可能是 undefined
}
console.log(getPrice(5)); // NaN
// let/const 时代的明确错误
function getPrice(quantity) {
if (quantity > 0) {
let discount = 0.1;
}
return quantity * (1 - discount); // ReferenceError: discount is not defined
}
2. 保证代码执行的确定性
let x = x + 1; // ReferenceError: Cannot access 'x' before initialization
// 明确阻止自引用初始化
// 而 var 会导致:
var y = y + 1; // y = undefined + 1 = NaN
执行上下文与变量环境
1. 执行上下文的组成
ExecutionContext = {
// 词法环境(let/const 变量存储在这里)
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// let/const 声明
},
outer: <reference to parent lexical environment>
},
// 变量环境(var 变量存储在这里)
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// var 声明
},
outer: <reference to parent lexical environment>
},
// this 绑定
ThisBinding: <value>
}
2. 完整的变量查找过程
let global = 'global';
function outer() {
let outerVar = 'outer';
function inner() {
let innerVar = 'inner';
console.log(innerVar); // 查找过程演示
console.log(outerVar);
console.log(global);
}
inner();
}
outer();
查找链:
// 1. 查找 innerVar
InnerExecutionContext.LexicalEnvironment
-> EnvironmentRecord.innerVar ✓ 找到
// 2. 查找 outerVar
InnerExecutionContext.LexicalEnvironment
-> EnvironmentRecord.outerVar ✗ 未找到
-> outer (外层引用)
-> OuterExecutionContext.LexicalEnvironment
-> EnvironmentRecord.outerVar ✓ 找到
// 3. 查找 global
InnerExecutionContext.LexicalEnvironment
-> ... (向外层查找)
-> GlobalExecutionContext.LexicalEnvironment
-> EnvironmentRecord.global ✓ 找到
3. var 与 let/const 的环境差异
var varVariable = 'var';
let letVariable = 'let';
function test() {
var varInFunction = 'var';
let letInFunction = 'let';
}
// 全局执行上下文
GlobalExecutionContext = {
VariableEnvironment: {
EnvironmentRecord: {
varVariable: 'var'
}
},
LexicalEnvironment: {
EnvironmentRecord: {
letVariable: 'let',
test: <function>
}
}
}
// 函数执行上下文
FunctionExecutionContext = {
VariableEnvironment: {
EnvironmentRecord: {
varInFunction: 'var'
}
},
LexicalEnvironment: {
EnvironmentRecord: {
letInFunction: 'let'
}
}
}
作用域与作用域链
1. 词法作用域(静态作用域)
JavaScript 采用词法作用域,在函数定义时确定,不是运行时确定
let value = 1;
function foo() {
console.log(value);
}
function bar() {
let value = 2;
foo(); // 输出 1,不是 2
}
bar();
// 原因:
// foo 函数在全局作用域定义
// 其外层作用域(outer reference)指向全局作用域
// 与在哪里调用无关
2. 作用域链的形成
let a = 1;
function level1() {
let b = 2;
function level2() {
let c = 3;
function level3() {
let d = 4;
console.log(a, b, c, d); // 1 2 3 4
}
level3();
}
level2();
}
level1();
作用域链图示:
level3 作用域
├─ d: 4
└─ outer -> level2 作用域
├─ c: 3
└─ outer -> level1 作用域
├─ b: 2
└─ outer -> 全局作用域
└─ a: 1
3. 闭包与作用域链
function createCounter() {
let count = 0;
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
内存结构:
// createCounter 执行后
Heap: {
ClosureScope: {
count: 0 // 被闭包捕获,不会被垃圾回收
}
}
// 返回的对象
counter = {
increment: <function>, // [[Scope]] -> ClosureScope
decrement: <function>, // [[Scope]] -> ClosureScope
getCount: <function> // [[Scope]] -> ClosureScope
}
// 三个方法共享同一个 ClosureScope
实战场景与最佳实践
1. 循环中的异步操作
问题场景:
// ❌ 常见错误
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}
// 输出: 5 5 5 5 5 (每秒一个)
解决方案对比:
// ✅ 方案 1: 使用 let
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}
// 输出: 0 1 2 3 4 (每秒一个)
// ✅ 方案 2: IIFE 闭包
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(() => {
console.log(j);
}, j * 1000);
})(i);
}
// ✅ 方案 3: 函数参数
for (var i = 0; i < 5; i++) {
setTimeout((j) => {
console.log(j);
}, i * 1000, i);
}
// ✅ 方案 4: bind 方法
for (var i = 0; i < 5; i++) {
setTimeout(console.log.bind(null, i), i * 1000);
}
2. 模块模式的演进
var 时代的模块模式:
var MyModule = (function() {
var privateVar = 'private';
function privateMethod() {
return privateVar;
}
return {
publicMethod: function() {
return privateMethod();
}
};
})();
let/const 时代的模块模式:
// ES6 模块
// module.js
const privateVar = 'private';
function privateMethod() {
return privateVar;
}
export function publicMethod() {
return privateMethod();
}
// 或使用块级作用域
{
const moduleData = {
privateVar: 'private'
};
window.MyModule = {
publicMethod() {
return moduleData.privateVar;
}
};
}
3. 条件声明的最佳实践
// ❌ 避免条件声明 let/const
if (condition) {
let x = 1; // 只在 if 块内有效
}
console.log(x); // ReferenceError
// ✅ 正确做法
let x;
if (condition) {
x = 1;
} else {
x = 2;
}
console.log(x); // 安全访问
// ✅ 或使用三元表达式
const x = condition ? 1 : 2;
4. 循环中的变量声明位置
// ❌ 性能较差(每次迭代都声明)
for (let i = 0; i < 1000; i++) {
let temp = i * 2; // 每次迭代创建新变量
console.log(temp);
}
// ✅ 优化后
let temp;
for (let i = 0; i < 1000; i++) {
temp = i * 2; // 复用变量
console.log(temp);
}
// 注意: 现代 JS 引擎会优化这种情况,差异很小
// 但在大规模数据处理时值得注意
5. const 的最佳实践
// ✅ 优先使用 const
const MAX_USERS = 100;
const API_URL = 'https://api.example.com';
// ✅ 对象和数组也用 const
const config = {
timeout: 3000,
retries: 3
};
const items = [1, 2, 3];
items.push(4); // 允许修改
// ✅ 需要真正不可变时
const IMMUTABLE_CONFIG = Object.freeze({
apiKey: 'xxx',
apiSecret: 'yyy'
});
// ❌ 避免过度使用 let
let unnecessary = 10; // 如果不会重新赋值,应该用 const
// ✅ 只在需要重新赋值时用 let
let counter = 0;
counter++;
面试高频问题精讲
Q1: var、let、const 的区别是什么?
标准答案框架:
// 1. 作用域
{
var a = 1; // 函数作用域
let b = 2; // 块级作用域
const c = 3; // 块级作用域
}
console.log(a); // 1 (可访问)
console.log(b); // ReferenceError
console.log(c); // ReferenceError
// 2. 变量提升
console.log(x); // undefined
var x = 1;
console.log(y); // ReferenceError
let y = 2;
// 3. 重复声明
var m = 1;
var m = 2; // 允许
let n = 1;
let n = 2; // SyntaxError
// 4. 全局对象属性
var globalVar = 1;
console.log(window.globalVar); // 1
let globalLet = 2;
console.log(window.globalLet); // undefined
// 5. 可变性
let changeable = 1;
changeable = 2; // 允许
const immutable = 1;
immutable = 2; // TypeError
深入点:
- var 的变量提升会导致变量在声明前就可以访问(值为 undefined)
- let/const 也会提升,但存在暂时性死区,访问会报错
- const 保证的是栈内存中的值不变,对于引用类型,堆内存的内容可以改变
Q2: 什么是暂时性死区(TDZ)?为什么需要它?
概念解释:
// TDZ 示例
{
// TDZ 开始
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 10; // TDZ 结束
console.log(x); // 10
}
为什么需要 TDZ:
// 1. 防止变量提升带来的问题
function getValue(condition) {
if (condition) {
return value; // 希望报错,而不是返回 undefined
}
let value = 10;
}
// 2. 保证初始化顺序
let x = x + 1; // ReferenceError (而不是 NaN)
// 3. 配合 const 的语义
const PI = 3.14159;
// 如果没有 TDZ,PI 在声明前可能被访问,违背 const 的不可变语义
Q3: 如何理解”let/const 也会提升”?
关键点:
// let/const 确实会提升
let x = 'outer';
function test() {
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 'inner';
}
test();
// 如果 let x 不提升,console.log(x) 应该访问外层的 'outer'
// 但实际报错,说明内层的 let x 已经"提升"了,只是在 TDZ 中
提升的本质:
// 创建执行上下文时
ExecutionContext = {
LexicalEnvironment: {
EnvironmentRecord: {
x: <uninitialized> // 已提升,但状态是 uninitialized
}
}
}
// var 的提升
VariableEnvironment: {
EnvironmentRecord: {
y: undefined // 已提升,且初始化为 undefined
}
}
Q4: 为什么 for 循环中 let 和 var 表现不同?
详细解析:
// var 版本
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出: 3 3 3
// 等价于:
var i;
for (i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 只有一个 i,所有回调都引用这个 i
// 循环结束时 i = 3
// let 版本
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出: 0 1 2
// 等价于:
{
let i = 0;
if (i < 3) {
let _i = i; // 每次迭代创建新的绑定
setTimeout(() => console.log(_i), 100);
i++;
}
if (i < 3) {
let _i = i;
setTimeout(() => console.log(_i), 100);
i++;
}
if (i < 3) {
let _i = i;
setTimeout(() => console.log(_i), 100);
i++;
}
}
规范说明:
- for 循环的 let 声明会在每次迭代创建新的词法环境
- 每个迭代的循环变量都是独立的
- 这是规范特别为 let/const 在循环中设计的行为
Q5: const 声明的对象为什么可以修改属性?
内存模型解释:
const obj = { value: 1 };
// 内存结构:
// Stack(栈):
// obj -> 0x1234 (内存地址,const 锁定的是这个地址)
//
// Heap(堆):
// 0x1234: { value: 1 } (对象内容,没有被 const 锁定)
obj.value = 2; // ✅ 修改堆内存中的内容
console.log(obj); // { value: 2 }
obj = {}; // ❌ 尝试改变栈中的地址引用
// TypeError: Assignment to constant variable
如何真正不可变:
// 1. 浅层冻结
const obj = Object.freeze({ value: 1 });
obj.value = 2; // 严格模式报错,非严格模式静默失败
// 2. 深层冻结
function deepFreeze(obj) {
Object.freeze(obj);
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'object' && obj[key] !== null) {
deepFreeze(obj[key]);
}
});
return obj;
}
const nested = deepFreeze({
a: 1,
b: { c: 2 }
});
// 3. 使用不可变数据库(如 Immutable.js)
import { Map } from 'immutable';
const immutableMap = Map({ value: 1 });
const newMap = immutableMap.set('value', 2); // 返回新对象
console.log(immutableMap.get('value')); // 1
console.log(newMap.get('value')); // 2
Q6: 说说作用域和作用域链
作用域定义:
// 作用域: 变量可访问的范围
// 1. 全局作用域
let globalVar = 'global';
// 2. 函数作用域
function func() {
var functionVar = 'function';
}
// 3. 块级作用域
{
let blockVar = 'block';
}
// 4. 模块作用域
// module.js
const moduleVar = 'module';
export { moduleVar };
作用域链:
let level0 = 'global';
function level1() {
let level1Var = 'level1';
function level2() {
let level2Var = 'level2';
function level3() {
let level3Var = 'level3';
// 作用域链查找顺序:
console.log(level3Var); // 1. 当前作用域
console.log(level2Var); // 2. level2 作用域
console.log(level1Var); // 3. level1 作用域
console.log(level0); // 4. 全局作用域
}
level3();
}
level2();
}
level1();
查找机制:
function outer() {
let x = 1;
function inner() {
console.log(x); // 如何查找 x?
}
inner();
}
// 1. inner 函数的 [[Scope]] 属性在定义时确定
inner.[[Scope]] = [
innerLexicalEnvironment,
outerLexicalEnvironment,
globalLexicalEnvironment
]
// 2. 执行 console.log(x) 时
// 沿着作用域链查找:
// innerLexicalEnvironment.x -> 未找到
// outerLexicalEnvironment.x -> 找到,值为 1
Q7: 如何解释闭包?
闭包定义: 函数能够访问其词法作用域外的变量,即使该函数在其词法作用域外执行。
function createCounter() {
let count = 0; // 被闭包捕获
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
// createCounter 执行完毕后,正常情况下 count 应该被回收
// 但由于返回的函数持有对 count 的引用
// count 被保存在闭包中,不会被垃圾回收
闭包的内存模型:
function outer() {
let outerVar = 'outer';
let unused = 'unused'; // 不会被捕获
return function inner() {
console.log(outerVar); // 使用了 outerVar
};
}
const fn = outer();
// 闭包内容(只包含被使用的变量):
Closure(outer) = {
outerVar: 'outer'
// unused 不在闭包中
}
经典闭包问题:
// ❌ 问题代码
function createFunctions() {
var result = [];
for (var i = 0; i < 3; i++) {
result[i] = function() {
return i;
};
}
return result;
}
const funcs = createFunctions();
console.log(funcs[0]()); // 3
console.log(funcs[1]()); // 3
console.log(funcs[2]()); // 3
// ✅ 解决方案 1: let
function createFunctions() {
var result = [];
for (let i = 0; i < 3; i++) {
result[i] = function() {
return i;
};
}
return result;
}
// ✅ 解决方案 2: IIFE
function createFunctions() {
var result = [];
for (var i = 0; i < 3; i++) {
result[i] = (function(j) {
return function() {
return j;
};
})(i);
}
return result;
}
Q8: 实际项目中如何选择 var/let/const?
最佳实践:
// ❌ 避免使用 var
var oldStyle = 'avoid';
// ✅ 默认使用 const
const API_KEY = 'xxx';
const config = { timeout: 3000 };
const users = [];
// ✅ 只在需要重新赋值时使用 let
let count = 0;
count++;
let result = null;
if (condition) {
result = getValue();
}
// ✅ 循环计数器
for (let i = 0; i < 10; i++) {
// ...
}
// ✅ 块级作用域中的临时变量
{
let temp = processData();
useTemp(temp);
}
// temp 在这里不可访问,避免污染
团队规范建议:
// ESLint 配置
{
"rules": {
"no-var": "error", // 禁止使用 var
"prefer-const": "error", // 优先使用 const
"no-const-assign": "error" // 禁止修改 const 变量
}
}
// 优先级:
// const > let > var (永远不用)
总结与记忆要点
核心记忆点
1. 三者本质差异
var: 函数作用域 + 提升并初始化为 undefined + 允许重复声明
let: 块级作用域 + 提升但不初始化(TDZ) + 禁止重复声明
const: 块级作用域 + 提升但不初始化(TDZ) + 禁止重复声明 + 禁止重新赋值
2. 变量提升机制
// var: 提升 + 初始化
var x; // undefined
// let/const: 提升 + 不初始化
let y; // <uninitialized> -> TDZ
3. TDZ 的本质
从作用域开始到声明语句之间的区域,变量处于 <uninitialized> 状态。
4. for 循环特殊性 let/const 在 for 循环中每次迭代创建新的词法环境。
5. const 的不可变 锁定的是栈中的引用,不是堆中的值。
实战练习题
练习 1: 预测输出
console.log(a); // ?
var a = 1;
console.log(b); // ?
let b = 2;
{
console.log(c); // ?
let c = 3;
}
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// ?
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 0);
}
// ?
答案
console.log(a); // undefined
var a = 1;
console.log(b); // ReferenceError
let b = 2;
{
console.log(c); // ReferenceError (TDZ)
let c = 3;
}
// 输出: 3 3 3
// 输出: 0 1 2
练习 2: 找出错误
const obj = { value: 1 };
obj.value = 2; // ?
obj = { value: 3 }; // ?
let x = 10;
let x = 20; // ?
function test(a = b, b = 2) {
console.log(a, b);
}
test(); // ?
typeof undeclared; // ?
typeof uninitialized; // ?
let uninitialized;
答案
const obj = { value: 1 };
obj.value = 2; // ✅ 允许
obj = { value: 3 }; // ❌ TypeError
let x = 10;
let x = 20; // ❌ SyntaxError
function test(a = b, b = 2) {
console.log(a, b);
}
test(); // ❌ ReferenceError (b 在 TDZ 中)
typeof undeclared; // ✅ "undefined"
typeof uninitialized; // ❌ ReferenceError (TDZ)
let uninitialized;
练习 3: 实现题
实现一个函数,创建多个计数器,每个计数器独立计数:
function createCounters(n) {
// 实现代码
}
const counters = createCounters(3);
console.log(counters[0]()); // 1
console.log(counters[0]()); // 2
console.log(counters[1]()); // 1
console.log(counters[2]()); // 1
console.log(counters[0]()); // 3
答案
function createCounters(n) {
const result = [];
for (let i = 0; i < n; i++) {
let count = 0; // 每个计数器独立的 count
result.push(() => ++count);
}
return result;
}
// 或使用闭包
function createCounters(n) {
return Array.from({ length: n }, () => {
let count = 0;
return () => ++count;
});
}
延伸阅读
-
ECMAScript 规范
-
深入文章
-
V8 引擎实现
- V8 如何实现 TDZ
- 执行上下文的创建过程
文档版本: v1.0
最后更新: 2024
适用于: ES6+ JavaScript 开发者
祝你在面试和工作中游刃有余! 🚀