引用js中的一道经典面试题,来具体展开说一说 var,let,const的区别

0 阅读7分钟

image.png

这段代码是 JavaScript 面试中非常经典的一道题,它主要考察的是闭包(Closure)以及 var 关键字的变量作用域特性。

为什么最后打印出来的是 10 而不是 6?我们可以从以下两个核心点来理解:

1. var 没有块级作用域

在 ES6 引入 letconst 之前,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 接收当前的 ia[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
    

为了让你更直观地对比,可以参考下面的总结表格:

特性varlet
作用域函数作用域块级作用域
变量提升提升并初始化为 undefined提升但不初始化(存在暂时性死区 TDZ)
重复声明✅ 允许❌ 不允许
全局属性会成为 window 对象的属性不会成为 window 对象的属性
循环表现整个循环共享同一个变量每次迭代都会创建新的变量绑定

在现代前端开发中,为了避免 var 带来的各种隐式 Bug(如变量泄露、意外覆盖等),最佳实践通常是:默认优先使用 const,需要重新赋值时使用 let,尽量完全抛弃 var

那介绍了var 跟let的区别了 let与const的区别呢?

constlet 其实非常相似,它们在作用域(都是块级作用域)、变量提升(都有暂时性死区 TDZ)、重复声明(都不允许)以及全局对象挂载(都不会挂载到 window)这四个方面是完全一致的。

它们最核心、也是唯一的本质区别在于:赋值与可变性

🔒 核心区别:赋值与可变性

  • let: 声明的是变量。你可以先声明不赋值,也可以在后续的代码中随时重新赋值。

    let a = 10;
    a = 20; // ✅ 合法,可以重新赋值
    let b; 
    b = 30; // ✅ 合法,可以先声明后赋值
    
  • const: 声明的是常量(更准确地说,是“只读的变量绑定”)。它有两个强制要求:

    1. 必须在声明的同时进行初始化赋值,不能留到后面。
    2. 一旦赋值,其绑定的内存地址(引用)就绝对不能改变,否则会直接报错。
    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 极简对比表

特性letconst
作用域块级作用域块级作用域
变量提升存在暂时性死区 (TDZ)存在暂时性死区 (TDZ)
重复声明不允许不允许
重新赋值✅ 允许❌ 禁止(引用不可变)
声明初始化可延迟赋值必须声明时立即赋值

💡 现代 JS 开发的最佳实践

在 ES6 及以后的现代 JavaScript 开发中,有一个被广泛推崇的黄金准则:

默认优先使用 const,只有在明确知道该变量后续需要被重新赋值时,才退而求其次使用 let,并且尽量完全抛弃 var

这样做的好处是:

  1. 向阅读代码的人(包括未来的你自己)明确表达意图:这个值在这里是不应该被改变的。
  2. 减少 Bug:防止在复杂的逻辑中不小心意外修改了不该修改的变量。
  3. 利于引擎优化:JavaScript 引擎看到 const 时,可能会对其进行特定的性能优化。