變數宣告

letconst 是 JavaScript 中變數宣告的兩個相對較新的概念。 如前所述let 在某些方面類似於 var,但允許使用者避免 JavaScript 中常見的某些「陷阱」。

constlet 的擴充,它防止重新指派變數。

由於 TypeScript 是 JavaScript 的擴充,該語言自然支援 letconst。在這裡,我們將進一步說明這些新的宣告,以及它們為何比 var 更受青睞。

如果您曾經隨意使用過 JavaScript,下一節可能是個復習的好方法。如果您非常熟悉 JavaScript 中 var 宣告的所有怪癖,您可能會發現跳過比較容易。

var 宣告

在 JavaScript 中宣告變數傳統上一直使用 var 關鍵字。

ts
var a = 10;

正如您可能已經發現的,我們剛剛宣告了一個名為 a 的變數,其值為 10

我們也可以在函式內宣告變數

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

我們也可以在其他函式內存取相同的變數

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 的值都會繫結到 fa 的值。即使在 f 執行完畢後才呼叫 g,它仍將能夠存取和修改 a

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

範圍規則

var 宣告對於習慣其他語言的人來說,有一些奇怪的範圍規則。以下為一個範例

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

有些讀者可能會對這個範例感到驚訝。變數 x 是宣告在 if 區塊中的,但我們卻可以在區塊外存取它。這是因為 var 宣告可以在其包含函式、模組、命名空間或全域範圍內的任何地方存取,而我們稍後會探討所有這些內容,不論包含區塊為何。有些人稱此為 var 範圍函式範圍。參數也是函式範圍的。

這些範圍規則可能會導致幾種類型的錯誤。它們會加劇的一個問題是,多次宣告同一個變數並不會產生錯誤

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 參照同一個函式範圍變數。正如有經驗的開發人員現在所知,類似類型的錯誤會通過程式碼檢閱,並且會帶來無盡的挫折感。

變數擷取怪癖

花個幾秒鐘猜猜看以下程式片段的輸出結果是什麼

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

對於不熟悉的人來說,setTimeout 會嘗試在一定毫秒數後執行函式(儘管等待其他任何東西停止執行)。

準備好了嗎?來看看

10 10 10 10 10 10 10 10 10 10

許多 JavaScript 開發人員都非常熟悉這種行為,但如果你感到驚訝,你絕對不孤單。大多數人預期的輸出結果是

0 1 2 3 4 5 6 7 8 9

還記得我們之前提到的變數擷取嗎?我們傳遞給 setTimeout 的每個函式表達式實際上都參考同一個範圍內的同一個 i

讓我們花點時間想想這代表什麼意思。setTimeout 會在若干毫秒後執行函式,但只有for 迴圈停止執行之後;當 for 迴圈停止執行時,i 的值為 10。因此,每次呼叫給定的函式時,它都會印出 10

一個常見的解決方法是使用 IIFE(立即呼叫函式表達式)在每次反覆運算中擷取 i

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,但由於我們將它們命名為相同名稱,因此我們不必修改迴圈主體太多。

let 宣告

現在你已經發現 var 有些問題,這正是 let 陳述式被引進的原因。除了使用的關鍵字不同,let 陳述式與 var 陳述式的寫法相同。

ts
let hello = "Hello!";

關鍵的不同點不在於語法,而在於語意,我們現在將深入探討。

區塊作用域

當使用 let 宣告變數時,它會使用一些人稱為詞法作用域區塊作用域。與作用域會外洩到其包含函式的 var 宣告的變數不同,區塊作用域變數在其最近的包含區塊或 for 迴圈之外不可見。

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 陳述式的區塊。

catch 子句中宣告的變數也有類似的範圍規則。

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

區塊範圍變數的另一個屬性是,它們在實際宣告之前無法讀取或寫入。雖然這些變數在其整個範圍內「存在」,但直到宣告為止的所有點都是其時間死區的一部分。這只是一個複雜的說法,表示您無法在 let 陳述式之前存取它們,而且幸運的是,TypeScript 會讓您知道這一點。

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

需要注意的是,您仍然可以在宣告之前擷取區塊範圍變數。唯一的缺點是,在宣告之前呼叫該函式是非法的。如果鎖定 ES2015,現代執行時期會擲回錯誤;然而,目前 TypeScript 是允許的,不會將此報告為錯誤。

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 Developer Network 上的相關內容。

重新宣告和陰影化

使用 var 宣告時,我們提到過宣告變數的次數並不重要;您只會得到一個。

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

在上述範例中,x 的所有宣告實際上指的是相同的 x,這是完全有效的。這通常會成為錯誤的來源。謝天謝地,let 宣告沒有那麼寬容。

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

TypeScript 不一定需要兩個變數都是區塊作用域,才能告訴我們有問題。

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'
}

