变量声明

letconst 是 JavaScript 中变量声明的两个相对较新的概念。正如我们前面提到的let 在某些方面与 var 相似,但允许用户避开一些用户在 JavaScript 中遇到的常见 “gotchas”。

¥let and const are two relatively new concepts for variable declarations in JavaScript. As we mentioned earlier, let is similar to var in some respects, but allows users to avoid some of the common “gotchas” that users run into in JavaScript.

constlet 的增强,因为它防止重新分配给变量。

¥const is an augmentation of let in that it prevents re-assignment to a variable.

由于 TypeScript 是 JavaScript 的扩展,该语言自然支持 letconst。在这里,我们将详细阐述这些新声明以及为什么它们比 var 更可取。

¥With TypeScript being an extension of JavaScript, the language naturally supports let and const. Here we’ll elaborate more on these new declarations and why they’re preferable to var.

如果你曾临时使用过 JavaScript,那么下一部分可能是刷新记忆的好方法。如果你非常熟悉 JavaScript 中 var 声明的所有怪癖,你可能会发现更容易跳过。

¥If you’ve used JavaScript offhandedly, the next section might be a good way to refresh your memory. If you’re intimately familiar with all the quirks of var declarations in JavaScript, you might find it easier to skip ahead.

var 声明

¥var declarations

传统上,在 JavaScript 中声明变量总是使用 var 关键字。

¥Declaring a variable in JavaScript has always traditionally been done with the var keyword.

ts
var a = 10;

你可能已经知道,我们刚刚声明了一个名为 a 的变量,其值为 10

¥As you might’ve figured out, we just declared a variable named a with the value 10.

我们还可以在函数内部声明一个变量:

¥We can also declare a variable inside of a function:

ts
function f() {
var message = "Hello, world!";
return message;
}

我们还可以在其他函数中访问这些相同的变量:

¥and we can also access those same variables within other functions:

ts
function f() {
var a = 10;
return function g() {
var b = a + 1;
return b;
};
}
var g = f();
g(); // returns '11'

在上述示例中,g 捕获了在 f 中声明的变量 a。在 g 被调用的任何时候,a 的值将与 f 中的 a 的值绑定。即使在 f 运行完成后调用 g,它也可以访问和修改 a

¥In this above example, g captured the variable a declared in f. At any point that g gets called, the value of a will be tied to the value of a in f. Even if g is called once f is done running, it will be able to access and modify a.

ts
function f() {
var a = 1;
a = 2;
var b = g();
a = 3;
return b;
function g() {
return a;
}
}
f(); // returns '2'

作用域规则

¥Scoping rules

var 声明对于那些用于其他语言的声明有一些奇怪的作用域规则。举个例子:

¥var declarations have some odd scoping rules for those used to other languages. Take the following example:

ts
function f(shouldInitialize: boolean) {
if (shouldInitialize) {
var x = 10;
}
return x;
}
f(true); // returns '10'
f(false); // returns 'undefined'

一些读者可能会对这个例子产生双重看法。变量 x 是在 if 块内声明的,但我们可以从该块外访问它。这是因为 var 声明可以在其包含的函数、模块、命名空间或全局范围内的任何位置访问 - 我们稍后会讨论所有这些 - 无论包含块如何。有些人称其为 var 作用域或函数作用域。参数也是函数作用域的。

¥Some readers might do a double-take at this example. The variable x was declared within the if block, and yet we were able to access it from outside that block. That’s because var declarations are accessible anywhere within their containing function, module, namespace, or global scope - all which we’ll go over later on - regardless of the containing block. Some people call this var-scoping or function-scoping. Parameters are also function scoped.

这些作用域规则可能会导致几种类型的错误。他们加剧的一个问题是多次声明同一个变量并不是错误的事实:

¥These scoping rules can cause several types of mistakes. One problem they exacerbate is the fact that it is not an error to declare the same variable multiple times:

ts
function sumMatrix(matrix: number[][]) {
var sum = 0;
for (var i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (var i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}
return sum;
}

也许对于一些有经验的 JavaScript 开发者来说很容易发现,但是内部的 for 循环会意外地覆盖变量 i,因为 i 指的是同一个函数作用域的变量。正如有经验的开发者现在所知道的那样,类似的错误会从代码审查中溜走,并且可能会导致无穷无尽的挫败感。

¥Maybe it was easy to spot out for some experienced JavaScript developers, but the inner for-loop will accidentally overwrite the variable i because i refers to the same function-scoped variable. As experienced developers know by now, similar sorts of bugs slip through code reviews and can be an endless source of frustration.

变量获取杂项

¥Variable capturing quirks

花点时间猜一下以下代码段的输出是什么:

¥Take a quick second to guess what the output of the following snippet is:

ts
for (var i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i);
}, 100 * i);
}

