第5篇:分清JS的变量声明var、let、const

111 阅读11分钟

上一篇说的是JS的关键字与保留字,其中提到了varletconst这三个关键字——它们是JS中声明变量、常量的核心工具。

不知道什么时候用var、什么时候用let,更分不清constlet的区别,随手用var声明变量,可能会遇到变量泄漏、重复声明覆盖等问题,只要吃透三者的核心区别、作用域和使用场景,就能彻底避开这些坑。

阅读收获:彻底分清varletconst的核心区别(作用域、变量提升、重复声明等),掌握三者的使用场景,能根据需求灵活选择合适的声明方式,写出规范、可维护的JS代码。

1. 为什么有三种变量声明方式?

既然都是声明变量,为什么JS要搞varletconst三种方式?其实这和JS的版本更新有关,核心是为了规范代码、避免bug。

  • var:是JS早期版本(ES5及之前)唯一的变量声明方式,存在很多设计缺陷,比如变量泄漏、允许重复声明,现在已被行业淘汰,不推荐使用;
  • letconst:ES6新增的声明方式,修复了var的所有缺陷,增加了块级作用域、暂时性死区等特性,是当前主流、推荐使用的声明方式;

核心原则:优先使用const(值不修改时),其次用let(值需要修改时),彻底放弃var(避免踩坑)。

虽然var现在还能在部分旧项目中看到,新手最好还是不要使用var,直接学习letconst的规范用法,避免养成不良编码习惯,减少后续修改成本。

2. varletconst 核心区别

ECMAScript 变量是松散类型的,意思是变量可以保存任何类型的数据。每个变量都是一个用来保存任意值的命名占位符。

特性varletconst
作用域函数作用域 / 全局作用域(无块级作用域)块级作用域({}界定,如iffor块内)块级作用域({}界定,与let一致)
变量提升存在(声明提升到顶部,值为undefined)不存在(暂时性死区,声明前访问报错)不存在(暂时性死区,声明前访问报错)
重复声明允许(同一作用域可重复声明)不允许(同一作用域不可重复声明)不允许(同一作用域不可重复声明)
重新赋值允许(可随意修改值,甚至改变类型)允许(值可修改,最好不要改变类型)必须初始化,不可重新赋值(对象/数组可修改内部属性)

3. var声明(已淘汰)

var是JS早期的变量声明方式,存在很多设计缺陷,现在主流项目已完全不用。核心记住:var 无块级作用域、存在变量提升、允许重复声明,这三个特性即可。

3.1 var 声明的核心特征

  1. 无块级作用域:if、for等块内声明的变量,块外可访问(变量泄漏)
if (true) {
    var name = "橘朵"; // 块内用var声明
    console.log(name); // 输出:橘朵(块内正常访问)
}
console.log(name); // 输出:橘朵(块外也能访问,变量泄漏,不符合直觉)
  1. 存在变量提升:声明被提升到作用域顶部,赋值留在原地,声明前访问值为undefined
console.log(age); // 输出:undefined(不报错,变量提升)
var age = 25; // 声明+赋值
//ECMAScript 运行时把它看成等价于如下代码
var age;
console.log(age); // undefined
age=25;
  1. 允许重复声明:同一作用域可多次声明同名变量,后面的覆盖前面的
var num = 10;
var num = 20; // 重复声明,不报错
console.log(num); // 输出:20(后面的覆盖前面的)
  1. 可随意重新赋值,甚至改变类型(不推荐,易导致逻辑错误)
var message = "你好";
message = 100; // 改变类型,不报错
console.log(message); // 输出:100

3.2 var声明在实际开发中可能遇到的问题

在实际开发中,var的变量提升特性会带来一些意想不到的问题。下面是几个典型的场景:

3.2.1 循环中的变量泄漏

for循环中使用 var声明计数器,循环结束后这个 变量仍然存在于循环体所在的作用域中,这可能导致错误引用。

for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // 输出 3, 3, 3
    }, 100);
}
console.log("循环结束后的 i:", i); // 输出 3

问题原因:变量 i被提升到全局或函数作用域,整个循环中共享同一个变量。异步操作(如 setTimeout)触发时,循环已结束,i的值为 3。

解决方案:使用 let声明循环变量,它为每次迭代创建新的绑定。每个回调函数捕获的是当次循环的 j值,而且循环外部无法访问 j

for (let j = 0; j < 3; j++) {
    setTimeout(function() {
        console.log(j); // 输出 0, 1, 2
    }, 100);
}
console . log (j); // 报错:j is not defined

