深入理解JavaScript变量声明:var、let、const面试精讲

85 阅读8分钟

摘要

在JavaScript的世界里,变量声明是基石。从ES6(ECMAScript 2015)开始,除了传统的var,我们又迎来了letconst。这三者之间的区别,不仅是面试中的高频考点,更是理解JavaScript作用域、变量生命周期和编程范式的关键。本文将以面试精讲的形式,深入剖析varletconst的底层原理、核心差异、应用场景,并结合面试常见问题,帮助读者透彻理解并灵活运用这三种变量声明方式,从而在面试中脱颖而出,并在实际开发中写出更健壮、更可维护的代码。

1. var:历史的遗产与“坑”的源泉

var是ES6之前JavaScript唯一的变量声明方式。它带来了“变量提升”和“函数作用域”这两个核心特性,但也因此埋下了不少“坑”。

1.1 变量提升(Hoisting)

概念var声明的变量会发生变量提升,即在代码执行之前,变量的声明会被“提升”到其所在作用域的顶部。但需要注意的是,只有声明会被提升,赋值操作不会被提升

示例分析(来自1.js

// 1.js
showName(); // 驼峰式命名
console.log(myName);
​
var myName = "wanjunhao"; // 全局变量
function showName() {
    let b=2; // 局部变量
    console.log("函数执行了");
}

这段代码的实际执行顺序可以理解为:

var myName; // 变量声明被提升到全局作用域顶部,此时值为 undefined
function showName() { // 函数声明也被提升到全局作用域顶部
    let b; // let声明的变量也会提升,但有暂时性死区
    b = 2;
    console.log("函数执行了");
}
​
showName(); // 正常执行,输出 "函数执行了"
console.log(myName); // 输出 undefined,因为 myName 此时只声明未赋值
​
myName = "wanjunhao"; // 赋值操作在原位置执行

面试考点

  • 为什么console.log(myName)会输出undefined而不是报错?

    • 因为var myName的声明被提升了,所以在console.log执行时,myName变量已经存在,只是还没有被赋值,所以是undefined
  • 变量提升的“坏处”是什么?

    • 代码可读性差:变量可以在声明之前被使用,导致代码的执行结果与阅读顺序不一致,容易产生歧义和难以追踪的bug。
    • 意外的全局变量:如果在函数内部不使用var声明变量,该变量会自动成为全局变量,污染全局作用域。

1.2 函数作用域

var声明的变量只具有函数作用域(Function Scope)或全局作用域(Global Scope)。这意味着var声明的变量在ifforwhile等块级语句中声明时,其作用域不会被限制在这些块中,而是会“泄露”到其所在的函数作用域或全局作用域。

示例

if (true) {
  var x = 10;
}
console.log(x); // 输出 10
​
for (var i = 0; i < 3; i++) {
  // ...
}
console.log(i); // 输出 3

面试考点

  • 为什么var声明的变量没有块级作用域?

    • 这是var的设计缺陷,它不遵循块级作用域的规则,导致变量容易在不期望的地方被访问或修改。
  • 经典的循环闭包问题

    for (var i = 0; i < 5; i++) {
      setTimeout(function() {
        console.log(i);
      }, 1000);
    }
    // 预期输出:0, 1, 2, 3, 4
    // 实际输出:5, 5, 5, 5, 5
    
    • 原因setTimeout是异步执行的,当回调函数执行时,for循环已经结束,i的值已经变为5。由于var没有块级作用域,所有回调函数共享同一个i变量。

2. let:块级作用域的救星

let是ES6引入的新的变量声明方式,旨在解决var的诸多问题,特别是引入了“块级作用域”和“暂时性死区”。

2.1 块级作用域(Block Scope)

let声明的变量只在声明它的代码块({})内有效。这使得变量的作用域更加清晰,避免了var带来的变量污染问题。

示例

if (true) {
  let x = 10;
}
console.log(x); // ReferenceError: x is not defined
​
for (let i = 0; i < 3; i++) {
  // ...
}
console.log(i); // ReferenceError: i is not defined

面试考点

  • 如何解决var的循环闭包问题?

    • 使用let声明循环变量即可。因为let为每次循环都创建了一个独立的块级作用域,确保每个回调函数都捕获到当前循环迭代的i值。
    for (let i = 0; i < 5; i++) {
      setTimeout(function() {
        console.log(i);
      }, 1000);
    }
    // 输出:0, 1, 2, 3, 4
    

2.2 暂时性死区(Temporal Dead Zone - TDZ)

letconst声明的变量也存在变量提升,但与var不同的是,它们在声明之前是不可访问的。从代码块的开始到变量声明语句之间的区域,被称为“暂时性死区”。在此区域内访问变量会抛出ReferenceError

示例分析(来自2.js

// 2.js
console.log(a);  // ReferenceError: Cannot access 'a' before initialization
let a = 1; // 词法作用域

底层原理

JavaScript引擎在解析代码时,会先扫描整个作用域,找到所有的letconst声明,并将其放入一个“未初始化”的状态。当代码执行到变量声明的那一行时,变量才会被初始化并赋值。在“未初始化”状态到初始化完成之前的这段时间,就是暂时性死区。

面试考点

  • 什么是暂时性死区?为什么letconst会有暂时性死区?

    • 解释概念和底层原理,强调这是为了避免var那种在声明前访问变量导致的混乱。
  • typeof操作符在暂时性死区中的表现?

    • 在暂时性死区中对letconst变量使用typeof也会抛出ReferenceError,而不是undefined
    console.log(typeof b); // ReferenceError
    let b = 1;
    

3. const:常量的守护者

const也是ES6引入的,用于声明常量。它与let有很多相似之处,例如都具有块级作用域和暂时性死区,但其核心特性是“常量不可变”。

3.1 常量不可变性

const声明的变量在声明时必须进行初始化,并且一旦赋值后,其值就不能再被重新赋值。

示例

const PI = 3.14;
PI = 3.14159; // TypeError: Assignment to constant variable.
​
const obj = { a: 1 };
obj = {}; // TypeError: Assignment to constant variable.

面试考点

  • const声明的对象或数组可以修改吗?

    • 可以const保证的是变量指向的内存地址不变,而不是内存地址中存储的值不变。对于引用类型(对象、数组),const只保证变量名指向的那个引用地址不变,但该引用地址指向的对象或数组内部的属性或元素是可以修改的。
    const arr = [1, 2, 3];
    arr.push(4); // 允许,arr 仍然指向同一个数组
    console.log(arr); // [1, 2, 3, 4]
    ​
    const user = { name: "Alice" };
    user.name = "Bob"; // 允许,user 仍然指向同一个对象
    console.log(user); // { name: "Bob" }
    

3.2 块级作用域与暂时性死区

constlet一样,也具有块级作用域和暂时性死区。

if (true) {
  const MAX_VALUE = 100;
}
console.log(MAX_VALUE); // ReferenceError: MAX_VALUE is not defined
​
console.log(MIN_VALUE); // ReferenceError: Cannot access 'MIN_VALUE' before initialization
const MIN_VALUE = 0;

4. varletconst的对比总结

特性varletconst
作用域函数作用域块级作用域块级作用域
变量提升有(声明提升,赋值不提升)有(声明提升,但存在暂时性死区)有(声明提升,但存在暂时性死区)
重复声明允许不允许不允许
修改允许允许不允许(对基本类型,对引用类型可修改内部)
初始值可不初始化可不初始化必须初始化

5. 面试常见问题与最佳实践

5.1 面试常见问题

  1. varletconst三者的区别是什么?

    • 从作用域、变量提升、重复声明、修改、初始值等方面进行全面对比。
  2. 什么是变量提升?varlet/const的变量提升有什么不同?

    • 解释概念,强调var的提升是“声明和初始化”一起,而let/const只有“声明”提升,但有暂时性死区。
  3. 什么是暂时性死区?为什么会有暂时性死区?

    • 解释概念、原因和typeof的表现。
  4. const声明的对象或数组可以修改吗?为什么?

    • 解释const对基本类型和引用类型的不同限制,强调其是“引用地址不可变”。
  5. 在实际开发中,如何选择使用varletconst

    • 优先使用const:如果变量的值在声明后不会改变,就使用const。这有助于提高代码的可读性和可维护性,避免意外修改。
    • 其次使用let:如果变量的值需要改变,就使用let
    • 避免使用var:除非是兼容老旧浏览器等特殊情况,否则应避免使用var,以避免其带来的变量提升和作用域问题。

5.2 最佳实践

  • 拥抱ES6+ :在现代JavaScript开发中,始终使用letconst来声明变量,彻底告别var
  • 明确变量意图:通过选择letconst,清晰地表达变量是否会被重新赋值。
  • 避免全局污染:利用letconst的块级作用域,减少全局变量的产生。
  • 合理使用const:即使是对象或数组,如果其引用本身不会改变,也应优先使用const,以表明其引用关系的稳定性。

6. 总结

varletconst不仅仅是JavaScript中声明变量的关键字,它们背后蕴含着JavaScript引擎对变量生命周期、作用域管理和内存分配的底层机制。理解这些机制,不仅能帮助我们更好地应对面试中的挑战,更能提升我们的编程素养,写出更严谨、更高效、更易于维护的JavaScript代码。

希望通过本文的深入解析,读者能够对varletconst有全面而底层的认识,并在未来的学习和工作中游刃有余。