对于那些不熟悉的人,setTimeout 会在一定的毫秒数后尝试执行一个函数(尽管等待其他任何东西停止运行)。

¥For those unfamiliar, setTimeout will try to execute a function after a certain number of milliseconds (though waiting for anything else to stop running).

准备好?看一看:

¥Ready? Take a look:

10
10
10
10
10
10
10
10
10
10

许多 JavaScript 开发者都非常熟悉这种行为,但如果你感到惊讶,那么你肯定并不孤单。大多数人期望输出是

¥Many JavaScript developers are intimately familiar with this behavior, but if you’re surprised, you’re certainly not alone. Most people expect the output to be

0
1
2
3
4
5
6
7
8
9

还记得我们之前提到的变量捕获吗?我们传递给 setTimeout 的每个函数表达式实际上都指向同一范围内的同一个 i

¥Remember what we mentioned earlier about variable capturing? Every function expression we pass to setTimeout actually refers to the same i from the same scope.

让我们花一点时间考虑一下这意味着什么。setTimeout 将在几毫秒后运行函数,但仅在 for 循环停止执行之后;当 for 循环停止执行时,i 的值为 10。所以每次调用给定的函数时,它都会打印出 10

¥Let’s take a minute to consider what that means. setTimeout will run a function after some number of milliseconds, but only after the for loop has stopped executing; By the time the for loop has stopped executing, the value of i is 10. So each time the given function gets called, it will print out 10!

常见的解决方法是使用 IIFE - 立即调用的函数表达式 - 在每次迭代时捕获 i

¥A common work around is to use an IIFE - an Immediately Invoked Function Expression - to capture i at each iteration:

ts
for (var i = 0; i < 10; i++) {
// capture the current state of 'i'
// by invoking a function with its current value
(function (i) {
setTimeout(function () {
console.log(i);
}, 100 * i);
})(i);
}

这种看起来很奇怪的模式实际上很常见。参数列表中的 i 实际上隐藏了在 for 循环中声明的 i,但是由于我们将它们命名为相同,因此我们不必过多地修改循环体。

¥This odd-looking pattern is actually pretty common. The i in the parameter list actually shadows the i declared in the for loop, but since we named them the same, we didn’t have to modify the loop body too much.

let 声明

¥let declarations

至此,你已经发现 var 存在一些问题,这正是引入 let 语句的原因。除了使用的关键字之外,let 语句的编写方式与 var 语句相同。

¥By now you’ve figured out that var has some problems, which is precisely why let statements were introduced. Apart from the keyword used, let statements are written the same way var statements are.

ts
let hello = "Hello!";

关键的区别不在于语法,而在于语义,我们现在将深入探讨。

¥The key difference is not in the syntax, but in the semantics, which we’ll now dive into.

块作用域

¥Block-scoping

当使用 let 声明一个变量时,它使用一些所谓的词法作用域或块作用域。与使用 var 声明的变量的作用域泄漏到其包含函数不同,块作用域变量在其最近的包含块或 for 循环之外不可见。

¥When a variable is declared using let, it uses what some call lexical-scoping or block-scoping. Unlike variables declared with var whose scopes leak out to their containing function, block-scoped variables are not visible outside of their nearest containing block or for-loop.

ts
function f(input: boolean) {
let a = 100;
if (input) {
// Still okay to reference 'a'
let b = a + 1;
return b;
}
// Error: 'b' doesn't exist here
return b;
}

在这里,我们有两个局部变量 aba 的作用域仅限于 f 的主体,而 b 的作用域仅限于包含 if 语句的块。

¥Here, we have two local variables a and b. a’s scope is limited to the body of f while b’s scope is limited to the containing if statement’s block.

catch 子句中声明的变量也有类似的作用域规则。

¥Variables declared in a catch clause also have similar scoping rules.

ts
try {
throw "oh no!";
} catch (e) {
console.log("Oh well.");
}
// Error: 'e' doesn't exist here
console.log(e);

块作用域变量的另一个特性是它们在实际声明之前不能被读取或写入。虽然这些变量在其整个作用域内都是 “present”,但直到它们声明之前的所有点都是它们临时死区的一部分。这只是在 let 语句之前表示你无法访问它们的一种复杂方式,幸运的是 TypeScript 会让你知道这一点。

