let
const
let
和 const
进行块级作用域const
创建不可变变量const
不会使值不可变const
var
声明的变量的生命周期let
声明的变量的生命周期typeof
会抛出 ReferenceError
let
和 const
for
循环for-of
循环和 for-in
循环const
与 let
与 var
ES6 提供了两种声明变量的新方法:let
和 const
,它们在很大程度上取代了 ES5 中声明变量的方式 var
。
let
let
的工作方式与 var
类似,但它声明的变量是块级作用域的,只存在于当前代码块中。var
是函数级作用域的。
在下面的代码中,您可以看到 let
声明的变量 tmp
只存在于从 A 行开始的代码块中
function
order
(
x
,
y
)
{
if
(
x
>
y
)
{
// (A)
let
tmp
=
x
;
x
=
y
;
y
=
tmp
;
}
console
.
log
(
tmp
===
x
);
// ReferenceError: tmp is not defined
return
[
x
,
y
];
}
const
const
的工作方式与 let
类似,但您声明的变量必须立即初始化,并且其值在之后不能更改。
const
foo
;
// SyntaxError: missing = in const declaration
const
bar
=
123
;
bar
=
456
;
// TypeError: `bar` is read-only
由于 for-of
为每次循环迭代创建一个绑定(变量的存储空间),因此可以使用 const
声明循环变量
for
(
const
x
of
[
'a'
,
'b'
])
{
console
.
log
(
x
);
}
// Output:
// a
// b
下表概述了 ES6 中声明变量的六种方式(灵感来自 kangax 的表格)
提升 | 作用域 | 创建全局属性 | |
---|---|---|---|
var |
声明 | 函数 | 是 |
let |
暂时性死区 | 块级 | 否 |
const |
暂时性死区 | 块级 | 否 |
函数 |
完整 | 块级 | 是 |
类 |
否 | 块级 | 否 |
导入 |
完整 | 模块全局 | 否 |
let
和 const
进行块级作用域 let
和 const
都创建了块级作用域的变量——它们只存在于包围它们的代码块中。下面的代码演示了 const
声明的变量 tmp
只存在于 if
语句的代码块中
function
func
()
{
if
(
true
)
{
const
tmp
=
123
;
}
console
.
log
(
tmp
);
// ReferenceError: tmp is not defined
}
相反,var
声明的变量是函数级作用域的
function
func
()
{
if
(
true
)
{
var
tmp
=
123
;
}
console
.
log
(
tmp
);
// 123
}
块级作用域意味着您可以在函数中遮蔽变量
function
func
()
{
const
foo
=
5
;
if
(
···
)
{
const
foo
=
10
;
// shadows outer `foo`
console
.
log
(
foo
);
// 10
}
console
.
log
(
foo
);
// 5
}
const
创建不可变变量 由 let
创建的变量是可变的
let
foo
=
'abc'
;
foo
=
'def'
;
console
.
log
(
foo
);
// def
常量,即由 const
创建的变量,是不可变的——您不能为它们分配不同的值
const
foo
=
'abc'
;
foo
=
'def'
;
// TypeError
const
不会使值不可变 const
仅表示变量始终具有相同的值,但这并不意味着值本身是或将变为不可变的。例如,obj
是一个常量,但它指向的值是可变的——我们可以向其中添加属性
const
obj
=
{};
obj
.
prop
=
123
;
console
.
log
(
obj
.
prop
);
// 123
但是,我们不能为 obj
分配不同的值
obj
=
{};
// TypeError
如果您希望 obj
的值不可变,则必须自己处理。例如,通过 冻结它
const
obj
=
Object
.
freeze
({});
obj
.
prop
=
123
;
// TypeError
Object.freeze()
是浅层的 请记住,Object.freeze()
是浅层的,它只冻结其参数的属性,而不冻结存储在其属性中的对象。例如,对象 obj
被冻结
>
const
obj
=
Object
.
freeze
(
{
foo
:
{
}
}
);
>
obj
.
bar
=
123
TypeError
:
Can
't add property bar, object is not extensible
> obj.foo = {}
TypeError: Cannot assign to read only property '
foo
'
of
#
<
Object
>
但对象 obj.foo
没有。
> obj.foo.qux = 'abc';
> obj.foo.qux
'abc'
const
一旦创建了 const
变量,就不能更改它。但这并不意味着您不能重新进入其作用域并以新值重新开始。例如,通过循环
function
logArgs
(...
args
)
{
for
(
const
[
index
,
elem
]
of
args
.
entries
())
{
// (A)
const
message
=
index
+
'. '
+
elem
;
// (B)
console
.
log
(
message
);
}
}
logArgs
(
'Hello'
,
'everyone'
);
// Output:
// 0. Hello
// 1. everyone
此代码中有两个 const
声明,分别在 A 行和 B 行。在每次循环迭代期间,它们的常量具有不同的值。
由 let
或 const
声明的变量有一个所谓的暂时性死区 (TDZ):进入其作用域时,在执行到达声明之前,无法访问(获取或设置)它。让我们比较一下 var
声明的变量(没有 TDZ)和 let
声明的变量(有 TDZ)的生命周期。
var
声明的变量的生命周期 var
变量没有暂时性死区。它们的生命周期包括以下步骤
var
变量的作用域(其周围的函数)时,会为其创建存储空间(绑定)。该变量立即被初始化,并设置为 undefined
。undefined
。let
声明的变量的生命周期 通过 let
声明的变量具有暂时性死区,它们的生命周期如下所示
let
变量的作用域(其周围的代码块)时,会为其创建存储空间(绑定)。该变量保持未初始化状态。ReferenceError
。undefined
。const
变量的工作方式与 let
变量类似,但它们必须具有初始化程序(即立即设置为一个值),并且不能更改。
在 TDZ 中,如果获取或设置变量,则会抛出异常
let
tmp
=
true
;
if
(
true
)
{
// enter new scope, TDZ starts
// Uninitialized binding for `tmp` is created
console
.
log
(
tmp
);
// ReferenceError
let
tmp
;
// TDZ ends, `tmp` is initialized with `undefined`
console
.
log
(
tmp
);
// undefined
tmp
=
123
;
console
.
log
(
tmp
);
// 123
}
console
.
log
(
tmp
);
// true
如果存在初始化程序,则在评估初始化程序并将结果分配给变量之后,TDZ 结束
let
foo
=
console
.
log
(
foo
);
// ReferenceError
以下代码演示了死区确实是暂时的(基于时间)而不是空间的(基于位置)
if
(
true
)
{
// enter new scope, TDZ starts
const
func
=
function
()
{
console
.
log
(
myVar
);
// OK!
};
// Here we are within the TDZ and
// accessing `myVar` would cause a `ReferenceError`
let
myVar
=
3
;
// TDZ ends
func
();
// called outside TDZ
}
typeof
会抛出 ReferenceError
如果您通过 typeof
访问暂时性死区中的变量,则会收到异常
if
(
true
)
{
console
.
log
(
typeof
foo
);
// ReferenceError (TDZ)
console
.
log
(
typeof
aVariableThatDoesntExist
);
// 'undefined'
let
foo
;
}
为什么?理由如下:foo
不是未声明的,而是未初始化的。您应该知道它的存在,但您不知道。因此,发出警告似乎是可取的。
此外,这种检查仅对有条件地创建全局变量有用。这不是您在普通程序中需要做的事情。
当需要有条件地创建变量时,您有两个选择。
选项 1 - typeof
和 var
if
(
typeof
someGlobal
===
'undefined'
)
{
var
someGlobal
=
{
···
};
}
此选项仅在全局作用域中有效(因此在 ES6 模块中无效)。
选项 2 - window
if
(
!
(
'someGlobal'
in
window
))
{
window
.
someGlobal
=
{
···
};
}
const
和 let
具有暂时性死区有几个原因
const
:使 const
正常工作很困难。引用 Allen Wirfs-Brock 的话:“TDZ……为 const
提供了合理的语义。关于该主题进行了大量的技术讨论,TDZ 被认为是最佳解决方案。” let
也有一个暂时性死区,因此在 let
和 const
之间切换不会以意想不到的方式改变行为。undefined
,则该值可能与其防护提供的保证相冲突。本节的来源
let
和 const
以下循环允许您在其头部声明变量
for
for-in
for-of
要进行声明,您可以使用 var
、let
或 const
。它们各自具有不同的效果,我将在接下来解释。
for
循环 在 for
循环的头部使用 var
声明变量会为该变量创建一个绑定(存储空间)
const
arr
=
[];
for
(
var
i
=
0
;
i
<
3
;
i
++
)
{
arr
.
push
(()
=>
i
);
}
arr
.
map
(
x
=>
x
());
// [3,3,3]
三个箭头函数体中的每个 i
都引用相同的绑定,这就是它们都返回相同值的原因。
如果您使用 let
声明变量,则每次循环迭代都会创建一个新的绑定
const
arr
=
[];
for
(
let
i
=
0
;
i
<
3
;
i
++
)
{
arr
.
push
(()
=>
i
);
}
arr
.
map
(
x
=>
x
());
// [0,1,2]
这一次,每个 i
都引用一个特定迭代的绑定,并保留当时的值。因此,每个箭头函数都返回不同的值。
const
的工作方式与 var
类似,但您不能更改 const
声明的变量的初始值
// TypeError: Assignment to constant variable
// (due to i++)
for (const i=0; i<3; i++) {
console.log(i);
}
对于每次迭代都获得一个新的绑定,乍一看可能很奇怪,但当您使用循环创建引用循环变量的函数时,这非常有用,如后面的章节中所述。
for-of
循环和 for-in
循环 在 for-of
循环中,var
创建一个单一绑定
const
arr
=
[];
for
(
var
i
of
[
0
,
1
,
2
])
{
arr
.
push
(()
=>
i
);
}
arr
.
map
(
x
=>
x
());
// [2,2,2]
const
为每次迭代创建一个不可变绑定
const
arr
=
[];
for
(
const
i
of
[
0
,
1
,
2
])
{
arr
.
push
(()
=>
i
);
}
arr
.
map
(
x
=>
x
());
// [0,1,2]
let
也为每次迭代创建一个绑定,但它创建的绑定是可变的。
for-in
循环的工作方式类似于 for-of
循环。
以下是一个显示三个链接的 HTML 页面
<!
doctype
html
>
<
html
>
<
head
>
<
meta
charset
=
"UTF-8"
>
<
/head>
<
body
>
<
div
id
=
"content"
><
/div>
<
script
>
const
entries
=
[
[
'yes'
,
'ja'
],
[
'no'
,
'nein'
],
[
'perhaps'
,
'vielleicht'
],
];
const
content
=
document
.
getElementById
(
'content'
);
for
(
const
[
source
,
target
]
of
entries
)
{
// (A)
content
.
insertAdjacentHTML
(
'beforeend'
,
`<div><a id="
${
source
}
" href="">
${
source
}
</a></div>`
);
document
.
getElementById
(
source
).
addEventListener
(
'click'
,
(
event
)
=>
{
event
.
preventDefault
();
alert
(
target
);
// (B)
});
}
<
/script>
<
/body>
<
/html>
显示的内容取决于变量 target
(第 B 行)。如果我们在 A 行使用 var
而不是 const
,则整个循环将只有一个绑定,并且 target
的值将是 'vielleicht'
。因此,无论您点击哪个链接,您总是会得到翻译 'vielleicht'
。
幸运的是,使用 const
,我们为每个循环迭代获得一个绑定,并且翻译显示正确。
如果您使用 let
声明一个与参数同名的变量,您将收到一个静态(加载时)错误
function
func
(
arg
)
{
let
arg
;
// static error: duplicate declaration of `arg`
}
在块内执行相同的操作会遮蔽参数
function
func
(
arg
)
{
{
let
arg
;
// shadows parameter `arg`
}
}
相反,使用 var
声明一个与参数同名的变量不会做任何事情,就像在同一作用域内重新声明一个 var
变量不会做任何事情一样。
function
func
(
arg
)
{
var
arg
;
// does nothing
}
function
func
(
arg
)
{
{
// We are still in same `var` scope as `arg`
var
arg
;
// does nothing
}
}
如果参数具有默认值,则它们将被视为一系列 let
语句,并受时间死区的约束
// OK: `y` accesses `x` after it has been declared
function
foo
(
x
=
1
,
y
=
x
)
{
return
[
x
,
y
];
}
foo
();
// [1,1]
// Exception: `x` tries to access `y` within TDZ
function
bar
(
x
=
y
,
y
=
2
)
{
return
[
x
,
y
];
}
bar
();
// ReferenceError
参数默认值的作用域与函数体的作用域是分开的(前者包围后者)。这意味着在参数默认值“内部”定义的方法或函数看不到函数体的局部变量
const
foo
=
'outer'
;
function
bar
(
func
=
x
=>
foo
)
{
const
foo
=
'inner'
;
console
.
log
(
func
());
// outer
}
bar
();
JavaScript 的 全局对象(Web 浏览器中的 window
,Node.js 中的 global
)与其说是一个特性,不如说是一个错误,尤其是在性能方面。这就是为什么 ES6 引入一个区别是有意义的
var
声明let
声明const
声明请注意,模块的函数体不是在全局作用域中执行的,只有脚本是在全局作用域中执行的。因此,各种变量的环境形成了以下链。
函数声明…
let
一样是块级作用域的。var
一样。以下代码演示了函数声明的提升
{
// Enter a new scope
console
.
log
(
foo
());
// OK, due to hoisting
function
foo
()
{
return
'hello'
;
}
}
类声明…
类不被提升可能会令人惊讶,因为在底层,它们会创建函数。这种行为的基本原理是,它们的 extends
子句的值是通过表达式定义的,而这些表达式必须在适当的时候执行。
{
// Enter a new scope
const
identity
=
x
=>
x
;
// Here we are in the temporal dead zone of `MyClass`
const
inst
=
new
MyClass
();
// ReferenceError
// Note the expression in the `extends` clause
class
MyClass
extends
identity
(
Object
)
{
}
}
const
与 let
与 var
我建议始终使用 let
或 const
const
。只要变量的值永远不会改变,您就可以使用它。换句话说:变量永远不应该出现在赋值语句的左侧或 ++
或 --
的操作数中。允许更改 const
变量引用的对象
const
foo
=
{};
foo
.
prop
=
123
;
// OK
您甚至可以在 for-of
循环中使用 const
,因为每次循环迭代都会创建一个(不可变的)绑定
for
(
const
x
of
[
'a'
,
'b'
])
{
console
.
log
(
x
);
}
// Output:
// a
// b
在 for-of
循环的函数体内,不能更改 x
。
let
- 当变量的初始值稍后发生更改时。
let
counter
=
0
;
// initial value
counter
++
;
// change
let
obj
=
{};
// initial value
obj
=
{
foo
:
123
};
// change
var
。如果您遵循这些规则,var
将只出现在遗留代码中,作为需要仔细重构的信号。
var
做了一件 let
和 const
做不到的事情:通过它声明的变量会成为全局对象的属性。然而,这通常不是一件好事。您可以通过赋值给 window
(在浏览器中)或 global
(在 Node.js 中)来达到相同的效果。
上述风格规则的另一种方法是,仅对完全不可变的事物(原始值和冻结对象)使用 const
。然后我们有两种方法
const
: const
标记不可变的绑定。let
: const
标记不可变的值。我稍微倾向于#1,但#2 也可以。