var、let、const之间的区别
问题解析
这个问题考察的是对 JavaScript 变量声明方式演进的理解。ES6 引入了 let 和 const,与原有的 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 引擎在代码执行前会进行编译,创建执行上下文:
- 编译阶段:扫描所有声明,将
var声明的变量初始化为undefined,let/const声明的变量放入 TDZ - 执行阶段:按顺序执行代码
// 编译阶段
// 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);
}
面试要点
-
理解变量提升的本质:能够解释为什么
var有变量提升而let/const没有 -
暂时性死区(TDZ):理解 TDZ 的概念,能够举例说明 TDZ 导致的错误
-
块级作用域的优势:能够说明块级作用域解决了哪些问题(如循环变量泄漏、变量覆盖等)
-
const 的"不变性":理解
const保证的是引用不变而非值不变,能够区分基本类型和引用类型的行为差异 -
实际应用场景:
- 为什么 for 循环中使用
let可以正确输出 0,1,2 而var输出 3,3,3 - 如何在实际开发中选择
let和const
- 为什么 for 循环中使用
-
常见陷阱:
// 陷阱1:const 对象属性可修改 const obj = {}; obj.a = 1; // 合法 // 陷阱2:typeof 在 TDZ 中的行为 typeof undeclaredVar; // 'undefined' typeof tdzVar; // ReferenceError let tdzVar; -
浏览器兼容性:了解 ES6 声明在现代浏览器中的支持情况,以及 Babel 等工具如何处理这些语法