¥Another property of block-scoped variables is that they can’t be read or written to before they’re actually declared. While these variables are “present” throughout their scope, all points up until their declaration are part of their temporal dead zone. This is just a sophisticated way of saying you can’t access them before the let statement, and luckily TypeScript will let you know that.

ts
a++; // illegal to use 'a' before it's declared;
let a;

需要注意的是,你仍然可以在声明之前捕获块作用域的变量。唯一的问题是在声明之前调用该函数是非法的。如果以 ES2015 为目标,现代运行时将抛出错误;但是,现在 TypeScript 是宽松的,不会将其报告为错误。

¥Something to note is that you can still capture a block-scoped variable before it’s declared. The only catch is that it’s illegal to call that function before the declaration. If targeting ES2015, a modern runtime will throw an error; however, right now TypeScript is permissive and won’t report this as an error.

ts
function foo() {
// okay to capture 'a'
return a;
}
// illegal call 'foo' before 'a' is declared
// runtimes should throw an error here
foo();
let a;

有关时间死区的更多信息,请参阅 Mozilla 开发者网络 上的相关内容。

¥For more information on temporal dead zones, see relevant content on the Mozilla Developer Network.

重新声明和阴影

¥Re-declarations and Shadowing

对于 var 声明,我们提到过声明变量多少次并不重要;你刚得到一个。

¥With var declarations, we mentioned that it didn’t matter how many times you declared your variables; you just got one.

ts
function f(x) {
var x;
var x;
if (true) {
var x;
}
}

在上面的例子中,x 的所有声明实际上都指向同一个 x,这是完全有效的。这通常最终成为错误的来源。值得庆幸的是,let 声明并不那么宽容。

¥In the above example, all declarations of x actually refer to the same x, and this is perfectly valid. This often ends up being a source of bugs. Thankfully, let declarations are not as forgiving.

ts
let x = 10;
let x = 20; // error: can't re-declare 'x' in the same scope

对于 TypeScript 来说,变量不一定都需要在块作用域内才能告诉我们存在问题。

¥The variables don’t necessarily need to both be block-scoped for TypeScript to tell us that there’s a problem.

ts
function f(x) {
let x = 100; // error: interferes with parameter declaration
}
function g() {
let x = 100;
var x = 100; // error: can't have both declarations of 'x'
}

这并不是说块作用域的变量永远不能用函数作用域的变量声明。块作用域的变量只需要在一个明显不同的块中声明。

¥That’s not to say that a block-scoped variable can never be declared with a function-scoped variable. The block-scoped variable just needs to be declared within a distinctly different block.

ts
function f(condition, x) {
if (condition) {
let x = 100;
return x;
}
return x;
}
f(false, 0); // returns '0'
f(true, 0); // returns '100'

在更嵌套的作用域内引入新名称的行为称为遮蔽。它有点像一把双刃剑,因为它可以在意外阴影的情况下自行引入某些错误,同时还可以防止某些错误。例如,假设我们使用 let 变量编写了早期的 sumMatrix 函数。

¥The act of introducing a new name in a more nested scope is called shadowing. It is a bit of a double-edged sword in that it can introduce certain bugs on its own in the event of accidental shadowing, while also preventing certain bugs. For instance, imagine we had written our earlier sumMatrix function using let variables.

