# JavaScript 变量作用域深度解析:var、let 与闭包陷阱

40 阅读3分钟

JavaScript 变量作用域深度解析:var、let 与闭包陷阱

前言

在 JavaScript 开发中,变量作用域是一个基础但容易混淆的概念。今天我们就来深入探讨 varlet 的作用域差异,以及那个经典的 for 循环闭包问题。

基础作用域对比

var 的函数作用域

javascript

function checkScope() {
  var i = 'function scope';
  if (true) {
    i = 'block scope';  // 修改的是同一个变量
    console.log('Block scope i is: ', i);  // block scope
  }
  console.log('Function scope i is: ', i);  // block scope
  return i;
}

let 的块级作用域

javascript

function checkScope() {
  let i = 'function scope';
  if (true) {
    let i = 'block scope';  // 不同的变量,只在块内有效
    console.log('Block scope i is: ', i);  // block scope
  }
  console.log('Function scope i is: ', i);  // function scope
  return i;
}

关键区别

  • var 只有函数作用域,没有块级作用域
  • let 有块级作用域,{} 内声明的变量外部无法访问

变量提升的差异

var 的变量提升

javascript

console.log(a);  // undefined,不会报错
var a = 5;
console.log(a);  // 5

// 实际执行顺序:
var a;
console.log(a);  // undefined
a = 5;
console.log(a);  // 5

let 的暂时性死区

javascript

console.log(b);  // ReferenceError: Cannot access 'b' before initialization
let b = 5;

经典闭包问题解析

问题重现

javascript

// 使用 var - 出现意外结果
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);  // 输出: 3, 3, 3
}

// 使用 let - 符合预期
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);  // 输出: 0, 1, 2
}

为什么会出现这种情况?

var 的执行机制

javascript

// 实际执行过程:
var i;  // 提升到外部作用域

for (i = 0; i < 3; i++) {
  // 创建闭包,但都引用同一个 i
  setTimeout(function() {
    console.log(i);  // 都指向同一个 i
  }, 100);
}
// 循环结束时 i = 3

关键点

  1. 整个循环只有一个 i 变量
  2. 循环快速执行完毕,i 最终值为 3
  3. 100ms 后所有回调执行,都读取同一个 i = 3
let 的执行机制

javascript

// 实际执行过程:
// 第一次迭代
{
  let i = 0;
  setTimeout(() => console.log(i), 100);  // 捕获 i=0
}

// 第二次迭代  
{
  let i = 1;
  setTimeout(() => console.log(i), 100);  // 捕获 i=1
}

// 第三次迭代
{
  let i = 2;
  setTimeout(() => console.log(i), 100);  // 捕获 i=2
}

关键点

  1. 每次迭代创建新的块级作用域
  2. 每个 setTimeout 回调捕获各自迭代中的 i
  3. 每个回调都有独立的变量引用

历史解决方案(ES6 之前)

在 let 出现之前,我们使用 IIFE(立即执行函数表达式)来解决:

javascript

// 使用 IIFE 创建闭包
for (var i = 0; i < 3; i++) {
  (function(index) {
    setTimeout(() => console.log(index), 100);
  })(i);  // 立即传入当前的 i 值
}
// 输出: 0, 1, 2

// 或者使用函数参数
for (var i = 0; i < 3; i++) {
  setTimeout((function(index) {
    return function() {
      console.log(index);
    };
  })(i), 100);
}

实际开发建议

1. 优先使用 const 和 let

javascript

// 推荐
const PI = 3.14;
let count = 0;

// 避免
var globalVar = 'old style';

2. 理解闭包的本质

javascript

function createCounter() {
  let count = 0;  // 闭包捕获的变量
  return {
    increment: () => ++count,
    getValue: () => count
  };
}

const counter = createCounter();
console.log(counter.increment());  // 1
console.log(counter.increment());  // 2

3. 注意循环中的异步操作

javascript

// 正确做法
for (let i = 0; i < elements.length; i++) {
  elements[i].addEventListener('click', () => {
    console.log(`Clicked element ${i}`);
  });
}

// 或者使用 forEach
elements.forEach((element, index) => {
  element.addEventListener('click', () => {
    console.log(`Clicked element ${index}`);
  });
});

总结

特性varletconst
作用域函数作用域块级作用域块级作用域
变量提升否(暂时性死区)否(暂时性死区)
重复声明允许不允许不允许
重新赋值允许允许不允许

最佳实践

  • 默认使用 const
  • 需要重新赋值时使用 let
  • 避免使用 var
  • 理解闭包和作用域链
  • 注意循环中的异步操作

掌握这些概念可以帮助我们写出更 predictable 的代码,避免常见的陷阱。希望这篇笔记对你有所帮助!