let 和 const 是 JavaScript 中變數宣告的兩個相對較新的概念。 如前所述,let 在某些方面類似於 var,但允許使用者避免 JavaScript 中常見的某些「陷阱」。
const 是 let 的擴充,它防止重新指派變數。
由於 TypeScript 是 JavaScript 的擴充,該語言自然支援 let 和 const。在這裡,我們將進一步說明這些新的宣告,以及它們為何比 var 更受青睞。
如果您曾經隨意使用過 JavaScript,下一節可能是個復習的好方法。如果您非常熟悉 JavaScript 中 var 宣告的所有怪癖,您可能會發現跳過比較容易。
var 宣告
在 JavaScript 中宣告變數傳統上一直使用 var 關鍵字。
tsvar a = 10;
正如您可能已經發現的,我們剛剛宣告了一個名為 a 的變數,其值為 10。
我們也可以在函式內宣告變數
tsfunction f() {var message = "Hello, world!";return message;}
我們也可以在其他函式內存取相同的變數
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。
tsfunction f() {var a = 1;a = 2;var b = g();a = 3;return b;function g() {return a;}}f(); // returns '2'
範圍規則
var 宣告對於習慣其他語言的人來說,有一些奇怪的範圍規則。以下為一個範例
tsfunction f(shouldInitialize: boolean) {if (shouldInitialize) {var x = 10;}return x;}f(true); // returns '10'f(false); // returns 'undefined'
有些讀者可能會對這個範例感到驚訝。變數 x 是宣告在 if 區塊中的,但我們卻可以在區塊外存取它。這是因為 var 宣告可以在其包含函式、模組、命名空間或全域範圍內的任何地方存取,而我們稍後會探討所有這些內容,不論包含區塊為何。有些人稱此為 var 範圍 或 函式範圍。參數也是函式範圍的。
這些範圍規則可能會導致幾種類型的錯誤。它們會加劇的一個問題是,多次宣告同一個變數並不會產生錯誤
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 參照同一個函式範圍變數。正如有經驗的開發人員現在所知,類似類型的錯誤會通過程式碼檢閱,並且會帶來無盡的挫折感。
變數擷取怪癖
花個幾秒鐘猜猜看以下程式片段的輸出結果是什麼
tsfor (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
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,但由於我們將它們命名為相同名稱,因此我們不必修改迴圈主體太多。
let 宣告
現在你已經發現 var 有些問題,這正是 let 陳述式被引進的原因。除了使用的關鍵字不同,let 陳述式與 var 陳述式的寫法相同。
tslet hello = "Hello!";
關鍵的不同點不在於語法,而在於語意,我們現在將深入探討。
區塊作用域
當使用 let 宣告變數時,它會使用一些人稱為詞法作用域或區塊作用域。與作用域會外洩到其包含函式的 var 宣告的變數不同,區塊作用域變數在其最近的包含區塊或 for 迴圈之外不可見。
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 陳述式的區塊。
在 catch 子句中宣告的變數也有類似的範圍規則。
tstry {throw "oh no!";} catch (e) {console.log("Oh well.");}// Error: 'e' doesn't exist hereconsole.log(e);
區塊範圍變數的另一個屬性是,它們在實際宣告之前無法讀取或寫入。雖然這些變數在其整個範圍內「存在」,但直到宣告為止的所有點都是其時間死區的一部分。這只是一個複雜的說法,表示您無法在 let 陳述式之前存取它們,而且幸運的是,TypeScript 會讓您知道這一點。
tsa++; // illegal to use 'a' before it's declared;let a;
需要注意的是,您仍然可以在宣告之前擷取區塊範圍變數。唯一的缺點是,在宣告之前呼叫該函式是非法的。如果鎖定 ES2015,現代執行時期會擲回錯誤;然而,目前 TypeScript 是允許的,不會將此報告為錯誤。
tsfunction foo() {// okay to capture 'a'return a;}// illegal call 'foo' before 'a' is declared// runtimes should throw an error herefoo();let a;
如需有關時間死區的更多資訊,請參閱 Mozilla Developer Network 上的相關內容。
重新宣告和陰影化
使用 var 宣告時,我們提到過宣告變數的次數並不重要;您只會得到一個。
tsfunction f(x) {var x;var x;if (true) {var x;}}
在上述範例中,x 的所有宣告實際上指的是相同的 x,這是完全有效的。這通常會成為錯誤的來源。謝天謝地,let 宣告沒有那麼寬容。
tslet x = 10;let x = 20; // error: can't re-declare 'x' in the same scope
TypeScript 不一定需要兩個變數都是區塊作用域,才能告訴我們有問題。
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'}
這並不是說區塊作用域變數永遠無法用函式作用域變數宣告。區塊作用域變數只需要在明顯不同的區塊中宣告即可。
tsfunction f(condition, x) {if (condition) {let x = 100;return x;}return x;}f(false, 0); // returns '0'f(true, 0); // returns '100'
在更巢狀的範圍中引入新名稱的行為稱為遮蔽。這有點像一把雙面刃,它可能會在意外遮蔽的情況下自行引入某些錯誤,同時也會防止某些錯誤。例如,想像我們使用 let 變數撰寫了先前的 sumMatrix 函式。
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。
為了撰寫更清晰的程式碼,通常應該避免遮蔽。雖然在某些情況下可能適合利用它,但你應該運用最佳判斷力。
區塊作用域變數擷取
當我們第一次接觸使用 var 宣告的變數擷取概念時,我們簡要介紹了變數在擷取後的作用方式。為了更直觀地了解這一點,每次執行範圍時,它都會建立一個變數「環境」。即使範圍內的所有內容都已執行完畢,該環境及其擷取的變數仍可以存在。
tsfunction theCityThatAlwaysSleeps() {let getCity;if (true) {let city = "Seattle";getCity = function () {return city;};}return getCity();}
由於我們從其環境中擷取了 city,因此儘管 if 區塊已執行完畢,我們仍然可以存取它。
回想一下我們較早的 setTimeout 範例,我們最後需要使用 IIFE 來擷取變數的狀態,以進行 for 迴圈的每次反覆運算。實際上,我們所做的就是為我們擷取的變數建立新的變數環境。這有點麻煩,但幸運的是,在 TypeScript 中你再也不必這麼做了。
當宣告為迴圈的一部分時,let 宣告具有截然不同的行為。這些宣告不僅僅為迴圈本身引入新的環境,它們還會為每次反覆運算建立新的範圍。由於這正是我們使用 IIFE 所做的,因此我們可以將舊的 setTimeout 範例變更為僅使用 let 宣告。
tsfor (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 宣告是宣告變數的另一種方式。
tsconst numLivesForCat = 9;
它們就像 let 宣告,但正如其名稱所暗示的,它們的值一旦繫結就無法變更。換句話說,它們具有與 let 相同的範圍規則,但你無法重新指定它們。
這不應與它們所引用的值不可變的概念混淆。
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。有關詳細資訊,請參閱 介面章節。
let 與 const
由於我們有兩種具有類似範圍語意的宣告,因此自然會想知道要使用哪一種。與大多數廣泛的問題一樣,答案是:視情況而定。
應用 最小權限原則,除了您計畫修改的宣告之外,所有宣告都應使用 const。其理由是,如果變數不需要寫入,則其他處理相同程式碼庫的人員不應自動能夠寫入物件,並且需要考慮是否真的需要重新指派變數。在推論資料流時,使用 const 也會使程式碼更具可預測性。
根據您的最佳判斷,並在適用的情況下,與您的團隊其他成員諮詢此事。
本手冊的大部分內容使用 let 宣告。
解構
TypeScript 擁有的另一項 ECMAScript 2015 功能是解構。有關完整參考,請參閱 Mozilla Developer Network 上的文章。在本節中,我們將提供簡要概述。
陣列解構
解構最簡單的形式是陣列解構賦值
tslet input = [1, 2];let [first, second] = input;console.log(first); // outputs 1console.log(second); // outputs 2
這會建立兩個新的變數,分別命名為 first 和 second。這相當於使用索引,但更為方便
tsfirst = input[0];second = input[1];
解構也適用於已宣告的變數
ts// swap variables[first, second] = [second, first];
以及函式的參數
tsfunction f([first, second]: [number, number]) {console.log(first);console.log(second);}f([1, 2]);
你可以使用語法 ... 為清單中剩下的項目建立一個變數
tslet [first, ...rest] = [1, 2, 3, 4];console.log(first); // outputs 1console.log(rest); // outputs [ 2, 3, 4 ]
當然,由於這是 JavaScript,你可以忽略你不在乎的尾端元素
tslet [first] = [1, 2, 3, 4];console.log(first); // outputs 1
或其他元素
tslet [, second, , fourth] = [1, 2, 3, 4];console.log(second); // outputs 2console.log(fourth); // outputs 4
元組解構
元組可以像陣列一樣解構;解構變數取得對應元組元素的型別
tslet tuple: [number, string, boolean] = [7, "hello", true];let [a, b, c] = tuple; // a: number, b: string, c: boolean
解構元組超出其元素範圍會產生錯誤
tslet [a, b, c, d] = tuple; // Error, no element at index 3
與陣列一樣,你可以使用 ... 解構元組的其餘部分,以取得較短的元組
tslet [a, ...bc] = tuple; // bc: [string, boolean]let [a, b, c, ...d] = tuple; // d: [], the empty tuple
或忽略尾隨元素或其他元素
tslet [a] = tuple; // a: numberlet [, b] = tuple; // b: string
物件解構
你也可以解構物件
tslet o = {a: "foo",b: 12,c: "bar",};let { a, b } = o;
這會從 o.a 和 o.b 建立新的變數 a 和 b。請注意,如果你不需要 c,你可以略過它。
與陣列解構一樣,你可以不宣告就進行指派
ts({ a, b } = { a: "baz", b: 101 });
請注意,我們必須用括號將此陳述式包起來。JavaScript 通常會將 { 解析為區塊的開頭。
你可以使用語法 ... 為物件中其餘的項目建立一個變數
tslet { a, ...passthrough } = o;let total = passthrough.b + passthrough.c.length;
屬性重新命名
您也可以為屬性指定不同的名稱
tslet { a: newName1, b: newName2 } = o;
此處的語法開始令人困惑。您可以將 a: newName1 讀為「a 為 newName1」。方向為從左至右,就像您寫下
tslet newName1 = o.a;let newName2 = o.b;
令人困惑的是,此處的冒號並未表示類型。如果您指定類型,仍需要在整個解構後寫入
tslet { a: newName1, b: newName2 }: { a: string; b: number } = o;
預設值
預設值讓您可以在屬性未定義時指定預設值
tsfunction keepWholeObject(wholeObject: { a: string; b?: number }) {let { a, b = 1001 } = wholeObject;}
在此範例中,b? 表示 b 是選用的,因此可能是 undefined。keepWholeObject 現在有一個變數 wholeObject,以及屬性 a 和 b,即使 b 是未定義的。
函式宣告
解構也在函式宣告中運作。對於簡單的案例,這是很直接的
tstype C = { a: string; b?: number };function f({ a, b }: C): void {// ...}
但為參數指定預設值比較常見,而且使用解構來正確取得預設值可能會很棘手。首先,你需要記得在預設值之前放置樣式。
tsfunction f({ a = "", b = 0 } = {}): void {// ...}f();
上面的程式碼片段是類型推論的範例,在手冊的前面有說明。
接著,你需要記得為解構屬性中的選用屬性提供預設值,而不是主初始化項。請記住 C 是使用選用的 b 定義的
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
小心使用解構。正如前一個範例所示,除了最簡單的解構運算式之外,其他都令人困惑。對於深度巢狀的解構尤其如此,即使沒有堆疊重新命名、預設值和類型註解,也很難理解。請盡量保持解構運算式簡潔。你總是可以用自己產生的解構來撰寫指定。
散佈
展開運算子與解構相反。它允許你將陣列展開到另一個陣列,或將物件展開到另一個物件。例如
tslet first = [1, 2];let second = [3, 4];let bothPlus = [0, ...first, ...second, 5];
這會讓 bothPlus 的值變成 [0, 1, 2, 3, 4, 5]。展開會建立 first 和 second 的淺層拷貝。它們不會因為展開而改變。
你也可以展開物件
tslet defaults = { food: "spicy", price: "$$", ambiance: "noisy" };let search = { ...defaults, food: "rich" };
現在 search 是 { food: "rich", price: "$$", ambiance: "noisy" }。物件展開比陣列展開複雜。就像陣列展開,它從左到右進行,但結果仍然是一個物件。這表示在展開物件中較後出現的屬性會覆寫較早出現的屬性。所以如果我們修改前一個範例,在最後展開
tslet defaults = { food: "spicy", price: "$$", ambiance: "noisy" };let search = { food: "rich", ...defaults };
那麼 defaults 中的 food 屬性會覆寫 food: "rich",這不是我們在這個情況下想要的。
物件展開也有其他幾個令人驚訝的限制。首先,它只包含物件的 自有可列舉屬性。基本上,這表示當你展開物件實例時,你會失去方法
tsclass C {p = 12;m() {}}let c = new C();let clone = { ...c };clone.p; // okclone.m(); // error!
其次,TypeScript 編譯器不允許從泛型函數展開類型參數。這項功能預計會在語言的未來版本中出現。
using 宣告
using 宣告是 JavaScript 中即將推出的功能,是 第 3 階段明確資源管理 提案的一部分。using 宣告很像 const 宣告,但它將繫結到宣告的值的生命週期與變數的作用域結合在一起。
當控制離開包含 using 宣告的區塊時,宣告值的 [Symbol.dispose]() 方法會被執行,這允許該值執行清理動作。
tsfunction f() {using x = new C();doSomethingWith(x);} // `x[Symbol.dispose]()` is called
在執行階段,這會產生大致上等同於下列內容的效果
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
或作用域運算(例如追蹤)時
tsfunction f() {using activity = new TraceActivity("f"); // traces entry into function// ...} // traces exit of function
與 var、let 和 const 不同,using 宣告不支援解構。
null 和 undefined
請務必注意,該值可以是 null 或 undefined,在這種情況下,區塊結束時不會處置任何內容
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 宣告
tsasync 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 backtx.success = true;// now the transaction will commit}
await using 與 await
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 using 與 return
如果您在返回 Promise 的非同步函數中使用 await using 宣告,請務必注意此行為有一個小警告,即您沒有先對其進行 await
tsfunction 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 的非同步函數中
tsasync function f() {try {return g(); // also reports an unhandled rejection}finally {await somethingElse();}}
為避免這種情況,建議您 await 回傳值,如果它可能是 Promise
tsasync function f() {await using x = new C();return await g();}
using 和 await using 在 for 和 for..of 陳述式中
using 和 await using 都可以在 for 陳述式中使用
tsfor (using x = getReader(); !x.eof; x.next()) {// ...}
在這種情況下,x 的生命週期涵蓋整個 for 陳述式,並且僅在控制權因 break、return、throw 或迴圈條件為假而離開迴圈時才會處置。
除了 for 陳述式之外,這兩個宣告也可以在 for..of 陳述式中使用
tsfunction * g() {yield createResource1();yield createResource2();}for (using x of g()) {// ...}
在此,x 在迴圈的每次迭代結束時處置,然後使用下一個值重新初始化。當使用產生器逐一產生資源時,這特別有用。
using 和 await using 在較舊的執行時期中
using 和 await using 宣告可用於目標較舊的 ECMAScript 版本,只要您使用相容的 Symbol.dispose/Symbol.asyncDispose polyfill,例如 NodeJS 近期版本中預設提供的 polyfill。