这段代码是 JavaScript 面试中非常经典的一道题,它主要考察的是闭包(Closure)以及 var 关键字的变量作用域特性。
为什么最后打印出来的是 10 而不是 6?我们可以从以下两个核心点来理解:
1. var 没有块级作用域
在 ES6 引入 let 和 const 之前,JavaScript 只有全局作用域和函数作用域。var 声明的变量是没有“块级作用域”的。
也就是说,虽然 var i = 0 写在了 for 循环的大括号 {} 里面,但变量 i 实际上是被提升到了包含这个循环的整个函数作用域(或全局作用域)中。
2. 闭包保存的是变量的引用
当你在循环里执行 a[i] = function () { console.log(i); }; 时,你并没有把当前 i 的值存进去,而是创建了一个个函数,这些函数内部都引用了同一个外部变量 i。
整个代码的执行过程是这样的:
- 循环阶段:
for循环飞快地跑完了 10 次。在这个过程中,数组a被填满了 10 个一模一样的函数。此时,外部的变量i也从 0 一路加到了 10(当i等于 10 时,不满足i < 10,循环结束)。 - 调用阶段: 当你执行
a[6]()时,第 7 个位置的函数才开始真正运行。它去查找自己引用的变量i。 - 结果: 因为它引用的是那个唯一的、外部的
i,而此时的i早就在循环结束后变成了10,所以控制台最终打印出了10。其实无论你调用a[0]()还是a[9](),结果全都是 10。
💡 如何让它打印出预期的 6?
如果你希望每个函数都能记住自己那一次循环时的 i 值,通常有两种经典的解决办法:
方法一:使用 ES6 的 let(推荐)
let 具有块级作用域。每次循环,JS 引擎都会为当前的循环块创建一个新的 i 绑定。
var a = [];
for (let i = 0; i < 10; i++) { // 把 var 换成 let
a[i] = function () {
console.log(i);
};
}
a[6](); // 输出 6
方法二:使用立即执行函数(IIFE)创造独立作用域
在 ES5 时代,我们通常通过传入参数的方式,把当前的 i 值“锁”在一个新的函数作用域里。
var a = [];
for (var i = 0; i < 10; i++) {
(function(j) { // j 接收当前的 i 值
a[j] = function () {
console.log(j); // 这里的 j 是独立的
};
})(i);
}
a[6](); // 输出 6
那除了这些,var 跟 let 还有哪些区别呢?
除了之前提到的作用域差异(var 是函数作用域,let 是块级作用域),它们在 JavaScript 中还有以下几个非常核心的区别:
🔄 变量提升与暂时性死区 (TDZ)
-
var: 存在“变量提升”现象。
var声明的变量会被提升到当前作用域的顶部,并且初始化为undefined。这意味着你可以在声明之前就使用它,不会报错,只是值为undefined。console.log(a); // 输出 undefined var a = 10; -
let: 虽然底层也有提升,但在代码执行到声明语句之前,该变量处于暂时性死区(Temporal Dead Zone, TDZ) 。如果你在声明之前访问它,JS 引擎会直接抛出
ReferenceError错误。console.log(b); // 报错:ReferenceError: Cannot access 'b' before initialization let b = 10;
🚫 重复声明
-
var: 允许在同一个作用域内多次声明同一个变量,后面的声明会覆盖前面的,或者被直接忽略,不会报错。
var x = 1; var x = 2; // 正常执行,x 变成了 2 -
let: 严禁在同一作用域内重复声明同一个变量,否则会抛出
SyntaxError语法错误。这能有效避免很多因手误或逻辑混乱导致的变量覆盖 Bug。let y = 1; let y = 2; // 报错:SyntaxError: Identifier 'y' has already been declared
🌍 全局对象属性
-
var: 在全局作用域下使用
var声明的变量,会自动成为全局对象(在浏览器中是window)的一个属性。var globalVar = "hello"; console.log(window.globalVar); // 输出 "hello" -
let: 在全局作用域下使用
let声明的变量,不会挂载到全局对象上。这使得let更加独立,不会污染全局命名空间。let globalLet = "world"; console.log(window.globalLet); // 输出 undefined
🔁 循环中的表现差异
这也是闭包面试题中最常考察的点之一。
-
var: 在
for循环中,所有迭代共享的是同一个变量。for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // 最终输出:3, 3, 3 (因为定时器触发时,i 已经变成了 3) -
let: 在
for循环中,JS 引擎会为每一次迭代都创建一个新的词法环境(绑定一个新的i)。因此每个异步回调都能捕获到当次循环正确的值。for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // 最终输出:0, 1, 2
为了让你更直观地对比,可以参考下面的总结表格:
| 特性 | var | let |
|---|---|---|
| 作用域 | 函数作用域 | 块级作用域 |
| 变量提升 | 提升并初始化为 undefined | 提升但不初始化(存在暂时性死区 TDZ) |
| 重复声明 | ✅ 允许 | ❌ 不允许 |
| 全局属性 | 会成为 window 对象的属性 | 不会成为 window 对象的属性 |
| 循环表现 | 整个循环共享同一个变量 | 每次迭代都会创建新的变量绑定 |
在现代前端开发中,为了避免 var 带来的各种隐式 Bug(如变量泄露、意外覆盖等),最佳实践通常是:默认优先使用 const,需要重新赋值时使用 let,尽量完全抛弃 var。
那介绍了var 跟let的区别了 let与const的区别呢?
const 和 let 其实非常相似,它们在作用域(都是块级作用域)、变量提升(都有暂时性死区 TDZ)、重复声明(都不允许)以及全局对象挂载(都不会挂载到 window)这四个方面是完全一致的。
它们最核心、也是唯一的本质区别在于:赋值与可变性。
🔒 核心区别:赋值与可变性
-
let: 声明的是变量。你可以先声明不赋值,也可以在后续的代码中随时重新赋值。
let a = 10; a = 20; // ✅ 合法,可以重新赋值 let b; b = 30; // ✅ 合法,可以先声明后赋值 -
const: 声明的是常量(更准确地说,是“只读的变量绑定”)。它有两个强制要求:
- 必须在声明的同时进行初始化赋值,不能留到后面。
- 一旦赋值,其绑定的内存地址(引用)就绝对不能改变,否则会直接报错。
const c = 10; c = 20; // ❌ 报错:TypeError: Assignment to constant variable. const d; d = 30; // ❌ 报错:SyntaxError: Missing initializer in const declaration
⚠️ 必须注意的“不可变”误区
很多初学者会误以为 const 声明的值就完全不能动了。这是一个巨大的误区!
const 保证的仅仅是变量的引用(内存地址)不变。如果 const 声明的是一个**对象(Object)或数组(Array)**等引用类型,你依然可以自由地修改这个对象内部的属性或数组的元素。
// 针对对象
const person = { name: "小明" };
person.name = "小红"; // ✅ 合法!修改对象内部属性是被允许的
console.log(person.name); // 输出: "小红"
person = { name: "小刚" }; // ❌ 报错!试图改变 person 指向的引用(内存地址)
// 针对数组
const arr = ;
arr.push(4); // ✅ 合法!修改数组内部元素是被允许的
console.log(arr); // 输出:
arr = ; // ❌ 报错!试图改变 arr 指向的引用
如果你希望连对象或数组的内部内容也彻底锁死、完全不可变,需要配合使用 Object.freeze() 方法。
📊 let 与 const 极简对比表
| 特性 | let | const |
|---|---|---|
| 作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 存在暂时性死区 (TDZ) | 存在暂时性死区 (TDZ) |
| 重复声明 | 不允许 | 不允许 |
| 重新赋值 | ✅ 允许 | ❌ 禁止(引用不可变) |
| 声明初始化 | 可延迟赋值 | 必须声明时立即赋值 |
💡 现代 JS 开发的最佳实践
在 ES6 及以后的现代 JavaScript 开发中,有一个被广泛推崇的黄金准则:
默认优先使用 const,只有在明确知道该变量后续需要被重新赋值时,才退而求其次使用 let,并且尽量完全抛弃 var。
这样做的好处是:
- 向阅读代码的人(包括未来的你自己)明确表达意图:这个值在这里是不应该被改变的。
- 减少 Bug:防止在复杂的逻辑中不小心意外修改了不该修改的变量。
- 利于引擎优化:JavaScript 引擎看到
const时,可能会对其进行特定的性能优化。