块级声明

块级声明也就是让所声明的变量在指定块的作用域外无法被访问。块级作用域(又被称为词法作用域)在如下情况被创建:

  1. 在一个函数内部

  2. 在一个代码块(由一对花括号包裹)内部

块级作用域是很多类 C 语言的工作机制,ES6 引入块级声明,是为了给 JS 添加灵活性以及与其他语言的一致性。

let声明

let 声明的语法与 var 的语法一致。你基本上可以用 let 来代替 var 进行变量声明, 但会将变量的作用域限制在当前代码块中(其他细微差别会在稍后讨论)。由于 let 声明并不会被提升到当前代码块的顶部,因此你需要手动将 let 声明放置到顶部,以便让变量在整个代码块内部可用。

let 声明:

  • let 声明和 var 声明的用法基本相同。

  • let 声明的变量不会被提升。

  • let 不能在同一个作用域中重复声明已经存在的变量,会报错。

  • let 声明的变量作用域范围仅存在于当前的块中,程序进入块开始时被创建,程序退出块时被销毁。

  • 全局作用域下使用 let 声明的变量不再挂载到 window 对象上。

这里有个范例:

function getValue (condition) {
    if (condition) {
        // 变量value只存在于这个块中。
        let value = 'value'
        return value
    } else {
        // 访问不到value变量
        console.log(value)
        return null
    }
}

如你所愿,这种写法的 getValue 函数的行为更接近其他类 C 语言。由于变量 value 声明使用的是 let 而非 var ,该声明就没有被提升到函数定义的顶部,因此变量 value 在 if 代码块外部是无法访问的;并且在 condition 的值为 false 时,该变量是永远不会被声明并初始化的。

禁止重复声明

如果一个标识符已经在代码块内部被定义,那么在此代码块内使用同一个标识符进行 let 声明就会导致抛出错误。例如:

var count = 30;
// 语法错误
let count = 40;

在本例中,count 变量被声明了两次:一次使用 var,另一次使用 let。因为 let 不能在同一作用域内重复声明一个已有标识符,此处的 let 声明就会抛出错误。另一方面,在嵌套的作用域内使用 let 声明一个同名的新变量,则不会抛出错误,以下代码对此进行了演示:

var count = 30;
// 不会抛出错误
if (condition) {
    let count = 40;
    // 其他代码
}

此处的 let 声明并没有抛出错误,这是因为它在 if 语句内部创建了一个新的 count 变量,而不是在同一级别再次创建此变量。在 if 代码块内部,这个新变量会屏蔽全局的 count 变量,从而在局部阻止对于后者的访问。

常量声明

在 ES6 中里也可以使用 const 语法进行声明。使用 const 声明的变量会被认为是常量(constant),意味着它们的值在被设置完成后就不能再被改变。正因为如此,所有的 const 变量都需要在声明时进行初始化,示例如下:

// 有效的常量
const maxItems = 30;
// 语法错误: 未进行初始化
const name;

maxItems 变量被初始化了,因此它的 const 声明能正常起效。而 name 变量没有被初始化,导致在试图运行这段代码时抛出了错误。

对比常量声明与let声明

常量声明与 let 声明一样,都是块级声明。这意味着常量在声明它们的语句块外部是无法访问的,并且声明也不会被提升,示例如下:

if (condition) {
    const maxItems = 5;
    // 其他代码
}
// maxItems 在此处无法访问

此代码中,常量 maxItems 在 if 语句内被声明。maxItems 在代码块外部无法被访问,因为该语句已结束执行。

与 let 的另一个相似之处,是 const 声明会在同一作用域( 全局或是函数作用域)内定义一个已有变量时会抛出错误,无论是该变量此前是用 var 声明的,还是用 let 声明的。例如以下代码:

var message = "Hello!";
let age = 25;

// 二者均会抛出错误
const message = "Goodbye!";
const age = 30;

两个 const 声明都可以单独使用,但在前面添加了 var 与 let 声明的情况下,二者都会出问题。

尽管有上述相似之处,但 let 与 const 之间仍然有个必须牢记的重大区别:试图对之前用 const 声明的常量进行赋值会抛出错误,无论是在严格模式还是非严格模式下:

const maxItems = 5;
maxItems = 6; // 抛出错误

与其他语言的常量类似,maxItems 变量不能被再次赋值。然而与其他语言不同,JS 的常量如果是一个对象,它所包含的值是可以被修改的。

使用const声明对象

const 声明会阻止对于变量绑定与变量自身值的修改,这意味着 const 声明并不会阻止对变量成员的修改。例如:

const person = {
    name: "Nicholas"
};

// 工作正常
person.name = "Greg";

// 抛出错误
person = {
    name: "Greg"
}

此处 person 在初始化时被绑定了带有一个属性的对象。修改 person.name 是可能的,并不会抛出错误,因为该操作只修改了 person 对象的成员,而没有修改 person 的绑定值。当代码试图为 person 对象自身赋值时(这会改变变量绑定),就会导致错误。 const 在变量上的微妙工作机制容易导致误解,但只需记住:const 阻止的是对于变量绑定的修改,而不阻止对成员值的修改。

我们说的const变量不可变,需要分两种类型来说:

  • 值类型:变量的值不能改变。

  • 引用类型:变量的地址不能改变,值可以改变。

暂时性死区

因为 let 和 const 声明的变量不会进行声明提升,所以在 let 和 const 变量声明之前任何访问(即使是 typeof 也不行)此变量的操作都会引发错误:

if (condition) {
    console.log(typeof value); // 引用错误
    let value = "blue";
}

此处的 value 变量使用了 let 进行定义与初始化,但该语句永远不会被执行,因为声明之前的那行代码抛出了一个错误。出现该问题是因为:value 位于被 JS 社区称为暂时性死区(temporal dead zone,TDZ)的区域内。该名称并未在 ECMAScript 规范中被明确命名,但经常被用于描述 let 或 const 声明的变量为何在声明处之前无法被访问。本小节的内容涵盖了暂时性死区所导致的声明位置的微妙之处,尽管这里使用的都是 let,但替换为 const 也会有相同情况。

当 JS 引擎检视接下来的代码块并发现变量声明时,它会在面对 var 的情况下将声明提升到函数或全局作用域的顶部,而面对 let 或 const 时会将声明放在暂时性死区内。任何在暂时性死区内访问变量的企图都会导致 “运行时” 错误(runtime error)。只有执行到变量的声明语句时,该变量才会从暂时性死区内被移除并可以安全使用。

使用 let 或 const 声明的变量,若试图在定义位置之前使用它,无论如何都不能避免暂时性死区。而且正如上例所演示的,这甚至影响了通常安全的 typeof 运算符。然而, 你可以在变量被定义的代码块之外对该变量使用 typeof,尽管其结果可能并非预期。考虑以下代码:

console.log(typeof value); // "undefined"
if (condition) {
    let value = "blue";
}

当 typeof 运算符被使用时,value 并没有在暂时性死区内,因为这发生在定义 value 变量的代码块外部。这意味着此时并没有绑定 value 变量,而 typeof 仅单纯返回了 "undefined"。

JavaScript 引擎在扫描代码发现变量声明时,要么将它们提升至作用域的顶部(var 声明),要么将声明放在 TDZ(暂时性死区)中(let 和 const 声明)。访问 TDZ 中的变量会触发错误,只有执行变量声明语句之后,变量才会从 TDZ 中移出,随后才能正常访问。

暂时性死区只是块级绑定的一个独特表现,而另一个独特表现则是在循环时使用它。