這並不是說區塊作用域變數永遠無法用函式作用域變數宣告。區塊作用域變數只需要在明顯不同的區塊中宣告即可。

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 函式。

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

為了撰寫更清晰的程式碼,通常應該避免遮蔽。雖然在某些情況下可能適合利用它,但你應該運用最佳判斷力。

區塊作用域變數擷取

當我們第一次接觸使用 var 宣告的變數擷取概念時,我們簡要介紹了變數在擷取後的作用方式。為了更直觀地了解這一點,每次執行範圍時,它都會建立一個變數「環境」。即使範圍內的所有內容都已執行完畢,該環境及其擷取的變數仍可以存在。

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

由於我們從其環境中擷取了 city,因此儘管 if 區塊已執行完畢,我們仍然可以存取它。

回想一下我們較早的 setTimeout 範例,我們最後需要使用 IIFE 來擷取變數的狀態,以進行 for 迴圈的每次反覆運算。實際上,我們所做的就是為我們擷取的變數建立新的變數環境。這有點麻煩,但幸運的是,在 TypeScript 中你再也不必這麼做了。

當宣告為迴圈的一部分時,let 宣告具有截然不同的行為。這些宣告不僅僅為迴圈本身引入新的環境,它們還會為每次反覆運算建立新的範圍。由於這正是我們使用 IIFE 所做的,因此我們可以將舊的 setTimeout 範例變更為僅使用 let 宣告。

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

而且正如預期,這會列印出

0 1 2 3 4 5 6 7 8 9

const 宣告

const 宣告是宣告變數的另一種方式。

ts
const numLivesForCat = 9;

它們就像 let 宣告,但正如其名稱所暗示的,它們的值一旦繫結就無法變更。換句話說,它們具有與 let 相同的範圍規則,但你無法重新指定它們。

這不應與它們所引用的值不可變的概念混淆。

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。有關詳細資訊,請參閱 介面章節

letconst

由於我們有兩種具有類似範圍語意的宣告,因此自然會想知道要使用哪一種。與大多數廣泛的問題一樣,答案是:視情況而定。

應用 最小權限原則,除了您計畫修改的宣告之外,所有宣告都應使用 const。其理由是,如果變數不需要寫入,則其他處理相同程式碼庫的人員不應自動能夠寫入物件,並且需要考慮是否真的需要重新指派變數。在推論資料流時,使用 const 也會使程式碼更具可預測性。

根據您的最佳判斷,並在適用的情況下,與您的團隊其他成員諮詢此事。

本手冊的大部分內容使用 let 宣告。

解構

TypeScript 擁有的另一項 ECMAScript 2015 功能是解構。有關完整參考,請參閱 Mozilla Developer Network 上的文章。在本節中,我們將提供簡要概述。

陣列解構

解構最簡單的形式是陣列解構賦值

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

這會建立兩個新的變數,分別命名為 firstsecond。這相當於使用索引,但更為方便

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

解構也適用於已宣告的變數

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

以及函式的參數

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

你可以使用語法 ... 為清單中剩下的項目建立一個變數

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

當然,由於這是 JavaScript,你可以忽略你不在乎的尾端元素

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

或其他元素

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

元組解構

元組可以像陣列一樣解構;解構變數取得對應元組元素的型別

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

解構元組超出其元素範圍會產生錯誤

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

與陣列一樣,你可以使用 ... 解構元組的其餘部分,以取得較短的元組

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

或忽略尾隨元素或其他元素

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

物件解構

你也可以解構物件

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

這會從 o.ao.b 建立新的變數 ab。請注意,如果你不需要 c,你可以略過它。

與陣列解構一樣,你可以不宣告就進行指派

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