3.2.2 条件块内的意外覆盖

由于 var是函数级作用域,在条件块(如 if)内部声明的变量会覆盖外部同名变量。

function example() {
    var x = 1;
    if (true) {
        var x = 2; // 覆盖了外层的 x
        console.log(x); // 输出 2
    }
    console.log(x); // 输出 2,而非预期的 1
}
example();

问题原因if块内的 var x被提升到函数顶部,与函数开头声明的 x是同一个变量,导致内部赋值影响外部。

解决方案:使用 letconst声明块级作用域变量。if块内的 x与外层的 x互不干扰。

function exampleFixed() {
    let x = 1;
    if (true) {
        let x = 2; // 独立的块级变量
        console.log(x); // 输出 2
    }
    console.log(x); // 输出 1
}

3.2.3 变量使用先于声明

变量提升允许在声明前使用变量,但值为 undefined,这可能导致逻辑错误。

console.log(username); // 输出 undefined
var username = "Alice";

问题原因var username的声明被提升,但赋值 (= "Alice") 仍在原地。在声明前访问,得到 undefined

解决方案:使用 letconst,它们存在“暂时性死区”(TDZ),在声明前访问会抛出错误,有助于提前发现问题。

console . log (usernameLet); // 报错:Cannot access 'usernameLet' before initialization
let usernameLet = "Alice";

3.2.4 函数与变量声明的优先级

当同一个作用域下存在同名函数声明和 var变量声明时,它们的提升优先级可能导致意外。

console.log(typeof foo); // 输出 "function"
var foo = "variable";
function foo() {
    console.log("I'm a function");
}
console.log(typeof foo); // 输出 "string" 

问题原因:函数声明提升优先于变量声明提升。最初 foo是函数,但后来被变量赋值覆盖为字符串。

解决方案:避免在同一作用域内重复声明同名标识符。使用 letconst声明变量可避免此问题,因为它们不允许重复声明。

function  bar () {}
let bar = "variable" ; // 报错:Identifier 'bar' has already been declared

4. let声明 (推荐使用,变量首选)

let是ES6新增的变量声明方式,跟var的作用差不多,但修复了var的所有缺陷,核心特性:块级作用域、无变量提升(暂时性死区)、不允许重复声明、可重新赋值,适合声明“值需要修改”的。

var最明显的区别是,let 声明的范围是块作用域,而 var 声明的范围是函数作用域。

let 声明的变量只在其所在的代码块(例如 {} if 语句、 for 循环)内有效。 这能避免变量泄露到外部,尤其是循环或条件判断。

4.1 let 的核心用法

  1. 块级作用域:块内声明的变量,块外不可访问(无变量泄漏)
if (true) {
    let age = 25;
    console.log(age); // 输出:25(块内正常访问)
}
// console.log(age); // 报错:age is not defined(块外不可访问,符合预期)
  1. 无变量提升(暂时性死区):声明前访问报错,强制先声明后使用
// console.log(gender); // 报错:Cannot access 'gender' before initialization
let gender = "女";
  1. 不允许重复声明:同一作用域内,同名变量不能重复声明
let userName = "橘朵";
// let userName = "张三"; // 报错:Identifier 'userName' has already been declared
  1. 可重新赋值:值可以修改,建议不改变类型(提升代码可读性)
let score = 85;
score = 90; // 允许重新赋值
// score = "优秀"; // 不推荐,改变类型,易导致逻辑错误
console.log(score); // 输出:90
  1. 嵌套声明:不同作用域内,可声明同名变量(互不干扰)
let color = "红色";
if (true) {
    let color = "蓝色"; // 独立的块级变量
    console.log(color); // 输出:蓝色
}
console.log(color); // 输出:红色

4.2 let 的特殊注意点

  • 注意1:let不能和varconst在同一作用域内重复声明(即使声明方式不同,也会报错);
  • 注意2:let在全局作用域声明的变量,不会成为window对象的属性(与var不同);
  • 注意3:不建议用let进行条件声明(比如在if块内声明let变量,块外无法访问,容易导致逻辑错误)。
// 错误:同一作用域,let与var重复声明
var name = "Matt";
// let name = "橘朵"; // 报错:Identifier 'name' has already been declared

// 区别:let在全局声明,不成为window属性
var name = "Matt";
console.log(window.name); // 输出:Matt(var声明的全局变量,挂载到window)

let age = 25;
console.log(window.age); // 输出:undefined(let声明的全局变量,不挂载到window)

