未找到匹配的笔记

2.2 - let/const/var区别、暂时性死区

JavaScript面试

JavaScript 变量声明深度解析:let/const/var 与暂时性死区

深入理解变量声明的底层原理,从执行上下文到作用域链的完整解析

目录


核心概念速览

三者对比表

特性varletconst
作用域函数作用域块级作用域块级作用域
变量提升提升并初始化为 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;
  });
}

延伸阅读

  1. ECMAScript 规范

  2. 深入文章

  3. V8 引擎实现

    • V8 如何实现 TDZ
    • 执行上下文的创建过程

文档版本: v1.0
最后更新: 2024
适用于: ES6+ JavaScript 开发者


祝你在面试和工作中游刃有余! 🚀