9. 变量和作用域
9.1 概述
ES6 提供了两种声明变量的新方法:let
和 const
,它们在很大程度上取代了 ES5 中声明变量的方式 var
。
9.1.1 let
let
的工作方式与 var
类似,但它声明的变量是块级作用域的,只存在于当前代码块中。var
是函数级作用域的。
在下面的代码中,您可以看到 let
声明的变量 tmp
只存在于从 A 行开始的代码块中
9.1.2 const
const
的工作方式与 let
类似,但您声明的变量必须立即初始化,并且其值在之后不能更改。
由于 for-of
为每次循环迭代创建一个绑定(变量的存储空间),因此可以使用 const
声明循环变量
9.1.3 声明变量的方式
下表概述了 ES6 中声明变量的六种方式(灵感来自 kangax 的表格)
|
提升 |
作用域 |
创建全局属性 |
var |
声明 |
函数 |
是 |
let |
暂时性死区 |
块级 |
否 |
const |
暂时性死区 |
块级 |
否 |
函数 |
完整 |
块级 |
是 |
类 |
否 |
块级 |
否 |
导入 |
完整 |
模块全局 |
否 |
9.2 通过 let
和 const
进行块级作用域
let
和 const
都创建了块级作用域的变量——它们只存在于包围它们的代码块中。下面的代码演示了 const
声明的变量 tmp
只存在于 if
语句的代码块中
相反,var
声明的变量是函数级作用域的
块级作用域意味着您可以在函数中遮蔽变量
9.3 const
创建不可变变量
由 let
创建的变量是可变的
常量,即由 const
创建的变量,是不可变的——您不能为它们分配不同的值
9.3.1 陷阱:const
不会使值不可变
const
仅表示变量始终具有相同的值,但这并不意味着值本身是或将变为不可变的。例如,obj
是一个常量,但它指向的值是可变的——我们可以向其中添加属性
但是,我们不能为 obj
分配不同的值
如果您希望 obj
的值不可变,则必须自己处理。例如,通过 冻结它
9.3.1.1 陷阱:Object.freeze()
是浅层的
请记住,Object.freeze()
是浅层的,它只冻结其参数的属性,而不冻结存储在其属性中的对象。例如,对象 obj
被冻结
但对象 obj.foo
没有。
9.3.2 循环体中的 const
一旦创建了 const
变量,就不能更改它。但这并不意味着您不能重新进入其作用域并以新值重新开始。例如,通过循环
此代码中有两个 const
声明,分别在 A 行和 B 行。在每次循环迭代期间,它们的常量具有不同的值。
9.4 暂时性死区
由 let
或 const
声明的变量有一个所谓的暂时性死区 (TDZ):进入其作用域时,在执行到达声明之前,无法访问(获取或设置)它。让我们比较一下 var
声明的变量(没有 TDZ)和 let
声明的变量(有 TDZ)的生命周期。
9.4.1 var
声明的变量的生命周期
var
变量没有暂时性死区。它们的生命周期包括以下步骤
- 当进入
var
变量的作用域(其周围的函数)时,会为其创建存储空间(绑定)。该变量立即被初始化,并设置为 undefined
。
- 当作用域内的执行到达声明时,该变量将设置为初始化程序(赋值)指定的值——如果存在的话。如果不存在,则变量的值保持为
undefined
。
9.4.2 let
声明的变量的生命周期
通过 let
声明的变量具有暂时性死区,它们的生命周期如下所示
- 当进入
let
变量的作用域(其周围的代码块)时,会为其创建存储空间(绑定)。该变量保持未初始化状态。
- 获取或设置未初始化的变量会导致
ReferenceError
。
- 当作用域内的执行到达声明时,该变量将设置为初始化程序(赋值)指定的值——如果存在的话。如果不存在,则变量的值将设置为
undefined
。
const
变量的工作方式与 let
变量类似,但它们必须具有初始化程序(即立即设置为一个值),并且不能更改。
9.4.3 示例
在 TDZ 中,如果获取或设置变量,则会抛出异常
如果存在初始化程序,则在评估初始化程序并将结果分配给变量之后,TDZ 结束
以下代码演示了死区确实是暂时的(基于时间)而不是空间的(基于位置)
9.4.4 对于暂时性死区中的变量,typeof
会抛出 ReferenceError
如果您通过 typeof
访问暂时性死区中的变量,则会收到异常
为什么?理由如下:foo
不是未声明的,而是未初始化的。您应该知道它的存在,但您不知道。因此,发出警告似乎是可取的。
此外,这种检查仅对有条件地创建全局变量有用。这不是您在普通程序中需要做的事情。
9.4.4.1 有条件地创建变量
当需要有条件地创建变量时,您有两个选择。
选项 1 - typeof
和 var
此选项仅在全局作用域中有效(因此在 ES6 模块中无效)。
选项 2 - window
9.4.5 为什么会有暂时性死区?
const
和 let
具有暂时性死区有几个原因
- 捕获编程错误:能够在声明变量之前访问它是很奇怪的。如果您这样做,通常是偶然的,您应该收到有关它的警告。
- 对于
const
:使 const
正常工作很困难。引用 Allen Wirfs-Brock 的话:“TDZ……为 const
提供了合理的语义。关于该主题进行了大量的技术讨论,TDZ 被认为是最佳解决方案。” let
也有一个暂时性死区,因此在 let
和 const
之间切换不会以意想不到的方式改变行为。
- 面向未来的防护:JavaScript 最终可能会具有防护,这是一种在运行时强制变量具有正确值的机制(想想运行时类型检查)。如果变量的值在声明之前是
undefined
,则该值可能与其防护提供的保证相冲突。
9.4.6 扩展阅读
本节的来源
9.5 循环头中的 let
和 const
以下循环允许您在其头部声明变量
要进行声明,您可以使用 var
、let
或 const
。它们各自具有不同的效果,我将在接下来解释。
9.5.1 for
循环
在 for
循环的头部使用 var
声明变量会为该变量创建一个绑定(存储空间)
三个箭头函数体中的每个 i
都引用相同的绑定,这就是它们都返回相同值的原因。
如果您使用 let
声明变量,则每次循环迭代都会创建一个新的绑定
这一次,每个 i
都引用一个特定迭代的绑定,并保留当时的值。因此,每个箭头函数都返回不同的值。
const
的工作方式与 var
类似,但您不能更改 const
声明的变量的初始值
对于每次迭代都获得一个新的绑定,乍一看可能很奇怪,但当您使用循环创建引用循环变量的函数时,这非常有用,如后面的章节中所述。
9.5.2 for-of
循环和 for-in
循环
在 for-of
循环中,var
创建一个单一绑定
const
为每次迭代创建一个不可变绑定
let
也为每次迭代创建一个绑定,但它创建的绑定是可变的。
for-in
循环的工作方式类似于 for-of
循环。
9.5.3 为什么每次迭代绑定有用?
以下是一个显示三个链接的 HTML 页面
- 如果您点击“yes”,它会被翻译成“ja”。
- 如果您点击“no”,它会被翻译成“nein”。
- 如果您点击“perhaps”,它会被翻译成“vielleicht”。
显示的内容取决于变量 target
(第 B 行)。如果我们在 A 行使用 var
而不是 const
,则整个循环将只有一个绑定,并且 target
的值将是 'vielleicht'
。因此,无论您点击哪个链接,您总是会得到翻译 'vielleicht'
。
幸运的是,使用 const
,我们为每个循环迭代获得一个绑定,并且翻译显示正确。
9.6 参数作为变量
9.6.1 参数与局部变量
如果您使用 let
声明一个与参数同名的变量,您将收到一个静态(加载时)错误
在块内执行相同的操作会遮蔽参数
相反,使用 var
声明一个与参数同名的变量不会做任何事情,就像在同一作用域内重新声明一个 var
变量不会做任何事情一样。
9.6.2 参数默认值和时间死区
如果参数具有默认值,则它们将被视为一系列 let
语句,并受时间死区的约束
9.6.3 参数默认值看不到函数体的作用域
参数默认值的作用域与函数体的作用域是分开的(前者包围后者)。这意味着在参数默认值“内部”定义的方法或函数看不到函数体的局部变量
9.7 全局对象
JavaScript 的 全局对象(Web 浏览器中的 window
,Node.js 中的 global
)与其说是一个特性,不如说是一个错误,尤其是在性能方面。这就是为什么 ES6 引入一个区别是有意义的
- 全局对象的所有属性都是全局变量。在全局作用域中,以下声明会创建此类属性
- 但现在也有一些全局变量不是全局对象的属性。在全局作用域中,以下声明会创建此类变量
请注意,模块的函数体不是在全局作用域中执行的,只有脚本是在全局作用域中执行的。因此,各种变量的环境形成了以下链。
9.8 函数声明和类声明
函数声明…
- 像
let
一样是块级作用域的。
- 在全局对象中创建属性(在全局作用域中时),就像
var
一样。
- 会被_提升_:无论函数声明在其作用域中的哪个位置被提及,它总是在作用域的开头创建。
以下代码演示了函数声明的提升
类声明…
- 是块级作用域的。
- 不会在全局对象上创建属性。
- _不会_被提升。
类不被提升可能会令人惊讶,因为在底层,它们会创建函数。这种行为的基本原理是,它们的 extends
子句的值是通过表达式定义的,而这些表达式必须在适当的时候执行。
9.9 编码风格:const
与 let
与 var
我建议始终使用 let
或 const
- 优先使用
const
。只要变量的值永远不会改变,您就可以使用它。换句话说:变量永远不应该出现在赋值语句的左侧或 ++
或 --
的操作数中。允许更改 const
变量引用的对象
您甚至可以在 for-of
循环中使用 const
,因为每次循环迭代都会创建一个(不可变的)绑定
在 for-of
循环的函数体内,不能更改 x
。
- 否则,请使用
let
- 当变量的初始值稍后发生更改时。
- 避免使用
var
。
如果您遵循这些规则,var
将只出现在遗留代码中,作为需要仔细重构的信号。
var
做了一件 let
和 const
做不到的事情:通过它声明的变量会成为全局对象的属性。然而,这通常不是一件好事。您可以通过赋值给 window
(在浏览器中)或 global
(在 Node.js 中)来达到相同的效果。
9.9.1 另一种方法
上述风格规则的另一种方法是,仅对完全不可变的事物(原始值和冻结对象)使用 const
。然后我们有两种方法
-
优先使用
const
: const
标记不可变的绑定。
-
优先使用
let
: const
标记不可变的值。
我稍微倾向于#1,但#2 也可以。