//不建议用let进行条件声明,条件外无法访问,可以在块外先声明,或使用三元运算符
let someVar; // 先声明
// ... 一些逻辑
if (someCondition) {
    someVar = 'value A';
} else {
    someVar = 'value B';
}
// 使用三元运算符
let someVar = someCondition ? 'value A' : 'value B' ;

5. const声明 (有限推荐,常量首选)

const 也是ES6新增的声明方式,和let基本相同,唯一一个重要的区别是用它声明的变量必须初始化,而且不可以重新赋值,修改 const 声明的变量会导致运行时错误

关键点注意:const声明是“引用不可变”,不是“值不可变”。如果const 声明的时候对象/数组,可以修改对象内部的属性、数组内的元素。

5.1 const的核心用法

  1. 必须初始化:声明时必须赋值,否则报错
const MY_CONSTANT; // SyntaxError: Missing initializer in const declaration
const MY_CONSTANT = "CONSTANT";
  1. 不可以重新赋值:赋值后不能修改,否则报错
const age = 26; 
//age = 36; // TypeError: 给常量赋值
  1. 块级作用域:和let一致,块内声明,块外不可访问
if (true) {
    const message = "Hello";
    console.log(message); // 输出:Hello
}
// console.log(message); // 报错:message is not defined
  1. const 不允许重复声明:和let一致,同一作用域不可以重复声明
const name = 'Matt'; 
const name = 'Nicholas'; // SyntaxError
  1. 对象/数组可以修改内部的属性/元素(引用不变即可)
// 声明对象
const user = { name: "橘朵", age: 25 };
user.age = 26; // 允许,修改对象内部属性(不改变引用)
console.log(user.age); // 输出:26
// user = { name: "张三" }; // 报错:Assignment to constant variable.(改变引用)

// 声明数组
const fruits = ["苹果", "香蕉"];
fruits.push("橙子"); // 允许,修改数组内部元素(不改变引用)
console.log(fruits); // 输出:["苹果", "香蕉", "橙子"]
// fruits = ["葡萄", "西瓜"]; // 报错:Assignment to constant variable.(改变引用)

5.2 const的使用场景

const的使用场景很广泛,只要变量的值不需要修改,都优先用const,能避免意外的修改,提升代码可读性和安全性:

  • 场景1:声明固定常量(如PI、最大/最小值、固定配置)
const PI = 3.14159;
const MAX_UPLOAD_SIZE = 10 * 1024 * 1024;
  • 场景2:声明对象、数组(只要不重新赋值,就用const
const user = { name: "橘朵" };
const fruits = ["苹果", "香蕉"];
  • 声明函数(如箭头函数、普通函数,不重新赋值时)
const add = (a, b) => a + b;
  • 场景4:for-offor-in循环中,声明不需要修改的迭代变量
// for-of循环
const arr = [1, 2, 3];
for (const item of arr) {
    console.log(item); // 输出:1, 2, 3(item不需要修改,用const)
}
// for-in循环
for (const key in {a: 1, b: 2}) { 
 console.log(key) ; // a, b
} 

6. varletconst 使用规范

  • 规范1:彻底放弃 var,无论什么场景,都不用var声明变量,避免变量泄漏、重复声明等问题;
  • 规范2:优先使用 const,只要变量的值不需要修改(包括对象、数组),就用const,能避免意外修改;
  • 规范3:按需使用 let,只有当变量的值需要动态修改(比如循环计数器、动态赋值的变量),才用let
  • 规范4:避免重复声明、改变变量类型,同一作用域内不重复声明同名变量,变量赋值时尽量不改变类型(提升代码可读性)。

7. 总结

这篇讲的是varletconst的核心区别和用法,核心就是记住:放弃var,优先用const,按需用let。记住使用规范,就能规范声明变量,写出更安全、可维护的JS代码。

总结一下核心要点:

  • var:无块级作用域、有变量提升、允许重复声明,已淘汰,不推荐使用;
  • let:块级作用域、无变量提升(暂时性死区)、不允许重复声明、可重新赋值,适合值需要修改的变量;
  • const:块级作用域、无变量提升(暂时性死区)、不允许重复声明、必须初始化、不可重新赋值,适合值不修改的常量(对象/数组可修改内部属性);
  • 核心规范:放弃var,优先const,按需let,不重复声明、不改变变量类型。

下一篇预告,解锁JS中最基础的“类型检测工具”——typeof操作符,判断数据类型。