JavaScript ES6:var、let、const 学习笔记
参考目录:
D:\learn329\wa\hl_ai\fe\js\es6\var_let_const本文结合
1.js~4.js的实际运行结果整理,并对容易混淆的概念作了校正。
1. 先建立整体认识
JavaScript 的变量不是"脱离位置而存在"的:变量声明在哪个作用域,就只能在相应的范围内访问。ES6(2015)新增 let 与 const,主要是为了弥补 var 在块级作用域、重复声明和变量提升方面容易造成的错误。
现在写业务代码时,一个实用的默认规则是:
js
const apiUrl = '/api/users'; // 默认先用 const
let total = 0; // 以后确实要重新赋值时,才用 let
total += 1;
// var 只在维护旧代码或需要理解历史代码时使用
注意:JavaScript 是动态类型语言,let 声明的变量可以被重新赋成另一种类型,但这通常会降低可读性,应尽量避免。
js
let points = 50;
points = '52'; // 语法允许,但不推荐:同一个变量的含义变得不稳定
2. 作用域:变量"在哪里可见"
可以把作用域理解为变量的可访问范围。常见的作用域有:
| 类型 | 典型位置 | 例子 |
|---|---|---|
| 全局作用域 | 最外层 | const siteName = 'Demo'; |
| 函数作用域 | 函数体内部 | function f() { var x = 1; } |
| 块级作用域 | {} 内部,如 if、for |
if (ok) { let x = 1; } |
访问变量时,JavaScript 会先在当前作用域 找;找不到就逐层向外层作用域查找;直到全局作用域仍找不到,便抛出 ReferenceError。这个过程常被称为词法作用域下的"向外查找"。
1.js:函数作用域和块级作用域
js
var height = 100;
function setWidth() {
var width = 100;
console.log(width, height);
}
setWidth(); // 100 100
函数内部能读取自己的 width,也能读取外层的 height;但函数外不能直接读取 width。
再看条件语句:
js
var age = 100;
if (age > 12) {
var dog = age * 7;
let x = 111;
console.log(dog); // 700
dog++;
}
console.log(dog); // 701
console.log(x); // ReferenceError: x is not defined
这里正好体现二者差异:
var dog不具有块级作用域,因此if外仍然能访问到它。let x只属于这对{},离开代码块后就不能访问。
因此,在 if、for、while、try...catch 等代码块中声明临时变量,优先使用 let 或 const。
3. var、let、const 对比
| 特性 | var |
let |
const |
|---|---|---|---|
| 作用域 | 函数作用域(没有块级作用域) | 块级作用域 | 块级作用域 |
| 同一作用域能否重复声明 | 可以,容易覆盖 | 不可以 | 不可以 |
| 声明后能否重新赋值 | 可以 | 可以 | 不可以重新绑定 |
| 是否必须声明时赋值 | 不必须 | 不必须 | 必须 |
| 初始化前读取 | 得到 undefined(典型情况) |
抛出 ReferenceError |
抛出 ReferenceError |
为什么少用 var
var 的"函数作用域 + 可重复声明 + 提升后值为 undefined"会让变量的真实状态不够直观。例如:
js
if (true) {
var message = 'hello';
}
console.log(message); // hello,很多初学者会误以为这里应该访问不到
改为 let 后,变量被限制在块内,代码边界更清晰:
js
if (true) {
const message = 'hello';
}
console.log(message); // ReferenceError
4. for 与 setTimeout:为什么 let 能保存每轮的 i
2.js 使用了:
js
for (let i = 0; i < 10; i++) {
console.log(i);
setTimeout(function () {
console.log(`This number is ${i}`);
});
}
同步部分先打印 0 到 9;定时器回调稍后执行时,也依次打印 This number is 0 到 This number is 9。
原因是:在 for (let i = ...) 中,let 会让每一次循环迭代拥有独立的 i 绑定;每个异步回调闭包捕获的是当轮的那个 i。
若换成 var:
js
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
});
}
循环结束时唯一的 i 已经是 3,所以通常会输出三次 3。旧代码常用立即执行函数(IIFE)解决此问题;ES6 中直接用 let 更清楚。
5. const:不能改"绑定",不等于对象完全不可变
基本类型
3.js 中:
js
const key = 'abc123';
// key = 'ABC123'; // TypeError: Assignment to constant variable.
const 在声明时必须初始化,之后不能把同一个变量重新指向其他值。
对象和数组
const 限制的是变量保存的引用/绑定,不是对象内部的每一个属性。因此下面代码合法:
js
const person = { name: '天启', age: 18 };
person.age++;
console.log(person); // { name: '天启', age: 19 }
但重新给 person 赋值不合法:
js
person = '111'; // TypeError: Assignment to constant variable.
同理,const list = [] 后可以 list.push(...),但不能执行 list = []。如果业务确实需要防止对象属性被修改,可考虑 Object.freeze();不过它默认只冻结第一层,深层对象仍需要额外处理。
6. 变量提升与暂时性死区(TDZ)
4.js:
js
console.log(pizza);
let pizza = 'Deep Dish';
运行会报:
text
ReferenceError: Cannot access 'pizza' before initialization
这是 let 的暂时性死区(Temporal Dead Zone,TDZ):从进入当前作用域开始,到执行声明并初始化的这一行之前,变量已经处于作用域中,却不能被读取或使用。
常见简化说法是"let/const 没有变量提升",但更准确的表达是:
var、let、const的声明都会在作用域建立时被处理;var会被初始化为undefined,所以声明语句之前读取通常不会立即报错;let和const在声明语句执行前处于 TDZ,访问会报错;let/const不是"绝对不存在提升",而是不能像var那样提前使用。
对比:
js
console.log(a); // undefined
var a = 1;
console.log(b); // ReferenceError
let b = 1;
最稳妥的编码习惯是不依赖任何形式的提前访问:先声明并初始化,再使用变量。
7. 关于变量生命周期的一点补充
函数或代码块结束后,其局部变量通常不再能从外部访问;如果没有任何引用指向相关数据,JavaScript 引擎会在合适的时机进行垃圾回收。这里有两个边界:
- 垃圾回收不是"代码块一结束就立刻发生",具体时机由引擎决定;
- 如果闭包仍引用局部变量,那么函数返回后该变量仍可能继续存活。
js
function makeCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counter = makeCounter();
counter(); // 1,count 仍被内部函数引用
8. 实战选择清单
- 默认使用
const:变量本身不需要重新赋值时,用它表达"绑定不变"的意图。 - 确需重新赋值时使用
let:如计数器、累加器、会更新的状态。 - 新代码避免
var:除非是在维护旧项目,或需要与其历史行为保持一致。 - 不要在声明前访问变量:它会导致
var的undefined问题或let/const的 TDZ 错误。 - 不要把
const当深度不可变:对象、数组内部仍可改变;有不可变需求时使用冻结、复制或状态管理策略。 - 在循环异步回调中优先用
let:避免多个回调共享同一个var i。
9. 小练习
判断下面每段代码的结果,并自己运行验证:
js
// A
if (true) {
var a = 1;
let b = 2;
}
console.log(a);
console.log(b);
// B
const user = { score: 0 };
user.score += 1;
// user = { score: 1 };
// C
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i));
}
答案:A 中先输出 1,随后因 b 不在块外作用域而报 ReferenceError;B 中修改属性合法、重新赋值不合法;C 中异步输出 0、1、2。