Skip to content

for (let ...) 的特殊处理:每次迭代创建独立词法环境

理解 for 循环中的作用域问题

在 JavaScript 中,for 循环的作用域处理是一个重要的概念,特别是在使用 let 声明循环变量时。ES6 对 for (let ...) 循环进行了特殊处理,每次迭代都会创建独立的词法环境,这解决了经典闭包问题。

传统 var 声明的问题

javascript
// 使用 var 声明循环变量的问题
var funcs = [];
for (var i = 0; i < 3; i++) {
  funcs[i] = function() {
    return i; // 所有函数都引用同一个 i 变量
  };
}

console.log(funcs[0]()); // 3
console.log(funcs[1]()); // 3
console.log(funcs[2]()); // 3

使用 let 声明的优势

javascript
// 使用 let 声明循环变量的优势
var funcs = [];
for (let i = 0; i < 3; i++) {
  funcs[i] = function() {
    return i; // 每个函数都捕获各自迭代中的 i 值
  };
}

console.log(funcs[0]()); // 0
console.log(funcs[1]()); // 1
console.log(funcs[2]()); // 2

for (let ...) 的词法环境机制

ES6 对 for (let ...) 循环进行了特殊处理,每次迭代都会创建一个新的词法环境,这个环境包含循环变量的绑定。

每次迭代创建新环境

javascript
// 每次迭代都创建新的词法环境
for (let i = 0; i < 3; i++) {
  // 第1次迭代: 词法环境1 { i: 0 }
  // 第2次迭代: 词法环境2 { i: 1 }
  // 第3次迭代: 词法环境3 { i: 2 }
  setTimeout(() => {
    console.log(i); // 0, 1, 2
  }, 100);
}

与普通块级作用域的对比

javascript
// 普通块级作用域
{
  let x = 1;
  {
    let x = 2;
    console.log(x); // 2
  }
  console.log(x); // 1
}

// for 循环的特殊处理
for (let i = 0; i < 3; i++) {
  // 每次迭代的 i 都是独立的绑定
  setTimeout(() => {
    console.log(i); // 0, 1, 2
  }, 100);
}

异步操作中的应用

setTimeout 示例

javascript
// 经典问题的解决方案
for (let i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(`数字: ${i}`); // 输出 0, 1, 2, 3, 4
  }, 100);
}

Promise 示例

javascript
// 在 Promise 中使用
function createPromises() {
  const promises = [];
  
  for (let i = 0; i < 3; i++) {
    promises.push(
      new Promise(resolve => {
        setTimeout(() => {
          resolve(i); // 每个 Promise 解析为不同的值
        }, 100);
      })
    );
  }
  
  return Promise.all(promises);
}

createPromises().then(results => {
  console.log(results); // [0, 1, 2]
});

事件监听器示例

javascript
// 为多个元素添加事件监听器
const buttons = document.querySelectorAll('.button');

for (let i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener('click', function() {
    console.log(`按钮 ${i} 被点击`); // 每个按钮都知道自己的索引
  });
}

for...in 和 for...of 循环中的处理

for...in 循环

javascript
const obj = { a: 1, b: 2, c: 3 };
const callbacks = [];

for (let key in obj) {
  callbacks.push(() => {
    return `${key}: ${obj[key]}`; // 每个回调捕获不同的 key
  });
}

console.log(callbacks[0]()); // "a: 1"
console.log(callbacks[1]()); // "b: 2"
console.log(callbacks[2]()); // "c: 3"

for...of 循环

javascript
const array = ['apple', 'banana', 'orange'];
const callbacks = [];

for (let value of array) {
  callbacks.push(() => {
    return value; // 每个回调捕获不同的 value
  });
}

console.log(callbacks[0]()); // "apple"
console.log(callbacks[1]()); // "banana"
console.log(callbacks[2]()); // "orange"

与其他循环形式的对比

for (var ...) vs for (let ...)

javascript
// 使用 var 的问题
console.log("使用 var:");
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 输出三次 3
  }, 100);
}

// 使用 let 的解决方案
console.log("使用 let:");
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 输出 0, 1, 2
  }, 100);
}

传统解决方案(IIFE)

javascript
// 在 ES6 之前使用 IIFE 解决闭包问题
for (var i = 0; i < 3; i++) {
  (function(index) {
    setTimeout(() => {
      console.log(index); // 输出 0, 1, 2
    }, 100);
  })(i);
}

复杂场景示例

嵌套循环

javascript
// 嵌套 for (let ...) 循环
for (let i = 0; i < 2; i++) {
  for (let j = 0; j < 3; j++) {
    setTimeout(() => {
      console.log(`i: ${i}, j: ${j}`); // 每个组合都有独立的 i 和 j
    }, 100);
  }
}
// 输出:
// i: 0, j: 0
// i: 0, j: 1
// i: 0, j: 2
// i: 1, j: 0
// i: 1, j: 1
// i: 1, j: 2

结合异步函数

javascript
// 在异步函数中使用
async function processItems() {
  const items = [1, 2, 3, 4, 5];
  
  for (let i = 0; i < items.length; i++) {
    // 每次迭代都等待异步操作完成
    await new Promise(resolve => {
      setTimeout(() => {
        console.log(`处理项目 ${items[i]}`); // 每个项目都被正确处理
        resolve();
      }, 100);
    });
  }
}

processItems();
// 输出:
// 处理项目 1
// 处理项目 2
// 处理项目 3
// 处理项目 4
// 处理项目 5

性能考虑

虽然每次迭代创建独立词法环境会有一些性能开销,但在大多数实际应用中,这种开销是微不足道的,而带来的代码清晰度和正确性提升是值得的。

javascript
// 性能对比示例
console.time('var-loop');
for (var i = 0; i < 1000000; i++) {
  // 空循环
}
console.timeEnd('var-loop');

console.time('let-loop');
for (let i = 0; i < 1000000; i++) {
  // 空循环
}
console.timeEnd('let-loop');

最佳实践

1. 优先使用 let 而不是 var

javascript
// 推荐
for (let i = 0; i < items.length; i++) {
  // 处理 items[i]
}

// 不推荐
for (var i = 0; i < items.length; i++) {
  // 处理 items[i]
}

2. 在需要闭包的场景中特别注意

javascript
// 创建多个事件监听器时使用 let
const elements = document.querySelectorAll('.item');
for (let i = 0; i < elements.length; i++) {
  elements[i].addEventListener('click', function() {
    console.log(`第 ${i} 个元素被点击`);
  });
}

总结

ES6 对 for (let ...) 循环的特殊处理是 JavaScript 语言的一个重要改进。通过在每次迭代中创建独立的词法环境,解决了长期存在的闭包问题,使开发者能够编写更清晰、更可预测的代码。理解这一机制对于编写高质量的 JavaScript 代码至关重要。