請注意,我們必須用括號將此陳述式包起來。JavaScript 通常會將 { 解析為區塊的開頭。

你可以使用語法 ... 為物件中其餘的項目建立一個變數

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

屬性重新命名

您也可以為屬性指定不同的名稱

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

此處的語法開始令人困惑。您可以將 a: newName1 讀為「anewName1」。方向為從左至右,就像您寫下

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

令人困惑的是,此處的冒號並未表示類型。如果您指定類型,仍需要在整個解構後寫入

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

預設值

預設值讓您可以在屬性未定義時指定預設值

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

在此範例中,b? 表示 b 是選用的,因此可能是 undefinedkeepWholeObject 現在有一個變數 wholeObject,以及屬性 ab,即使 b 是未定義的。

函式宣告

解構也在函式宣告中運作。對於簡單的案例,這是很直接的

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

但為參數指定預設值比較常見,而且使用解構來正確取得預設值可能會很棘手。首先,你需要記得在預設值之前放置樣式。

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

上面的程式碼片段是類型推論的範例,在手冊的前面有說明。

接著,你需要記得為解構屬性中的選用屬性提供預設值,而不是主初始化項。請記住 C 是使用選用的 b 定義的

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

小心使用解構。正如前一個範例所示,除了最簡單的解構運算式之外,其他都令人困惑。對於深度巢狀的解構尤其如此,即使沒有堆疊重新命名、預設值和類型註解,也很難理解。請盡量保持解構運算式簡潔。你總是可以用自己產生的解構來撰寫指定。

散佈

展開運算子與解構相反。它允許你將陣列展開到另一個陣列,或將物件展開到另一個物件。例如

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

這會讓 bothPlus 的值變成 [0, 1, 2, 3, 4, 5]。展開會建立 firstsecond 的淺層拷貝。它們不會因為展開而改變。

你也可以展開物件

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

現在 search{ food: "rich", price: "$$", ambiance: "noisy" }。物件展開比陣列展開複雜。就像陣列展開,它從左到右進行,但結果仍然是一個物件。這表示在展開物件中較後出現的屬性會覆寫較早出現的屬性。所以如果我們修改前一個範例,在最後展開

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

那麼 defaults 中的 food 屬性會覆寫 food: "rich",這不是我們在這個情況下想要的。

物件展開也有其他幾個令人驚訝的限制。首先,它只包含物件的 自有可列舉屬性。基本上,這表示當你展開物件實例時,你會失去方法

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

其次,TypeScript 編譯器不允許從泛型函數展開類型參數。這項功能預計會在語言的未來版本中出現。

using 宣告

using 宣告是 JavaScript 中即將推出的功能,是 第 3 階段明確資源管理 提案的一部分。using 宣告很像 const 宣告,但它將繫結到宣告的值的生命週期與變數的作用域結合在一起。

當控制離開包含 using 宣告的區塊時,宣告值的 [Symbol.dispose]() 方法會被執行,這允許該值執行清理動作。

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

在執行階段,這會產生大致上等同於下列內容的效果

ts
function 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

或作用域運算(例如追蹤)時

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

varletconst 不同,using 宣告不支援解構。

nullundefined

請務必注意,該值可以是 nullundefined,在這種情況下,區塊結束時不會處置任何內容

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

大致上等同於

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

這允許你在宣告 using 宣告時有條件地取得資源,而不需要複雜的分支或重複。

定義一次性資源

您可以透過實作 Disposable 介面來指出您產生的類別或物件是一次性的

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 宣告

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

await using 宣告會呼叫並等待其值的 [Symbol.asyncDispose]() 方法,因為控制權會離開包含區塊。這允許非同步清除作業,例如資料庫交易執行回滾或提交,或檔案串流在關閉之前清除任何待處理的寫入作業到儲存體。

await 相同,await using 只能用於 async 函式或方法中,或模組的最上層。

定義非同步可處置資源

如同 using 依賴於 Disposable 的物件,await using 依賴於 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 宣告中的一部分 await 關鍵字僅表示資源的 處置await 的。它不會 await 值本身

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

await usingreturn

如果您在返回 Promise 的非同步函數中使用 await using 宣告,請務必注意此行為有一個小警告,即您沒有先對其進行 await

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

由於回傳的 Promise 沒有 await,因此 JavaScript 執行階段可能會回報未處理的拒絕,因為執行會暫停,同時 await 非同步的 x 處置,而沒有訂閱回傳的 Promise。不過,這並非 await using 獨有的問題,因為這也可能發生在使用 try..finally 的非同步函數中

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

為避免這種情況,建議您 await 回傳值,如果它可能是 Promise

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

usingawait usingforfor..of 陳述式中

usingawait using 都可以在 for 陳述式中使用

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

在這種情況下,x 的生命週期涵蓋整個 for 陳述式,並且僅在控制權因 breakreturnthrow 或迴圈條件為假而離開迴圈時才會處置。

除了 for 陳述式之外,這兩個宣告也可以在 for..of 陳述式中使用

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

在此,x 在迴圈的每次迭代結束時處置,然後使用下一個值重新初始化。當使用產生器逐一產生資源時,這特別有用。

usingawait using 在較舊的執行時期中

usingawait using 宣告可用於目標較舊的 ECMAScript 版本,只要您使用相容的 Symbol.dispose/Symbol.asyncDispose polyfill,例如 NodeJS 近期版本中預設提供的 polyfill。

TypeScript 文件是一個開源專案。透過 傳送 Pull Request ❤ 來協助我們改善這些頁面

此頁面的貢獻者
DRDaniel Rosenwasser (58)
OTOrta Therox (20)
NSNathan Shively-Sanders (9)
VRVimal Raghubir (3)
BCBrett Cannon (3)
24+

最後更新:2024 年 3 月 21 日