- 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对象的属性 | 不会 | 不会 | 
| 可以重复声明 | 不可以重复声明 | 不可以重复声明 | 
| 可以条件式声明 | 不可以条件式声明 | 不可以条件式声明 | 
| 迭代变量保存的是退出循环的值 | 每次迭代声明一个独立实例 | 报错,因为值不可改变 |