现代 JavaScript (ES6+) 入门到实战(一):告别 var!拥抱 let 与 const,彻底搞懂作用域
如果你写过几年 JavaScript,你一定对 var
又爱又恨。它简单、直接,但它的一些“怪异行为”也可能是你调试半天的罪魁祸首。
今天,我们就从 JS 进化之旅的第一站开始:彻底告别 var
,迎接更强大、更安全的 let
与 const
。
一、回忆杀:var
的两大“坑”
在 ES6 出现之前,var
是我们声明变量的唯一方式。它有两个现在看来非常令人困惑的特性。
坑 1:变量提升 (Hoisting)
用 var
声明的变量,无论写在哪里,都会被“提升”到其所在作用域的顶部。
【过去我们这么写 (ES5)】
console.log(myVar); // 输出:undefined
var myVar = 'Hello, world!';
是不是很奇怪?明明在 console.log
之后才声明 myVar
,但程序没有报错,而是输出了 undefined
。这是因为 JS 引擎在执行代码前,会先把 var myVar
这部分“提升”到最前面,但赋值操作 myVar = '...'
留在了原地。上面的代码实际等同于:
var myVar; // 变量被提升,并默认赋值 undefined
console.log(myVar);
myVar = 'Hello, world!'; // 赋值操作留在原地
这种行为在代码复杂时,会造成意想不到的 bug。
坑 2:没有块级作用域
var
只认函数作用域和全局作用域,不认 if
、for
等语句块产生的作用域。
【过去我们这么写 (ES5)】
for (var i = 0; i < 3; i++) {setTimeout(function() {console.log(i); // 你期望输出 0, 1, 2}, 100);
}
// 实际输出:3, 3, 3
这是经典的面试题。因为 setTimeout
是异步的,当它里面的函数执行时,for
循环早已经结束了。此时,i
的值已经变成了 3。由于 var
没有块级作用域,所有的 setTimeout
回调函数共享着同一个 i
,所以它们最后都打印出了 3。
为了解决这个问题,我们过去需要用“立即执行函数表达式 (IIFE)”来创建一个闭包,非常繁琐。
二、进化时刻:let
与 const
登场
ES6 带来了两个全新的变量声明命令,它们的设计初衷就是为了解决 var
的历史遗留问题。
1. let
:真正的块级作用域
let
的行为和你直觉中的变量行为几乎完全一致。
- 没有变量提升:在声明之前访问
let
变量,会直接报错ReferenceError
。这被称为“暂时性死区 (TDZ)”,它能帮我们更早地发现错误。 - 拥有块级作用域:
let
声明的变量只在它所在的{}
代码块内有效。
【现在我们这么写 (ES6+)】
现在我们再来看上面那个 for
循环的例子,只需要把 var
换成 let
:
for (let i = 0; i < 3; i++) {setTimeout(function() {console.log(i);}, 100);
}
// 输出:0, 1, 2
奇迹发生了!结果完全符合预期。这是因为每次循环,let
都会创建一个新的、独立的块级作用域,并且把当前的 i
值绑定到这个作用域中。每个 setTimeout
回调都“记住”了自己那一次循环的 i
值。
2. const
:声明一个不可变的常量
const
的行为和 let
基本一样(拥有块级作用域,无变量提升),但有一个额外的特点:一旦声明,就必须立即赋值,并且之后不能再被修改。
【现在我们这么写 (ES6+)】
const PI = 3.14159;
PI = 3; // Uncaught TypeError: Assignment to constant variable.const USER_CONFIG = { theme: 'dark' };
USER_CONFIG.theme = 'light'; // 这可以!USER_CONFIG = {}; // 这不行!
注意:const
保证的是变量指向的内存地址不可变。对于基本类型(数字、字符串),就意味着值不可变。但对于对象、数组等引用类型,你不能让变量指向一个新对象,但可以修改该对象内部的属性。
三、该用哪个?我的建议
- 默认使用
const
:优先使用const
是一个好习惯。这能防止你在不经意间修改了不该修改的变量,让代码更具可预测性。 - 当变量需要被重新赋值时,使用
let
:比如循环中的计数器,或者值会根据逻辑改变的变量。 - 彻底忘记
var
:在新的项目中,没有任何理由再使用var
。
总结
特性 | var | let | const |
---|---|---|---|
作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
变量提升 | 是 | 否 (有 TDZ) | 否 (有 TDZ) |
重复声明 | 允许 | 不允许 | 不允许 |
重新赋值 | 允许 | 允许 | 不允许 |
今天,我们完成了从 var
到 let
和 const
的进化。这不仅仅是语法的替换,更是编码思维的升级。从现在开始,让你的代码更严谨、更安全吧!
下一篇,我们将探讨函数的革命性变化——箭头函数。它将如何简化你的代码并解决 this
的大麻烦?敬请期待!