ts
function sumMatrix(matrix: number[][]) {
let sum = 0;
for (let i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (let i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}
return sum;
}

这个版本的循环实际上会正确地执行求和,因为内循环的 i 从外循环中遮住了 i

¥This version of the loop will actually perform the summation correctly because the inner loop’s i shadows i from the outer loop.

为了编写更清晰的代码,通常应该避免阴影。虽然在某些情况下可能适合利用它,但你应该使用最佳判断。

¥Shadowing should usually be avoided in the interest of writing clearer code. While there are some scenarios where it may be fitting to take advantage of it, you should use your best judgement.

块作用域的变量捕获

¥Block-scoped variable capturing

当我们第一次接触到使用 var 声明进行变量捕获的想法时,我们简要介绍了变量在捕获后的行为方式。为了更好地理解这一点,每次运行作用域时,它都会创建 “environment” 个变量。即使在其作用域内的所有内容都完成执行之后,该环境及其捕获的变量仍然存在。

¥When we first touched on the idea of variable capturing with var declaration, we briefly went into how variables act once captured. To give a better intuition of this, each time a scope is run, it creates an “environment” of variables. That environment and its captured variables can exist even after everything within its scope has finished executing.

ts
function theCityThatAlwaysSleeps() {
let getCity;
if (true) {
let city = "Seattle";
getCity = function () {
return city;
};
}
return getCity();
}

因为我们已经从其环境中捕获了 city,所以尽管 if 块已完成执行,我们仍然可以访问它。

¥Because we’ve captured city from within its environment, we’re still able to access it despite the fact that the if block finished executing.

回想一下我们之前的 setTimeout 示例,我们最终需要使用 IIFE 来为 for 循环的每次迭代捕获变量的状态。实际上,我们所做的是为我们捕获的变量创建一个新的变量环境。这有点痛苦,但幸运的是,你再也不用在 TypeScript 中这样做了。

¥Recall that with our earlier setTimeout example, we ended up needing to use an IIFE to capture the state of a variable for every iteration of the for loop. In effect, what we were doing was creating a new variable environment for our captured variables. That was a bit of a pain, but luckily, you’ll never have to do that again in TypeScript.

let 声明在声明为循环的一部分时具有截然不同的行为。这些声明不仅仅是为循环本身引入一个新环境,而是在每次迭代时创建一个新的作用域。由于这就是我们对 IIFE 所做的事情,我们可以将旧的 setTimeout 示例更改为仅使用 let 声明。

¥let declarations have drastically different behavior when declared as part of a loop. Rather than just introducing a new environment to the loop itself, these declarations sort of create a new scope per iteration. Since this is what we were doing anyway with our IIFE, we can change our old setTimeout example to just use a let declaration.

ts
for (let i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i);
}, 100 * i);
}

正如预期的那样,这将打印出来

¥and as expected, this will print out

0
1
2
3
4
5
6
7
8
9

const 声明

¥const declarations

const 声明是另一种声明变量的方式。

¥const declarations are another way of declaring variables.

ts
const numLivesForCat = 9;

它们类似于 let 声明,但顾名思义,一旦绑定,它们的值就不能更改。换句话说,它们具有与 let 相同的作用域规则,但你不能重新分配给它们。

¥They are like let declarations but, as their name implies, their value cannot be changed once they are bound. In other words, they have the same scoping rules as let, but you can’t re-assign to them.

这不应与它们引用的值是不可变的想法相混淆。

¥This should not be confused with the idea that the values they refer to are immutable.

ts
const numLivesForCat = 9;
const kitty = {
name: "Aurora",
numLives: numLivesForCat,
};
// Error
kitty = {
name: "Danielle",
numLives: numLivesForCat,
};
// all "okay"
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;

除非你采取特定措施来避免它,否则 const 变量的内部状态仍然是可修改的。幸运的是,TypeScript 允许你指定对象的成员是 readonly接口章节 有细节。

¥Unless you take specific measures to avoid it, the internal state of a const variable is still modifiable. Fortunately, TypeScript allows you to specify that members of an object are readonly. The chapter on Interfaces has the details.

letconst

¥let vs. const

鉴于我们有两种类型的声明具有相似的作用域语义,很自然地会发现自己询问使用哪一种。像大多数广泛的问题一样,答案是:视情况而定。

¥Given that we have two types of declarations with similar scoping semantics, it’s natural to find ourselves asking which one to use. Like most broad questions, the answer is: it depends.

应用 最小特权原则,除你计划修改的声明之外的所有声明都应使用 const。基本原理是,如果不需要写入变量,则使用同一代码库的其他人不应该自动写入对象,并且需要考虑他们是否真的需要重新分配给变量。在推断数据流时,使用 const 还可以使代码更可预测。

¥Applying the principle of least privilege, all declarations other than those you plan to modify should use const. The rationale is that if a variable didn’t need to get written to, others working on the same codebase shouldn’t automatically be able to write to the object, and will need to consider whether they really need to reassign to the variable. Using const also makes code more predictable when reasoning about flow of data.

使用你的最佳判断,如果适用,请与团队的其他成员协商此事。

¥Use your best judgement, and if applicable, consult the matter with the rest of your team.

本手册的大部分内容使用 let 声明。

¥The majority of this handbook uses let declarations.

解构

¥Destructuring

TypeScript 的另一个 ECMAScript 2015 特性是解构。如需完整参考,请参阅 Mozilla 开发者网络上的文章。在本节中,我们将给出一个简短的概述。

¥Another ECMAScript 2015 feature that TypeScript has is destructuring. For a complete reference, see the article on the Mozilla Developer Network. In this section, we’ll give a short overview.

