let 和 const 是 JavaScript 中两种相对较新的变量声明概念。
如我们之前提到的,let 在某些方面类似于 var,但允许用户避免在 JavaScript 中遇到的一些常见“陷阱”。
const 是 let 的一种增强,因为它可以防止对变量进行重新赋值。
由于 TypeScript 是 JavaScript 的扩展,该语言自然支持 let 和 const。在这里,我们将详细说明这些新声明以及它们为什么比 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.
tsvar 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:
tsfunction f() {var message = "Hello, world!";return message;}
我们还可以在其他函数中访问这些相同的变量:
🌐 and we can also access those same variables within other functions:
tsfunction 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.
tsfunction f() {var a = 1;a = 2;var b = g();a = 3;return b;function g() {return a;}}f(); // returns '2'
作用域规则
🌐 Scoping rules
var 声明对于习惯其他语言的人来说有一些奇怪的作用域规则。
看以下示例:
tsfunction 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:
tsfunction 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:
tsfor (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:
10101010101010101010
许多 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
0123456789
还记得我们之前提到的关于变量捕获的内容吗?
我们传给 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:
tsfor (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.
tslet 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.
tsfunction f(input: boolean) {let a = 100;if (input) {// Still okay to reference 'a'let b = a + 1;return b;}// Error: 'b' doesn't exist herereturn b;}
这里,我们有两个局部变量 a 和 b。a 的作用域仅限于 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.
tstry {throw "oh no!";} catch (e) {console.log("Oh well.");}// Error: 'e' doesn't exist hereconsole.log(e);
块作用域变量的另一个特性是,在实际声明之前,它们不能被读取或写入。虽然这些变量在整个作用域内都是“存在”的,但从作用域开始到声明之前的所有点都属于它们的_暂时性死区_。这只是一个复杂的说法,意思是你不能在 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.
tsa++; // 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.
tsfunction foo() {// okay to capture 'a'return a;}// illegal call 'foo' before 'a' is declared// runtimes should throw an error herefoo();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.
tsfunction 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.
tslet 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.
tsfunction 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.
tsfunction f(condition, x) {if (condition) {let x = 100;return x;}return x;}f(false, 0); // returns '0'f(true, 0); // returns '100'
在更内层的作用域中引入一个新名称的行为被称为_屏蔽(shadowing)_。
它有点像一把双刃剑,因为在不小心发生屏蔽时,它本身可能引入某些错误,同时也可以防止某些错误。
例如,想象一下,如果我们之前的 sumMatrix 函数是使用 let 变量编写的。
🌐 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.
tsfunction 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 声明的变量捕获的概念时,我们简要讨论了变量在被捕获后是如何表现的。为了更好地理解这一点,每次执行一个作用域时,它都会创建一个变量的“环境”。即使作用域内的所有内容都执行完毕,该环境及其捕获的变量仍然可以存在。
🌐 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.
tsfunction 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 声明。
tsfor (let i = 0; i < 10; i++) {setTimeout(function () {console.log(i);}, 100 * i);}
正如预期的那样,这将打印出来
🌐 and as expected, this will print out
0123456789
const 声明
🌐 const declarations
const 声明是声明变量的另一种方式。
tsconst 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.
tsconst numLivesForCat = 9;const kitty = {name: "Aurora",numLives: numLivesForCat,};// Errorkitty = {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.
let 对比 const
🌐 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:
tslet input = [1, 2];let [first, second] = input;console.log(first); // outputs 1console.log(second); // outputs 2
这会创建两个名为 first 和 second 的新变量。这等同于使用索引,但更加方便:
🌐 This creates two new variables named first and second.
This is equivalent to using indexing, but is much more convenient:
tsfirst = 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:
tsfunction 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 ...:
tslet [first, ...rest] = [1, 2, 3, 4];console.log(first); // outputs 1console.log(rest); // outputs [ 2, 3, 4 ]
当然,由于这是 JavaScript,你可以忽略你不关心的尾随元素:
🌐 Of course, since this is JavaScript, you can just ignore trailing elements you don’t care about:
tslet [first] = [1, 2, 3, 4];console.log(first); // outputs 1
或其他元素:
🌐 Or other elements:
tslet [, second, , fourth] = [1, 2, 3, 4];console.log(second); // outputs 2console.log(fourth); // outputs 4
元组解构
🌐 Tuple destructuring
元组可以像数组一样进行解构;解构的变量会获得对应元组元素的类型:
🌐 Tuples may be destructured like arrays; the destructuring variables get the types of the corresponding tuple elements:
tslet 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:
tslet [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:
tslet [a, ...bc] = tuple; // bc: [string, boolean]let [a, b, c, ...d] = tuple; // d: [], the empty tuple
或忽略尾随元素或其他元素:
🌐 Or ignore trailing elements, or other elements:
tslet [a] = tuple; // a: numberlet [, b] = tuple; // b: string
对象解构
🌐 Object destructuring
你还可以解构对象:
🌐 You can also destructure objects:
tslet o = {a: "foo",b: 12,c: "bar",};let { a, b } = o;
这会从 o.a 和 o.b 创建新的变量 a 和 b。注意,如果不需要 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 ...:
tslet { a, ...passthrough } = o;let total = passthrough.b + passthrough.c.length;
属性重命名
🌐 Property renaming
你还可以为属性赋予不同的名称:
🌐 You can also give different names to properties:
tslet { 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:
tslet 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:
tslet { 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:
tsfunction keepWholeObject(wholeObject: { a: string; b?: number }) {let { a, b = 1001 } = wholeObject;}
在此示例中,b? 表示 b 是可选的,因此它可能是 undefined。keepWholeObject 现在除了 wholeObject 的变量外,还具有属性 a 和 b,即使 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:
tstype 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.
tsfunction f({ a = "", b = 0 } = {}): void {// ...}f();
上面的代码片段是类型推断的一个示例,在手册前面已经解释过。
然后,你需要记住在解构属性上为可选属性提供默认值,而不是在主初始化器上提供。记住 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:
tsfunction f({ a, b = 0 } = { a: "" }): void {// ...}f({ a: "yes" }); // ok, default b = 0f(); // ok, default to { a: "" }, which then defaults b = 0f({}); // 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:
tslet first = [1, 2];let second = [3, 4];let bothPlus = [0, ...first, ...second, 5];
这会给 bothPlus 赋值 [0, 1, 2, 3, 4, 5]。
展开会创建 first 和 second 的浅拷贝。
它们不会被展开操作改变。
🌐 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:
tslet 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:
tslet 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:
tsclass C {p = 12;m() {}}let c = new C();let clone = { ...c };clone.p; // okclone.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 即将推出的功能,是 Stage 3 明确资源管理 提案的一部分。using 声明非常类似于 const 声明,但它将与声明绑定的值的 生命周期 与变量的 作用域 关联起来。
当控制流退出包含 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:
tsfunction f() {using x = new C();doSomethingWith(x);} // `x[Symbol.dispose]()` is called
在运行时,这大致上会产生与以下内容相同的效果:
🌐 At runtime, this has an effect roughly equivalent to the following:
tsfunction f() {const x = new C();try {doSomethingWith(x);}finally {x[Symbol.dispose]();}}
using 声明在处理持有本地引用(如文件句柄)的 JavaScript 对象时,非常有助于避免内存泄漏
ts{using file = await openFile();file.write(text);doSomethingThatMayThrow();} // `file` is disposed, even if an error is thrown
或范围操作,如跟踪
🌐 or scoped operations like tracing
tsfunction f() {using activity = new TraceActivity("f"); // traces entry into function// ...} // traces exit of function
与 var、let 和 const 不同,using 声明不支持解构。
🌐 Unlike var, let, and const, using declarations do not support destructuring.
null 和 undefined
🌐 null and undefined
重要的是要注意,该值可以是 null 或 undefined,在这种情况下,在代码块结束时不会释放任何内容:
🌐 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:
tsasync 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 backtx.success = true;// now the transaction will commit}
await using 对比 await
🌐 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 using 和 return
🌐 await using and return
需要注意的是,如果你在一个返回 Promise 的 async 函数中使用 await using 声明,而没有先对其进行 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:
tsfunction g() {return Promise.reject("error!");}async function f() {await using x = new C();return g(); // missing an `await`}
因为返回的 promise 没有被 await,所以在执行暂停以 await 异步处理 x 时,由于没有订阅返回的 promise,JavaScript 运行时可能会报告一个未处理的拒绝。然而,这并不是 await using 独有的问题,因为在使用 try..finally 的 async 函数中也可能发生这种情况:
🌐 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:
tsasync function f() {try {return g(); // also reports an unhandled rejection}finally {await somethingElse();}}
为了避免这种情况,建议如果返回值可能是 Promise,你应当 await:
🌐 To avoid this situation, it is recommended that you await your return value if it may be a Promise:
tsasync function f() {await using x = new C();return await g();}
for 和 for..of 语句中的 using 和 await using
🌐 using and await using in for and for..of statements
using 和 await using 都可以在 for 语句中使用:
🌐 Both using and await using can be used in a for statement:
tsfor (using x = getReader(); !x.eof; x.next()) {// ...}
在这种情况下,x 的生命周期范围限定在整个 for 语句内,只有在由于 break、return、throw 或循环条件为假而离开循环时才会被释放。
🌐 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:
tsfunction * 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.
较旧运行时中的 using 和 await using
🌐 using and await using in older runtimes
using 和 await using 声明可以在针对较旧的 ECMAScript 版本时使用,只要你使用了兼容的 Symbol.dispose/Symbol.asyncDispose 填充(polyfill),比如最近版本的 NodeJS 默认提供的填充。