let
const
const
和不可变性const
和循环const
和 let
之间做出选择globalThis
[ES2020]const
和 let
:暂时性死区var
:变量提升(部分提前激活)这些是 JavaScript 中声明变量的主要方式
在 ES6 之前,还有 var
。但它有一些怪癖,所以在现代 JavaScript 中最好避免使用它。您可以在 Speaking JavaScript 中阅读有关它的更多信息。
let
通过 let
声明的变量是可变的
let i;
= 0;
i = i + 1;
i .equal(i, 1); assert
您也可以同时声明和赋值
let i = 0;
const
通过 const
声明的变量是不可变的。您必须始终立即初始化
const i = 0; // must initialize
.throws(
assert=> { i = i + 1 },
()
{name: 'TypeError',
message: 'Assignment to constant variable.',
}; )
const
和不可变性在 JavaScript 中,const
仅表示绑定(变量名和变量值之间的关联)是不可变的。值本身可能是可变的,如下例中的 obj
。
const obj = { prop: 0 };
// Allowed: changing properties of `obj`
.prop = obj.prop + 1;
obj.equal(obj.prop, 1);
assert
// Not allowed: assigning to `obj`
.throws(
assert=> { obj = {} },
()
{name: 'TypeError',
message: 'Assignment to constant variable.',
}; )
const
和循环您可以将 const
与 for-of
循环一起使用,其中每次迭代都会创建一个新的绑定
const arr = ['hello', 'world'];
for (const elem of arr) {
console.log(elem);
}// Output:
// 'hello'
// 'world'
但是,在普通的 for
循环中,您必须使用 let
const arr = ['hello', 'world'];
for (let i=0; i<arr.length; i++) {
const elem = arr[i];
console.log(elem);
}
const
和 let
之间做出选择我建议使用以下规则来决定使用 const
还是 let
const
表示不可变绑定,并且变量永远不会改变其值。优先使用它。let
表示变量的值会发生变化。仅当您不能使用 const
时才使用它。 练习:const
exercises/variables-assignment/const_exrc.mjs
变量的作用域是程序中可以访问它的区域。请考虑以下代码。
// // Scope A. Accessible: x
{ const x = 0;
.equal(x, 0);
assert// Scope B. Accessible: x, y
{ const y = 1;
.equal(x, 0);
assert.equal(y, 1);
assert// Scope C. Accessible: x, y, z
{ const z = 2;
.equal(x, 0);
assert.equal(y, 1);
assert.equal(z, 2);
assert
}
}
}// Outside. Not accessible: x, y, z
.throws(
assert=> console.log(x),
()
{name: 'ReferenceError',
message: 'x is not defined',
}; )
x
的(直接)作用域。每个变量都可以在其直接作用域和嵌套在该作用域内的所有作用域中访问。
通过 const
和 let
声明的变量称为块级作用域,因为它们的作用域始终是最内层的包围块。
您不能在同一级别两次声明同一个变量
.throws(
assert=> {
() eval('let x = 1; let x = 2;');
,
}
{name: 'SyntaxError',
message: "Identifier 'x' has already been declared",
; })
为什么要使用 eval()
?
eval()
会延迟解析(以及 SyntaxError
),直到执行 assert.throws()
的回调函数。如果我们不使用它,那么在解析此代码时,我们已经会收到一个错误,并且 assert.throws()
甚至不会被执行。
但是,您可以嵌套一个块,并使用与块外部使用的变量名 x
相同的变量名
const x = 1;
.equal(x, 1);
assert
{const x = 2;
.equal(x, 2);
assert
}.equal(x, 1); assert
在块内部,内部 x
是唯一可以访问的具有该名称的变量。据说内部 x
遮蔽了外部 x
。一旦您离开块,您就可以再次访问旧值。
测验:基础
请参阅 测验应用程序。
所有剩余部分均为进阶内容。
这两个形容词描述了编程语言中的现象
让我们看看这两个术语的示例。
变量作用域是一种静态现象。请考虑以下代码
function f() {
const x = 3;
// ···
}
x
是静态(或词法)作用域的。也就是说,它的作用域是固定的,并且在运行时不会改变。
变量作用域形成一个静态树(通过静态嵌套)。
函数调用是一种动态现象。请考虑以下代码
function g(x) {}
function h(y) {
if (Math.random()) g(y); // (A)
}
A 行中的函数调用是否发生,只能在运行时决定。
函数调用形成一个动态树(通过动态调用)。
JavaScript 的变量作用域是嵌套的。它们形成一棵树
根也称为全局作用域。在 Web 浏览器中,唯一可以直接位于该作用域中的位置是脚本的顶层。全局作用域的变量称为全局变量,并且可以在任何地方访问。全局变量有两种
const
、let
和类声明创建。var
和函数声明创建的。globalThis
访问全局对象。它可以用来创建、读取和删除全局对象变量。以下 HTML 代码段演示了 globalThis
和两种全局变量。
<script>
const declarativeVariable = 'd';
var objectVariable = 'o';
</script>
<script>
// All scripts share the same top-level scope:
console.log(declarativeVariable); // 'd'
console.log(objectVariable); // 'o'
// Not all declarations create properties of the global object:
console.log(globalThis.declarativeVariable); // undefined
console.log(globalThis.objectVariable); // 'o'
</script>
每个 ECMAScript 模块都有自己的作用域。因此,存在于模块顶层的变量不是全局变量。图 5 说明了各种作用域之间的关系。
globalThis
[ES2020]全局变量 globalThis
是访问全局对象的新标准方法。它的名字来源于它在全局作用域中与 this
具有相同的值。
globalThis
并不总是直接指向全局对象
例如,在浏览器中,存在间接关系。这种间接关系通常不明显,但它确实存在,并且可以被观察到。
globalThis
的替代方案访问全局对象的旧方法取决于平台
window
:是引用全局对象的经典方法。但它在 Node.js 和 Web Workers 中不起作用。self
:在 Web Workers 和浏览器中普遍可用。但 Node.js 不支持它。global
:仅在 Node.js 中可用。globalThis
的用例全局对象现在被认为是 JavaScript 由于向后兼容性而无法摆脱的一个错误。它会对性能产生负面影响,并且通常会造成混淆。
ECMAScript 6 引入了一些功能,可以更容易地避免使用全局对象,例如
const
、let
和类声明在全局作用域中使用时不会创建全局对象属性。通常最好通过变量而不是通过 globalThis
的属性来访问全局对象变量。前者在所有 JavaScript 平台上的工作方式始终相同。
网络上的教程偶尔会通过 window.globVar
访问全局变量 globVar
。但前缀“window.
”不是必需的,我建议省略它
window.encodeURIComponent(str); // no
encodeURIComponent(str); // yes
因此,globalThis
的用例相对较少,例如
这是声明的两个关键方面
表 1 总结了各种声明如何处理这些方面。
作用域 | 激活 | 重复项 | 全局属性 | |
---|---|---|---|---|
const |
块 | 声明 (TDZ) | ✘ |
✘ |
let |
块 | 声明 (TDZ) | ✘ |
✘ |
function |
块 (*) | 开始 | ✔ |
✔ |
class |
块 | 声明 (TDZ) | ✘ |
✘ |
import |
模块 | 与 export 相同 | ✘ |
✘ |
var |
函数 | 开始,部分 | ✔ |
✔ |
import
在 §27.5 “ECMAScript 模块” 中进行了描述。以下部分将更详细地描述其他结构。
const
和 let
:暂时性死区对于 JavaScript,TC39 需要决定如果您在常量的声明之前在其直接作用域中访问它会发生什么
{console.log(x); // What happens here?
const x;
}
一些可能的方法是
undefined
。方法 1 被拒绝,因为该语言中没有这种方法的先例。因此,对于 JavaScript 程序员来说,这并不直观。
方法 2 被拒绝,因为这样 x
就不是常量了——它在声明之前和之后会有不同的值。
let
使用与 const
相同的方法 3,以便两者工作方式类似,并且易于在它们之间切换。
从进入变量的作用域到执行其声明之间的时间称为该变量的暂时性死区 (TDZ)
ReferenceError
。undefined
(如果没有初始化器)。以下代码说明了时间死区
if (true) { // entering scope of `tmp`, TDZ starts
// `tmp` is uninitialized:
.throws(() => (tmp = 'abc'), ReferenceError);
assert.throws(() => console.log(tmp), ReferenceError);
assert
let tmp; // TDZ ends
.equal(tmp, undefined);
assert }
下一个例子表明时间死区确实是与时间相关的
if (true) { // entering scope of `myVar`, TDZ starts
const func = () => {
console.log(myVar); // executed later
;
}
// We are within the TDZ:
// Accessing `myVar` causes `ReferenceError`
let myVar = 3; // TDZ ends
func(); // OK, called outside TDZ
}
即使 func()
位于 myVar
声明之前并使用了该变量,我们也可以调用 func()
。但我们必须等到 myVar
的时间死区结束。
更多关于函数的信息
在本节中,我们将在正确学习函数之前使用它们。希望一切仍然有意义。如果遇到问题,请参阅 §25 “可调用值”。
函数声明总是在进入其作用域时执行,而不管它在该作用域内的位置。这使您能够在声明函数 foo()
之前调用它
.equal(foo(), 123); // OK
assertfunction foo() { return 123; }
foo()
的提前激活意味着前面的代码等同于
function foo() { return 123; }
.equal(foo(), 123); assert
如果通过 const
或 let
声明函数,则不会提前激活它。在以下示例中,您只能在其声明后使用 bar()
。
.throws(
assert=> bar(), // before declaration
() ReferenceError);
const bar = () => { return 123; };
.equal(bar(), 123); // after declaration assert
即使函数 g()
没有被提前激活,如果我们遵守以下规则,它也可以被前面的函数 f()
(在相同的作用域内)调用:f()
必须在 g()
声明之后被调用。
const f = () => g();
const g = () => 123;
// We call f() after g() was declared:
.equal(f(), 123); assert
模块的函数通常在其完整主体执行后被调用。因此,在模块中,您很少需要担心函数的顺序。
最后,请注意提前激活如何自动遵守上述规则:进入作用域时,所有函数声明都会先执行,然后再进行任何调用。
如果您依赖提前激活在声明之前调用函数,那么您需要注意它不会访问未提前激活的数据。
funcDecl();
const MY_STR = 'abc';
function funcDecl() {
.throws(
assert=> MY_STR,
() ReferenceError);
}
如果在 MY_STR
声明之后调用 funcDecl()
,问题就会消失。
我们已经看到提前激活有一个陷阱,并且您可以在不使用它的情况下获得它的大部分好处。因此,最好避免提前激活。但我对此并不强烈,而且如前所述,我经常使用函数声明,因为我喜欢它们的语法。
尽管它们在某些方面类似于函数声明,但 类声明 不会被提前激活
.throws(
assert=> new MyClass(),
() ReferenceError);
class MyClass {}
.equal(new MyClass() instanceof MyClass, true); assert
为什么会这样?请考虑以下类声明
class MyClass extends Object {}
extends
的操作数是一个表达式。因此,您可以执行以下操作
const identity = x => x;
class MyClass extends identity(Object) {}
对这样的表达式的求值必须在其被提及的位置进行。其他任何事情都会令人困惑。这就解释了为什么类声明不会被提前激活。
var
:变量提升(部分提前激活)var
是一种比 const
和 let
(现在是首选)更古老的变量声明方式。请考虑以下 var
声明。
var x = 123;
此声明有两个部分
var x
:var
声明的变量的作用域是最内层的函数,而不是像大多数其他声明那样是最内层的块。这样的变量在其作用域的开头就已经处于活动状态,并初始化为 undefined
。x = 123
:赋值始终在原地执行。以下代码演示了 var
的效果
function f() {
// Partial early activation:
.equal(x, undefined);
assertif (true) {
var x = 123;
// The assignment is executed in place:
.equal(x, 123);
assert
}// Scope is function, not block:
.equal(x, 123);
assert }
在我们探索闭包之前,我们需要了解绑定变量和自由变量。
每个作用域都有一组被提及的变量。在这些变量中,我们区分
请考虑以下代码
function func(x) {
const y = 123;
console.log(z);
}
在 func()
的函数体中,x
和 y
是绑定变量。z
是一个自由变量。
那么什么是闭包呢?
闭包是一个函数加上与其“诞生之地”存在的变量的连接。
保持这种连接的意义何在?它为函数的自由变量提供了值——例如
function funcFactory(value) {
return () => {
return value;
;
}
}
const func = funcFactory('abc');
.equal(func(), 'abc'); // (A) assert
funcFactory
返回一个被赋值给 func
的闭包。因为 func
与其诞生之地的变量有连接,所以它在 A 行被调用时仍然可以访问自由变量 value
(即使它“逃离”了它的作用域)。
JavaScript 中的所有函数都是闭包
JavaScript 中的静态作用域是通过闭包来支持的。因此,每个函数都是一个闭包。
以下函数返回递增器(我刚编造的名称)。递增器是一个在内部存储一个数字的函数。当它被调用时,它会通过将参数添加到该数字来更新该数字,并返回新值。
function createInc(startValue) {
return (step) => { // (A)
+= step;
startValue return startValue;
;
}
}const inc = createInc(5);
.equal(inc(2), 7); assert
我们可以看到,在 A 行创建的函数将其内部数字保存在自由变量 startValue
中。这一次,我们不仅仅是从诞生作用域读取数据,我们还使用它来存储我们更改的数据,并且这些数据在函数调用之间持久存在。
我们可以通过局部变量在诞生作用域中创建更多存储空间
function createInc(startValue) {
let index = -1;
return (step) => {
+= step;
startValue ++;
indexreturn [index, startValue];
;
}
}const inc = createInc(5);
.deepEqual(inc(2), [0, 7]);
assert.deepEqual(inc(2), [1, 9]);
assert.deepEqual(inc(2), [2, 11]); assert
闭包有什么用?
首先,它们只是静态作用域的一种实现。因此,它们为回调函数提供上下文数据。
它们还可以被函数用来存储在函数调用之间持久化的状态。createInc()
就是一个例子。
它们还可以为对象(通过字面量或类生成)提供私有数据。有关其工作原理的详细信息,请参阅 探索 ES6。
测验:高级
请参阅 测验应用程序。