数组解构

¥Array destructuring

最简单的解构形式是数组解构赋值:

¥The simplest form of destructuring is array destructuring assignment:

ts
let input = [1, 2];
let [first, second] = input;
console.log(first); // outputs 1
console.log(second); // outputs 2

这将创建两个名为 firstsecond 的新变量。这相当于使用索引,但更方便:

¥This creates two new variables named first and second. This is equivalent to using indexing, but is much more convenient:

ts
first = input[0];
second = input[1];

解构也适用于已经声明的变量:

¥Destructuring works with already-declared variables as well:

ts
// swap variables
[first, second] = [second, first];

并带有函数的参数:

¥And with parameters to a function:

ts
function f([first, second]: [number, number]) {
console.log(first);
console.log(second);
}
f([1, 2]);

你可以使用语法 ... 为列表中的其余项目创建一个变量:

¥You can create a variable for the remaining items in a list using the syntax ...:

ts
let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // outputs 1
console.log(rest); // outputs [ 2, 3, 4 ]

当然,由于这是 JavaScript,你可以忽略你不关心的尾随元素:

¥Of course, since this is JavaScript, you can just ignore trailing elements you don’t care about:

ts
let [first] = [1, 2, 3, 4];
console.log(first); // outputs 1

或其他元素:

¥Or other elements:

ts
let [, second, , fourth] = [1, 2, 3, 4];
console.log(second); // outputs 2
console.log(fourth); // outputs 4

元组解构

¥Tuple destructuring

元组可以像数组一样被解构;解构变量获取相应元组元素的类型:

¥Tuples may be destructured like arrays; the destructuring variables get the types of the corresponding tuple elements:

ts
let tuple: [number, string, boolean] = [7, "hello", true];
let [a, b, c] = tuple; // a: number, b: string, c: boolean

对超出其元素作用域的元组进行解构是错误的:

¥It’s an error to destructure a tuple beyond the range of its elements:

ts
let [a, b, c, d] = tuple; // Error, no element at index 3

与数组一样,你可以使用 ... 解构元组的其余部分,以获得更短的元组:

¥As with arrays, you can destructure the rest of the tuple with ..., to get a shorter tuple:

ts
let [a, ...bc] = tuple; // bc: [string, boolean]
let [a, b, c, ...d] = tuple; // d: [], the empty tuple

或忽略尾随元素或其他元素:

¥Or ignore trailing elements, or other elements:

ts
let [a] = tuple; // a: number
let [, b] = tuple; // b: string

对象解构

¥Object destructuring

你还可以解构对象:

¥You can also destructure objects:

ts
let o = {
a: "foo",
b: 12,
c: "bar",
};
let { a, b } = o;

这会从 o.ao.b 创建新的变量 ab。请注意,如果不需要,可以跳过 c

¥This creates new variables a and b from o.a and o.b. Notice that you can skip c if you don’t need it.

像数组解构一样,你可以在没有声明的情况下进行赋值:

¥Like array destructuring, you can have assignment without declaration:

ts
({ a, b } = { a: "baz", b: 101 });

