letconstlet 和 const 进行块级作用域const 创建不可变变量const 不会使值不可变constvar 声明的变量的生命周期let 声明的变量的生命周期typeof 会抛出 ReferenceErrorlet 和 constfor 循环for-of 循环和 for-in 循环const 与 let 与 varES6 提供了两种声明变量的新方法: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 以下循环允许您在其头部声明变量
forfor-infor-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 也可以。