• ECMAScript变量是松散类型的,意思是变量可以用于保存任何类型的数据
  • 每个变量只不过是一个用于保存任意值的命名占位符
  • 有3个关键字可以声明变量:varconstlet
  • 其中,var在ES的所有版本中都可以使用,而constlet只能在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. 定义变量时,操作符的使用建议

  1. 不使用var

  2. const优先,let次之

这样可以让开发者更有信心地推断某些变量的值永远不会变,同时也能迅速发现因意外赋值导致的非预期行为

# 5. 总结

var let const
函数作用域 块级作用域 块级作用域
变量提升 不存在变量提升 不存在变量提升
值可更改 值可更改 值不可更改
全局声明会变成window对象的属性 不会 不会
可以重复声明 不可以重复声明 不可以重复声明
可以条件式声明 不可以条件式声明 不可以条件式声明
迭代变量保存的是退出循环的值 每次迭代声明一个独立实例 报错,因为值不可改变
上次更新: 2022/4/21 22:21:34