请注意,我们必须用括号括住这个语句。JavaScript 通常将 { 解析为块的开始。

¥Notice that we had to surround this statement with parentheses. JavaScript normally parses a { as the start of block.

你可以使用语法 ... 为对象中的剩余项创建变量:

¥You can create a variable for the remaining items in an object using the syntax ...:

ts
let { a, ...passthrough } = o;
let total = passthrough.b + passthrough.c.length;

属性重命名

¥Property renaming

你还可以为属性赋予不同的名称:

¥You can also give different names to properties:

ts
let { a: newName1, b: newName2 } = o;

在这里,语法开始变得混乱。你可以将 a: newName1 读作“a 读作 newName1”。方向是从左到右的,就好像你写过:

¥Here the syntax starts to get confusing. You can read a: newName1 as ”a as newName1”. The direction is left-to-right, as if you had written:

ts
let newName1 = o.a;
let newName2 = o.b;

令人困惑的是,这里的冒号并不表示类型。类型,如果你指定它,仍然需要在整个解构之后编写:

¥Confusingly, the colon here does not indicate the type. The type, if you specify it, still needs to be written after the entire destructuring:

ts
let { a: newName1, b: newName2 }: { a: string; b: number } = o;

默认值

¥Default values

默认值允许你在未定义属性的情况下指定默认值:

¥Default values let you specify a default value in case a property is undefined:

ts
function keepWholeObject(wholeObject: { a: string; b?: number }) {
let { a, b = 1001 } = wholeObject;
}

在这个例子中,b? 表示 b 是可选的,所以它可能是 undefinedkeepWholeObject 现在具有 wholeObject 的变量以及属性 ab,即使 b 未定义。

¥In this example the b? indicates that b is optional, so it may be undefined. keepWholeObject now has a variable for wholeObject as well as the properties a and b, even if b is undefined.

函数声明

¥Function declarations

解构也适用于函数声明。对于简单的情况,这很简单:

¥Destructuring also works in function declarations. For simple cases this is straightforward:

ts
type C = { a: string; b?: number };
function f({ a, b }: C): void {
// ...
}

但是为参数指定默认值更为常见,而通过解构获得默认值可能会很棘手。首先,你需要记住将模式放在默认值之前。

¥But specifying defaults is more common for parameters, and getting defaults right with destructuring can be tricky. First of all, you need to remember to put the pattern before the default value.

ts
function f({ a = "", b = 0 } = {}): void {
// ...
}
f();

上面的代码片段是类型推断的一个例子,在手册前面已经解释过了。

¥The snippet above is an example of type inference, explained earlier in the handbook.

然后,你需要记住为解构属性而不是主初始化器上的可选属性提供默认值。请记住,C 是用可选的 b 定义的:

¥Then, you need to remember to give a default for optional properties on the destructured property instead of the main initializer. Remember that C was defined with b optional:

ts
function f({ a, b = 0 } = { a: "" }): void {
// ...
}
f({ a: "yes" }); // ok, default b = 0
f(); // ok, default to { a: "" }, which then defaults b = 0
f({}); // error, 'a' is required if you supply an argument

谨慎使用解构。正如前面的例子所展示的,除了最简单的解构表达式之外,任何东西都是令人困惑的。对于深度嵌套的解构尤其如此,即使没有重命名、默认值和类型注释,也很难理解。尽量保持解构表达式小而简单。你始终可以编写解构将自己生成的分配。

¥Use destructuring with care. As the previous example demonstrates, anything but the simplest destructuring expression is confusing. This is especially true with deeply nested destructuring, which gets really hard to understand even without piling on renaming, default values, and type annotations. Try to keep destructuring expressions small and simple. You can always write the assignments that destructuring would generate yourself.

展开

¥Spread

扩展运算符与解构相反。它允许你将一个数组传播到另一个数组中,或者将一个对象传播到另一个对象中。例如:

¥The spread operator is the opposite of destructuring. It allows you to spread an array into another array, or an object into another object. For example:

ts
let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];

这给 bothPlus 值 [0, 1, 2, 3, 4, 5]。传播会创建 firstsecond 的浅表副本。它们不会因价差而改变。

¥This gives bothPlus the value [0, 1, 2, 3, 4, 5]. Spreading creates a shallow copy of first and second. They are not changed by the spread.

你还可以传播对象:

¥You can also spread objects:

ts
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };

现在 search{ food: "rich", price: "$$", ambiance: "noisy" }。对象传播比数组传播更复杂。像数组扩展一样,它是从左到右进行的,但结果仍然是一个对象。这意味着传播对象中较晚出现的属性会覆盖较早出现的属性。所以如果我们把前面的例子修改为最后展开:

¥Now search is { food: "rich", price: "$$", ambiance: "noisy" }. Object spreading is more complex than array spreading. Like array spreading, it proceeds from left-to-right, but the result is still an object. This means that properties that come later in the spread object overwrite properties that come earlier. So if we modify the previous example to spread at the end:

ts
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { food: "rich", ...defaults };

那么 defaults 中的 food 属性会覆盖 food: "rich",这在本例中不是我们想要的。

¥Then the food property in defaults overwrites food: "rich", which is not what we want in this case.

对象传播还有其他一些令人惊讶的限制。首先,它只包含一个对象的 拥有的、可枚举的属性。基本上,这意味着在传播对象实例时会丢失方法:

¥Object spread also has a couple of other surprising limits. First, it only includes an objects’ own, enumerable properties. Basically, that means you lose methods when you spread instances of an object:

ts
class C {
p = 12;
m() {}
}
let c = new C();
let clone = { ...c };
clone.p; // ok
clone.m(); // error!

其次,TypeScript 编译器不允许从泛型函数传播类型参数。该功能预计将出现在该语言的未来版本中。

¥Second, the TypeScript compiler doesn’t allow spreads of type parameters from generic functions. That feature is expected in future versions of the language.

