返回首页

var、let、const之间的区别

问题解析

这个问题考察的是对 JavaScript 变量声明方式演进的理解。ES6 引入了 letconst,与原有的 var 形成了三种声明方式。理解它们之间的区别是掌握现代 JavaScript 的基础,也是面试中的高频考点。

核心概念

1. 变量提升(Hoisting)

  • var:存在变量提升,声明会被提升到作用域顶部
  • let/const:不存在变量提升,存在暂时性死区(TDZ)

2. 重复声明

  • var:允许重复声明
  • let/const:不允许在同一作用域内重复声明

3. 全局对象属性

  • var/function:声明的全局变量会成为全局对象(window/global)的属性
  • let/const/class:不会成为全局对象的属性

4. 初始化与赋值

  • const:必须立即初始化,且不能重新赋值(但对象属性可以修改)
  • let/var:可以先声明后赋值

5. 作用域

  • var:函数作用域
  • let/const:块级作用域

详细解答

1. 变量提升与暂时性死区

// var 存在变量提升
console.log(a); // undefined
var a = 10;

// 等价于
var a;
console.log(a); // undefined
a = 10;

// let 不存在变量提升
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 20;

// const 同样存在暂时性死区
console.log(c); // ReferenceError
const c = 30;

暂时性死区(Temporal Dead Zone, TDZ):从块级作用域开始到变量声明语句之间,变量处于"死区",访问会报错。

var tmp = 123;

if (true) {
  // TDZ 开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ 结束
  console.log(tmp); // undefined

  tmp = 456;
  console.log(tmp); // 456
}

2. 重复声明

// var 允许重复声明
var x = 1;
var x = 2; // 合法,x 被覆盖

// let 不允许重复声明
let y = 1;
let y = 2; // SyntaxError: Identifier 'y' has already been declared

// const 同样不允许
const z = 1;
const z = 2; // SyntaxError

// 不同作用域可以重复声明
let a = 1;
{
  let a = 2; // 合法,不同块级作用域
}

3. 全局对象属性

// 浏览器环境
var foo = 1;
console.log(window.foo); // 1

let bar = 2;
console.log(window.bar); // undefined

const baz = 3;
console.log(window.baz); // undefined

// class 同样不会成为全局对象属性
class MyClass {}
console.log(window.MyClass); // undefined

4. const 的不可重新赋值

// 基本类型
const PI = 3.14159;
PI = 3.14; // TypeError: Assignment to constant variable.

// 引用类型 - 可以修改对象属性
const person = { name: 'Alice' };
person.name = 'Bob'; // 合法
person.age = 25;     // 合法
console.log(person); // { name: 'Bob', age: 25 }

// 但不能重新赋值
person = {}; // TypeError

// 如果需要完全不可变,使用 Object.freeze()
const frozen = Object.freeze({ name: 'Alice' });
frozen.name = 'Bob'; // 静默失败(严格模式下报错)
console.log(frozen.name); // 'Alice'

5. 块级作用域

// var 只有函数作用域
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// 输出:3, 3, 3(因为 i 是全局变量)

// let 有块级作用域
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 100);
}
// 输出:0, 1, 2(每次迭代都有独立的 j)

// 块级作用域示例
{
  let blockVar = 'inside';
  const blockConst = 'also inside';
}
console.log(blockVar);  // ReferenceError
console.log(blockConst); // ReferenceError

深入理解

1. 为什么需要块级作用域?

在 ES6 之前,JavaScript 只有全局作用域和函数作用域,这导致了一些问题:

// 问题1:内层变量可能覆盖外层变量
var tmp = new Date();
function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world'; // 变量提升导致 tmp 被覆盖
  }
}
f(); // undefined

// 问题2:循环变量泄漏为全局变量
var s = 'hello';
for (var i = 0; i < s.length; i++) {
  console.log(s[i]);
}
console.log(i); // 5(循环结束后 i 仍然可访问)

块级作用域解决了这些问题,使得代码更加安全和可预测。

2. 函数声明在块级作用域中的行为

ES6 规定函数声明在块级作用域内有效,但行为类似于 let

function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重复声明 f
    function f() { console.log('I am inside!'); }
  }
  f(); // 实际行为因环境而异
}());

注意:在严格模式下,函数声明在块级作用域内才有块级作用域的效果;非严格模式下,浏览器实现可能有差异。

3. const 的本质

const 保证的是变量指向的内存地址不变,而不是值不变:

// 对于基本类型,值就存储在内存地址中
const a = 1; // 内存地址存储的就是 1

// 对于引用类型,内存地址存储的是指向堆内存的指针
const arr = [1, 2, 3];
// arr 存储的是指向数组的指针
// 可以修改数组内容,因为指针没变
arr.push(4); // OK

// 但不能改变指针指向
arr = [4, 5, 6]; // Error

4. 变量提升的底层原理

JavaScript 引擎在代码执行前会进行编译,创建执行上下文:

  1. 编译阶段:扫描所有声明,将 var 声明的变量初始化为 undefinedlet/const 声明的变量放入 TDZ
  2. 执行阶段:按顺序执行代码
// 编译阶段
// var x = undefined;
// let y = <uninitialized> (TDZ)

console.log(x); // undefined
console.log(y); // ReferenceError

var x = 1;
let y = 2;

最佳实践

1. 默认使用 const

// 推荐
const API_URL = 'https://api.example.com';
const MAX_COUNT = 100;
const config = {
  timeout: 5000,
  retries: 3
};

// 当需要重新赋值时使用 let
let count = 0;
let currentUser = null;

2. 避免使用 var

// 不推荐
var name = 'John';
var items = [];

// 推荐
const name = 'John';
const items = [];

3. 在循环中优先使用 const

// for-of 循环中,如果不需要修改迭代变量,使用 const
for (const item of items) {
  console.log(item);
}

// for 循环中,计数器使用 let
for (let i = 0; i < items.length; i++) {
  console.log(items[i]);
}

4. 对象冻结实现真正的常量

// 如果需要完全不可变的对象
const CONFIG = Object.freeze({
  API_URL: 'https://api.example.com',
  VERSION: '1.0.0'
});

// 深度冻结
function deepFreeze(obj) {
  Object.keys(obj).forEach(key => {
    if (typeof obj[key] === 'object' && obj[key] !== null) {
      deepFreeze(obj[key]);
    }
  });
  return Object.freeze(obj);
}

面试要点

  1. 理解变量提升的本质:能够解释为什么 var 有变量提升而 let/const 没有

  2. 暂时性死区(TDZ):理解 TDZ 的概念,能够举例说明 TDZ 导致的错误

  3. 块级作用域的优势:能够说明块级作用域解决了哪些问题(如循环变量泄漏、变量覆盖等)

  4. const 的"不变性":理解 const 保证的是引用不变而非值不变,能够区分基本类型和引用类型的行为差异

  5. 实际应用场景

    • 为什么 for 循环中使用 let 可以正确输出 0,1,2 而 var 输出 3,3,3
    • 如何在实际开发中选择 letconst
  6. 常见陷阱

    // 陷阱1:const 对象属性可修改
    const obj = {};
    obj.a = 1; // 合法
    
    // 陷阱2:typeof 在 TDZ 中的行为
    typeof undeclaredVar; // 'undefined'
    typeof tdzVar;        // ReferenceError
    let tdzVar;
    
  7. 浏览器兼容性:了解 ES6 声明在现代浏览器中的支持情况,以及 Babel 等工具如何处理这些语法