- ECMAScript变量是松散类型的,意思是变量可以用于保存任何类型的数据
- 每个变量只不过是一个用于保存任意值的命名占位符
- 有3个关键字可以声明变量:
var
、const
和let
- 其中,
var
在ES的所有版本中都可以使用,而const
和let
只能在ES6及更晚的版本中使用
# 1. var
var message;
定义了一个名为message 的变量,可以用它保存任何类型的值,不初始化的情况下,变量会保存一个特殊值undefined
# 1.1 var 声明作用域
- 使用var 操作符定义的变量会成为包含它的函数的局部变量
# 1.2 var 声明提升
- 使用var关键字声明的变量会自动提升(hoist)到函数作用域顶部
function foo() {
console.log(age);
var age = 26;
}
foo(); // undefined
等价于
function foo() {
var age;
console.log(age);
age = 26;
}
foo(); // undefined
- 这就是所谓的“提升”(hoist),也就是把所有变量声明都拉到函数作用域的顶部。
- 反复多次使用var声明同一个变量也没有问题
# 2. let(注意与var的区别)
# 2.1 let 声明作用域(块级作用域)
- 与var最明显的区别是,let 声明的范围是块作用域,而var声明的范围是函数作用域
- let 不允许同一个块作用域中出现冗余声明
if (true) {
let a = 10;
}
console.log(a) // a is not defined
- 块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。
// IIFE 写法
(function () {
var tmp = ...;
...
}());
// 块级作用域写法
{
let tmp = ...;
...
}
# 2.2 暂时性死区
- let与var的另一个重要的区别,就是let声明的变量不会在作用域中被提升
- 在解析代码时,JavaScript 引擎也会注意出现在块后面的let 声明,只不过在此之前不能以任何方式来引用未声明的变量
- 在let声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出 ReferenceError
- 总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。
var num = 10;
if(true){
console.log(num); // Uncaught ReferenceError: Cannot access 'num' before initialization
let num = 20;
}
# 仔细解释一下暂时性死区
- 只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
- ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。
- 凡是在声明之前就使用这些变量,就会报错。
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
- “暂时性死区”也意味着typeof不再是一个百分之百安全的操作。(在没有let之前,typeof运算符是百分之百安全的,永远不会报错)
typeof x; // ReferenceError
let x;
typeof undeclared_variable // "undefined"
// 不报错
var x = x;
// 报错
let x = x;
// ReferenceError: x is not defined
- 总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量
# 2.3 全局声明
- 与var 关键字不同,使用let 在全局作用域中声明的变量不会成为window 对象的属性(var 声明的变量则会)
- 不过,let声明仍然是在全局作用域中发生的,相应变量会在页面的生命周期内存续
# 2.4 条件声明
- 因为let的作用域是块,所以不可能检查前面是否已经使用let声明过同名变量,同时也就不可能在没有声明的情况下声明它
<script>
var name = 'Nicholas';
let age = 26;
</script>
<script>
// 假设脚本不确定页面中是否已经声明了同名变量
// 那它可以假设还没有声明过
var name = 'Matt';
// 这里没问题,因为可以被作为一个提升声明来处理
let age = 36;
// 如果age之前声明过,这里会报错
</script>
<script>
let name = 'Nicholas';
let age = 36;
</script>
<script>
if (typeof name === 'undefined') {
let name; // name 被限制在if {} 块的作用域内
}
// 因此这个赋值形同全局赋值
name = 'Matt';
try {
console.log(age); // 如果age 没有声明过,则会报错
}
catch(error) {
let age; // age 被限制在catch {}块的作用域内
}
// 因此这个赋值形同全局赋值
age = 26;
</script>
- 不能使用let进行条件式声明
# 2.4 for 循环中的let声明
for (var i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // 5
for (let i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // ReferenceError: i 没有定义
for (var i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
}
// 你可能以为会输出0、1、2、3、4
// 实际上会输出5、5、5、5、5
for (let i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
}
// 会输出0、1、2、3、4
- 另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
- 上面代码正确运行,输出了 3 次abc。
- 这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。
# 3. const
- const 的行为与let基本相同(1. 块级作用域),唯一一个重要的区别是用它声明变量时必须同时初始化变量(2. 初始化必须赋值),且尝试修改const声明的变量会导致运行时错误(3. 不可修改)。
- const 声明的限制只适用于它指向的变量的引用。如果const变量引用的是一个对象,那么修改这个对象内部的属性并不违反const的限制。
# 4. 定义变量时,操作符的使用建议
不使用var
const优先,let次之
这样可以让开发者更有信心地推断某些变量的值永远不会变,同时也能迅速发现因意外赋值导致的非预期行为
# 5. 总结
var | let | const |
---|---|---|
函数作用域 | 块级作用域 | 块级作用域 |
变量提升 | 不存在变量提升 | 不存在变量提升 |
值可更改 | 值可更改 | 值不可更改 |
全局声明会变成window对象的属性 | 不会 | 不会 |
可以重复声明 | 不可以重复声明 | 不可以重复声明 |
可以条件式声明 | 不可以条件式声明 | 不可以条件式声明 |
迭代变量保存的是退出循环的值 | 每次迭代声明一个独立实例 | 报错,因为值不可改变 |