using 声明

¥using declarations

using 声明是 JavaScript 即将推出的功能,是 第三阶段显式资源管理 提案的一部分。using 声明与 const 声明非常相似,不同之处在于它将绑定到声明的值的生命周期与变量的范围结合起来。

¥using declarations are an upcoming feature for JavaScript that are part of the Stage 3 Explicit Resource Management proposal. A using declaration is much like a const declaration, except that it couples the lifetime of the value bound to the declaration with the scope of the variable.

当控制退出包含 using 声明的块时,将执行声明值的 [Symbol.dispose]() 方法,这允许该值执行清理:

¥When control exits the block containing a using declaration, the [Symbol.dispose]() method of the declared value is executed, which allows that value to perform cleanup:

ts
function f() {
using x = new C();
doSomethingWith(x);
} // `x[Symbol.dispose]()` is called

在运行时,这具有大致相当于以下效果:

¥At runtime, this has an effect roughly equivalent to the following:

ts
function f() {
const x = new C();
try {
doSomethingWith(x);
}
finally {
x[Symbol.dispose]();
}
}

在使用保存原生引用(如文件句柄)的 JavaScript 对象时,using 声明对于避免内存泄漏非常有用

¥using declarations are extremely useful for avoiding memory leaks when working with JavaScript objects that hold on to native references like file handles

ts
{
using file = await openFile();
file.write(text);
doSomethingThatMayThrow();
} // `file` is disposed, even if an error is thrown

或范围操作,如跟踪

¥or scoped operations like tracing

ts
function f() {
using activity = new TraceActivity("f"); // traces entry into function
// ...
} // traces exit of function

varletconst 不同,using 声明不支持解构。

¥Unlike var, let, and const, using declarations do not support destructuring.

nullundefined

¥null and undefined

值得注意的是,该值可以是 nullundefined,在这种情况下,块末尾不会处理任何内容:

¥It’s important to note that the value can be null or undefined, in which case nothing is disposed at the end of the block:

ts
{
using x = b ? new C() : null;
// ...
}

这大致相当于:

¥which is roughly equivalent to:

ts
{
const x = b ? new C() : null;
try {
// ...
}
finally {
x?.[Symbol.dispose]();
}
}

这允许你在声明 using 声明时有条件地获取资源,而无需复杂的分支或重复。

¥This allows you to conditionally acquire resources when declaring a using declaration without the need for complex branching or repetition.

定义一次性资源

¥Defining a disposable resource

你可以通过实现 Disposable 接口来指示你生成的类或对象是一次性的:

¥You can indicate the classes or objects you produce are disposable by implementing the Disposable interface:

ts
// from the default lib:
interface Disposable {
[Symbol.dispose](): void;
}
// usage:
class TraceActivity implements Disposable {
readonly name: string;
constructor(name: string) {
this.name = name;
console.log(`Entering: ${name}`);
}
[Symbol.dispose](): void {
console.log(`Exiting: ${name}`);
}
}
function f() {
using _activity = new TraceActivity("f");
console.log("Hello world!");
}
f();
// prints:
// Entering: f
// Hello world!
// Exiting: f

await using 声明

¥await using declarations

某些资源或操作可能需要异步执行清理。为了适应这一点,显式资源管理 提案还引入了 await using 声明:

¥Some resources or operations may have cleanup that needs to be performed asynchronously. To accommodate this, the Explicit Resource Management proposal also introduces the await using declaration:

ts
async function f() {
await using x = new C();
} // `await x[Symbol.asyncDispose]()` is invoked

当控制离开包含块时,await using 声明调用并等待其值的 [Symbol.asyncDispose]() 方法。这允许异步清理,例如数据库事务执行回滚或提交,或者文件流在关闭之前将任何挂起的写入刷新到存储。

¥An await using declaration invokes, and awaits, its value’s [Symbol.asyncDispose]() method as control leaves the containing block. This allows for asynchronous cleanup, such as a database transaction performing a rollback or commit, or a file stream flushing any pending writes to storage before it is closed.

await 一样,await using 只能在 async 函数或方法中使用,或者在模块的顶层使用。

¥As with await, await using can only be used in an async function or method, or at the top level of a module.

定义异步的一次性资源

¥Defining an asynchronously disposable resource

正如 using 依赖于 Disposable 的对象一样,await using 依赖于 AsyncDisposable 的对象:

¥Just as using relies on objects that are Disposable, an await using relies on objects that are AsyncDisposable:

