本文深入解析 JavaScript 中 var、let、const 三种变量声明方式的核心区别。详细探讨变量提升(Hoisting)机制、暂时性死区(TDZ)原理及块级作用域特性,结合经典面试题剖析常见陷阱。最后给出 ES6 环境下的变量声明最佳实践建议,帮助开发者编写更安全、规范的代码。
简介
var, let and const是JavaScript中三种定义变量的方式,它们之间有什么区别呢?这是前端面试中常见的一道题,今天我们来一文说透它。
let和const区别不大,主要是const声明的是常量,不可修改,而let声明的变量是可修改的。所以我们重点放在var和let上。
变量初始化
在声明变量的同时为其赋予初始值,这一过程被称为初始化。
- 使用
var和let声明变量时,可以不立即赋值,此时变量的默认值为undefined。 - 相比之下,使用
const声明变量时必须同时提供初始值,否则程序会抛出语法错误。
// `var`和`let`声明的变量可以不赋值,此时变量的值为`undefined`。
var num; // num的值是undefined
num = 1; // num的值是1
let str; // str的值是undefined
str = 'hello'; // str的值是'hello' // `const`声明的变量必须赋值,否则会报错。
const a; // SyntaxError: Missing initializer in const declaration变量提升 - Hoisting
“Hoisting”一词中文通常译为“提升”,指的是变量的声明会被移动到其所在作用域的顶部,但需要注意的是,只有声明会被提升,赋值操作不会。
- 使用
var声明的变量,其声明部分会被提升至当前作用域的顶部。 - 而使用
let和const声明的变量则不会发生提升现象(注:关于此点的具体行为存在不同解读,建议参考 MDN 官方文档)。 - 提升机制仅针对变量声明,不涉及赋值操作。
当 在全局作用域中声明时,其声明会被提升至全局作用域的顶部。
console.log(name); // undefined
var name = 'Philip';上述代码的执行逻辑等价于以下形式:
var name; // `var`声明的变量会被提升到其作用域顶部。
console.log(name); // undefined
name = 'Philip';若 在函数作用域内声明,其声明同样会被提升至该函数作用域的顶部。
function printName() {
console.log(name); // undefined
var name = 'Philip';
}
printName();上述代码的执行逻辑等价于以下形式:
function printName() {
var name; // `var`声明的变量会被提升到其作用域顶部。
console.log(name); // undefined
name = 'Philip';
}
printName();使用 和 声明的变量不会发生提升。
由于 let 和 const 不会提升,因此尝试在声明前访问它们会导致错误。
console.log(num); // ReferenceError: Cannot access 'num' before initialization
const num = 1;
此前曾提及,关于 let 和 const 是否被提升存在争议。
- 一种观点认为,
let和const不会被提升,因此在声明前访问会直接报错。 - 另一种观点则认为,
let和const会被提升,但在声明前的访问会抛出Temporal Dead Zone错误。
参考以下代码示例:
const x = 1;
{
console.log(x); // ReferenceError: Cannot access 'x' before initialization
const x = 2;
}该代码会抛出错误。然而,如果将 {} 块内的 const x = 2; 注释掉,代码即可正常运行。如果 const x = 2 真的没有被提升,那么 console.log(x) 应该能访问到全局的 const x = 1 而不会报错。反之,正是因为 const x = 2 发生了提升,导致 console.log(x) 访问的是提升后的 x,而此时 x 尚未初始化,从而引发错误。
提升机制仅针对变量声明,不包括赋值操作。
以下代码会报错,因为 `x = 1` 属于赋值操作而非声明,因此不会触发提升。(注意:如果变量声明前未添加 var, let 或 const,则会意外创建一个全局变量。)
console.log(x); // ReferenceError: x is not defined
x = 1;当存在同名函数和变量时,提升后变量声明会位于函数声明之前(即函数声明会覆盖变量声明)。
在以下代码中,存在一个同名函数和变量。
console.log(foo); // [Function: foo], not undefined.
function foo() {
console.log('function foo');
}
var foo = 1;提升后的代码逻辑如下:
var foo;
function foo() {
console.log('function foo');
}
console.log(foo);
foo = 1;面试题
通过几道经典面试题来检验对提升机制的理解,请判断以下代码的输出结果:
- 第一题
a = 2;
var a;
console.log(a); // 2解决 提升问题的方法很简单,只需按照提升规则重写代码即可,上述代码等价于如下形式,结果一目了然。
var a;
a = 2;
console.log(a); // 2- 第二题
var a = true;
foo();
function foo() {
if (a) {
var a = 10;
}
console.log(a);
}只要函数内部存在 var 声明的变量,所有全局声明的 var 变量都会被忽略。上述代码提升后等价于如下形式(注意 function 也会发生提升),函数内部的 声明永远会覆盖全局的 var。
var a = true;
function foo() {
var a; // value of a is `undefined`
if (a) {
a = 10; // never executed.
}
console.log(a);
}
foo();- 第三题
function fn() {
console.log(typeof foo);
var foo = 'variable';
function foo() {
return 'function';
}
console.log(typeof foo);
}
fn();同样地,此类题目的解法是依据提升规则重写代码,上述代码提升后等价于如下形式:
function fn() {
var foo;
function foo() {
return 'function';
}
console.log(typeof foo);
foo = 'variable';
console.log(typeof foo);
}
fn();因此,输出结果分别为 function 和 string。
变量的作用域
- 使用
var声明的变量仅具备两种作用域:全局作用域和函数作用域(不具备块级作用域)。 - 使用
let和const声明的变量具备三种作用域:全局作用域、函数作用域以及块级作用域。 - 使用
var声明的全局变量会挂载到window对象上,而let和const声明的全局变量则不会。 let和const存在“暂时性死区”,而var没有此概念。
面试题
第一题
以下代码的输出结果是什么?
let x = 1;
{
let x = 2;
}
console.log(x);答案:1。因为 let 具备块级作用域,所以 let x = 2 仅在 {} 代码块内有效。
第二题
以下代码的输出结果是什么?
var x = 1;
{
var x = 2;
}
console.log(x);答案:2。因为 var 不具备块级作用域,所以 var x = 2 会覆盖外部的 var x = 1。
第三题
以下代码的输出结果是什么?
let name = 'zdd';
{
console.log(name);
let name = 'Philip';
}答案:ReferenceError: Cannot access 'name' before initialization。因为 let 具备块级作用域,所以 console.log(name); 访问的是 let name = 'Philip'; 之前的 name,而此时 name 尚未初始化,处于暂时性死区中,因此报错。
第四题
以下代码的输出结果是什么?
'use strict';
{
function foo() {
console.log('foo');
}
}
foo();答案:ReferenceError: foo is not defined。因为 foo 是在块级作用域内声明的,所以在外部无法访问。但如果移除 'use strict'; 严格模式限制,代码即可正常运行,因为在非严格模式下,函数声明会被提升至全局作用域。
第五题
以下代码的输出结果是什么?
(() => {
let x;
let y;
try {
throw new Error();
} catch (x) {
x = 1;
y = 2;
console.log(x);
}
console.log(x);
console.log(y);
})();答案:1 undefined 2。因为 catch 块中的 x 是一个新变量,并非外部的 x,所以 x = 1 仅会修改 catch 块内的 x,而不会影响外部的 x。而 y = 2 并非 catch 的参数,仅在 catch 块内赋值,因此会改变外部的 y。
暂时性死区 - Temporal Dead Zone
TDZ 即 Temporal Dead Zone(中文名为暂时性死区),是指 let 和 const 声明的变量在其作用域开始到变量实际声明之间的这段区域。在暂时性死区内无法访问变量,否则会导致错误。
function foo() {
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let a = 1;
const b = 2;
}
foo();对于上述代码,常量 b 的暂时性死区从函数第一行开始,直到 b 声明结束。而 console.log(b); 这句代码恰恰在暂时性死区内访问了 b,因此会报错。
面试题
以下代码的输出结果是什么?
function foo() {
console.log(typeof bar);
const bar = 1;
}
foo();答案:
ReferenceError: Cannot access 'bar' before initialization。因为 console.log(typeof bar); 这句代码在 bar 的暂时性死区内访问了 bar,所以会报错。由此可见,即使是 typeof 这种通常不会报错的操作符,也无法规避暂时性死区。
如果移除 const bar = 1;,代码将不再报错。typeof 操作符对于未声明的变量不会报错,而是返回 undefined。
function foo() {
console.log(typeof bar); // 输出undefined
}重新声明- Redeclaration
- 使用
var声明的变量允许被重复声明,后声明的会覆盖先声明的。 - 使用
let和const声明的变量不允许被重复声明。
面试题
查看几道面试题,判断以下代码的输出结果:
- 第一题
var a = 1;
function foo() {
var a = 2;
{
var a = 3;
console.log(a);
}
console.log(a);
}
foo();
console.log(a);答案:3 3 1。此题主要考察两个知识点:
- 使用
var声明的变量没有块级作用域。 - 使用
var声明的变量可以被重复声明,后声明的会覆盖先声明的。 因此var a = 3会覆盖外部的var a = 2,但var a = 2不会覆盖最外层的var a = 1。这是因为var仅具备函数作用域。
上述代码提升后等价于如下形式:
var a;
a = 1;
function foo() {
var a;
var a; // redeclaration
a = 2;
{
a = 3;
console.log(a);
}
console.log(a);
}
foo();
console.log(a);注意:面试题中凡是用 {} 包裹 var 的通常都是障眼法,var 并不具备块级作用域。
第二题
这道题较为简单,考察的是 let 的块级作用域,代码输出结果为 2, 1。因为 let 具备块级作用域,let a = 2 仅在 {} 代码块内有效。
function foo() {
let a = 1;
{
let a = 2;
console.log(a);
}
console.log(a);
}
foo();意外的全局变量
如果在声明变量时忘记了书写 var, let 或者 const,那么该变量将成为所谓的“Accidental Global Variables”,意为“意外的全局变量”。
function f1() {
b = 2; // accident global variable
}
f1();
console.log(b); // 2面试题
以下代码的输出结果是什么?
for (var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
})
}答案:3 3 3。
因为 var 不具备块级作用域,所以 setTimeout 内的 i 均指向同一个 i。而 setTimeout 是异步操作,其回调函数代码需先进入宏任务队列,待 for 循环结束后才能执行,此时 i 的值已是 3。关于此题的详细解释,请参阅相关文章。
最佳实践
如今 ES6 已广泛普及,在业务代码中基本不再需要使用
var,var目前仅见于 JS 框架或底层工具库中。对于
let和const,建议优先使用const,仅在确实需要修改变量值的情况下才使用let。经典的 `for` 循环应使用
let,因为循环变量在迭代过程中会被修改。for (let i = 0; i < 5; i++) { console.log(i); }对于
for...in和for...of循环,应使用const,因为循环变量在迭代过程中不会被修改。const arr = [1, 2, 3]; for (const item of arr) { console.log(item); }const obj = {a: 1, b: 2}; for (const key in obj) { console.log(key); }
至此,关于 JavaScript 中 var、let、const 的文章介绍完毕,更多相关 js var let const 内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!