ts
// from the default lib:
interface AsyncDisposable {
[Symbol.asyncDispose]: PromiseLike<void>;
}
// usage:
class DatabaseTransaction implements AsyncDisposable {
public success = false;
private db: Database | undefined;
private constructor(db: Database) {
this.db = db;
}
static async create(db: Database) {
await db.execAsync("BEGIN TRANSACTION");
return new DatabaseTransaction(db);
}
async [Symbol.asyncDispose]() {
if (this.db) {
const db = this.db:
this.db = undefined;
if (this.success) {
await db.execAsync("COMMIT TRANSACTION");
}
else {
await db.execAsync("ROLLBACK TRANSACTION");
}
}
}
}
async function transfer(db: Database, account1: Account, account2: Account, amount: number) {
using tx = await DatabaseTransaction.create(db);
if (await debitAccount(db, account1, amount)) {
await creditAccount(db, account2, amount);
}
// if an exception is thrown before this line, the transaction will roll back
tx.success = true;
// now the transaction will commit
}

await usingawait

¥await using vs await

作为 await using 声明一部分的 await 关键字仅指示资源的处置是 await 式的。它不会对值本身进行 await

¥The await keyword that is part of the await using declaration only indicates that the disposal of the resource is await-ed. It does not await the value itself:

ts
{
await using x = getResourceSynchronously();
} // performs `await x[Symbol.asyncDispose]()`
{
await using y = await getResourceAsynchronously();
} // performs `await y[Symbol.asyncDispose]()`

await usingreturn

¥await using and return

需要注意的是,如果你在 async 函数中使用 await using 声明,而该函数返回 Promise 而没有先对其进行 await 处理,则此行为有一个小警告:

¥It’s important to note that there is a small caveat with this behavior if you are using an await using declaration in an async function that returns a Promise without first await-ing it:

ts
function g() {
return Promise.reject("error!");
}
async function f() {
await using x = new C();
return g(); // missing an `await`
}

由于返回的 Promise 不是 await-ed,因此 JavaScript 运行时可能会报告未处理的拒绝,因为在 await-ing 异步处理 x 时执行暂停,而没有订阅返回的 Promise。然而,这并不是 await using 特有的问题,因为这也可能发生在使用 try..finallyasync 函数中:

¥Because the returned promise isn’t await-ed, it’s possible that the JavaScript runtime may report an unhandled rejection since execution pauses while await-ing the asynchronous disposal of x, without having subscribed to the returned promise. This is not a problem that is unique to await using, however, as this can also occur in an async function that uses try..finally:

ts
async function f() {
try {
return g(); // also reports an unhandled rejection
}
finally {
await somethingElse();
}
}

为了避免这种情况,建议你对返回值进行 await(如果它可能是 Promise):

¥To avoid this situation, it is recommended that you await your return value if it may be a Promise:

ts
async function f() {
await using x = new C();
return await g();
}

forfor..of 语句中的 usingawait using

¥using and await using in for and for..of statements

usingawait using 都可以在 for 语句中使用:

¥Both using and await using can be used in a for statement:

ts
for (using x = getReader(); !x.eof; x.next()) {
// ...
}

在这种情况下,x 的生存期仅限于整个 for 语句,并且仅当控制因 breakreturnthrow 离开循环或循环条件为 false 时才被释放。

¥In this case, the lifetime of x is scoped to the entire for statement and is only disposed when control leaves the loop due to break, return, throw, or when the loop condition is false.

除了 for 语句之外,这两个声明也可以在 for..of 语句中使用:

¥In addition to for statements, both declarations can also be used in for..of statements:

ts
function * g() {
yield createResource1();
yield createResource2();
}
for (using x of g()) {
// ...
}

这里,x 在每次循环迭代结束时被处理,然后用下一个值重新初始化。当消耗由生成器一次生成一个的资源时,这尤其有用。

¥Here, x is disposed at the end of each iteration of the loop, and is then reinitialized with the next value. This is especially useful when consuming resources produced one at a time by a generator.

旧运行时中的 usingawait using

¥using and await using in older runtimes

只要你使用 Symbol.dispose/Symbol.asyncDispose 的兼容填充程序(例如最新版本的 NodeJS 中默认提供的填充程序),在针对较旧的 ECMAScript 版本时就可以使用 usingawait using 声明。

¥using and await using declarations can be used when targeting older ECMAScript editions as long as you are using a compatible polyfill for Symbol.dispose/Symbol.asyncDispose, such as the one provided by default in recent editions of NodeJS.