TypeScript 是一種由微軟開發的自由和開源的程式語言。它是 JavaScript 的一個超集,而且本質上向這個語言新增了可選的靜態型別和基於類的物件導向程式設計。TypeScript 提供最新的和不斷髮展的 JavaScript 特性,包括那些來自 2015 年的 ECMAScript 和未來的提案中的特性,比如非同步功能和 Decorators,以幫助建立健壯的元件。
阿寶哥第一次使用 TypeScript 是在 Angular 2.x 專案中,那時候 TypeScript 還沒有進入大眾的視野。然而現在學習 TypeScript 的小夥伴越來越多了,本文阿寶哥將分享這些年在學習 TypeScript 過程中,曾被困擾過的一些 TS 問題,希望本文對學習 TypeScript 的小夥伴能有一些幫助。
好的,下面我們來開始介紹第一個問題 —— 如何在 window 物件上顯式設定屬性。
一、如何在 window 物件上顯式設定屬性
對於使用過 JavaScript 的開發者來說,對於 window.MyNamespace = window.MyNamespace || {};
這行程式碼並不會陌生。為了避免開發過程中出現衝突,我們一般會為某些功能設定獨立的名稱空間。
然而,在 TS 中對於 window.MyNamespace = window.MyNamespace || {};
這行程式碼,TS 編譯器會提示以下異常資訊:
Property 'MyNamespace' does not exist on type 'Window & typeof globalThis'.(2339)
以上異常資訊是說在 Window & typeof globalThis
交叉型別上不存在 MyNamespace
屬性。那麼如何解決這個問題呢?最簡單的方式就是使用型別斷言:
(window as any).MyNamespace = {};
雖然使用 any 大法可以解決上述問題,但更好的方式是擴充套件 lib.dom.d.ts
檔案中的 Window
介面來解決上述問題,具體方式如下:
declare interface Window {
MyNamespace: any;
}
window.MyNamespace = window.MyNamespace || {};
下面我們再來看一下 lib.dom.d.ts
檔案中宣告的 Window
介面:
/**
* A window containing a DOM document; the document property
* points to the DOM document loaded in that window.
*/
interface Window extends EventTarget, AnimationFrameProvider, GlobalEventHandlers,
WindowEventHandlers, WindowLocalStorage, WindowOrWorkerGlobalScope, WindowSessionStorage {
// 已省略大部分內容
readonly devicePixelRatio: number;
readonly document: Document;
readonly top: Window;
readonly window: Window & typeof globalThis;
addEventListener(type: string, listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof WindowEventMap>(type: K,
listener: (this: Window, ev: WindowEventMap[K]) => any,
options?: boolean | EventListenerOptions): void;
[index: number]: Window;
}
在上面我們宣告瞭兩個相同名稱的 Window
介面,這時並不會造成衝突。TypeScript 會自動進行介面合併,即把雙方的成員放到一個同名的介面中。
二、如何為物件動態分配屬性
在 JavaScript 中,我們可以很容易地為物件動態分配屬性,比如:
let developer = {};
developer.name = "semlinker";
以上程式碼在 JavaScript 中可以正常執行,但在 TypeScript 中,編譯器會提示以下異常資訊:
Property 'name' does not exist on type '{}'.(2339)
{}
型別表示一個沒有包含成員的物件,所以該型別沒有包含 name
屬性。為了解決這個問題,我們可以宣告一個 LooseObject
型別:
interface LooseObject {
[key: string]: any
}
該型別使用 索引簽名 的形式描述 LooseObject
型別可以接受 key 型別是字串,值的型別是 any 型別的欄位。有了 LooseObject
型別之後,我們就可以通過以下方式來解決上述問題:
interface LooseObject {
[key: string]: any
}
let developer: LooseObject = {};
developer.name = "semlinker";
對於 LooseObject
型別來說,它的約束是很寬鬆的。在一些應用場景中,我們除了希望能支援動態的屬性之外,也希望能夠宣告一些必選和可選的屬性。
比如對於一個表示開發者的 Developer 介面來說,我們希望它的 name 屬性是必填,而 age 屬性是可選的,此外還支援動態地設定字串型別的屬性。針對這個需求我們可以這樣做:
interface Developer {
name: string;
age?: number;
[key: string]: any
}
let developer: Developer = { name: "semlinker" };
developer.age = 30;
developer.city = "XiaMen";
其實除了使用 索引簽名 之外,我們也可以使用 TypeScript 內建的工具型別 Record
來定義 Developer 介面:
// type Record<K extends string | number | symbol, T> = { [P in K]: T; }
interface Developer extends Record<string, any> {
name: string;
age?: number;
}
let developer: Developer = { name: "semlinker" };
developer.age = 30;
developer.city = "XiaMen";
三、如何理解泛型中的 <T>
對於剛接觸 TypeScript 泛型的讀者來說,首次看到 <T>
語法會感到陌生。其實它沒有什麼特別,就像傳遞引數一樣,我們傳遞了我們想要用於特定函式呼叫的型別。
參考上面的圖片,當我們呼叫 identity<Number>(1)
,Number
型別就像引數 1
一樣,它將在出現 T
的任何位置填充該型別。圖中 <T>
內部的 T
被稱為型別變數,它是我們希望傳遞給 identity 函式的型別佔位符,同時它被分配給 value
引數用來代替它的型別:此時 T
充當的是型別,而不是特定的 Number 型別。
其中 T
代表 Type,在定義泛型時通常用作第一個型別變數名稱。但實際上 T
可以用任何有效名稱代替。除了 T
之外,以下是常見泛型變數代表的意思:
- K(Key):表示物件中的鍵型別;
- V(Value):表示物件中的值型別;
- E(Element):表示元素型別。
其實並不是只能定義一個型別變數,我們可以引入希望定義的任何數量的型別變數。比如我們引入一個新的型別變數 U
,用於擴充套件我們定義的 identity
函式:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity<Number, string>(68, "Semlinker"));
除了為型別變數顯式設定值之外,一種更常見的做法是使編譯器自動選擇這些型別,從而使程式碼更簡潔。我們可以完全省略尖括號,比如:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity(68, "Semlinker"));
對於上述程式碼,編譯器足夠聰明,能夠知道我們的引數型別,並將它們賦值給 T 和 U,而不需要開發人員顯式指定它們。
四、如何理解裝飾器的作用
在 TypeScript 中裝飾器分為類裝飾器、屬性裝飾器、方法裝飾器和引數裝飾器四大類。裝飾器的本質是一個函式,通過裝飾器我們可以方便地定義與物件相關的後設資料。
比如在 ionic-native 專案中,它使用 Plugin
裝飾器來定義 IonicNative 中 Device 外掛的相關資訊:
@Plugin({
pluginName: 'Device',
plugin: 'cordova-plugin-device',
pluginRef: 'device',
repo: 'https://github.com/apache/cordova-plugin-device',
platforms: ['Android', 'Browser', 'iOS', 'macOS', 'Windows'],
})
@Injectable()
export class Device extends IonicNativePlugin {}
在以上程式碼中 Plugin 函式被稱為裝飾器工廠,呼叫該函式之後會返回類裝飾器,用於裝飾 Device 類。Plugin 工廠函式的定義如下:
// https://github.com/ionic-team/ionic-native/blob/v3.x/src/%40ionic-native/core/decorators.ts
export function Plugin(config: PluginConfig): ClassDecorator {
return function(cls: any) {
// 把config物件中屬性,作為靜態屬性新增到cls類上
for (let prop in config) {
cls[prop] = config[prop];
}
cls['installed'] = function(printWarning?: boolean) {
return !!getPlugin(config.pluginRef);
};
// 省略其他內容
return cls;
};
}
通過觀察 Plugin 工廠函式的方法簽名,我們可以知道呼叫該函式之後會返回 ClassDecorator
型別的物件,其中 ClassDecorator
型別的宣告如下所示:
declare type ClassDecorator = <TFunction extends Function>(target: TFunction)
=> TFunction | void;
類裝飾器顧名思義,就是用來裝飾類的。它接收一個引數 —— target: TFunction
,表示被裝飾器的類。介紹完上述內容之後,我們來看另一個問題 @Plugin({...})
中的 @
符號有什麼用?
其實 @Plugin({...})
中的 @
符號只是語法糖,為什麼說是語法糖呢?這裡我們來看一下編譯生成的 ES5 程式碼:
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var Device = /** @class */ (function (_super) {
__extends(Device, _super);
function Device() {
return _super !== null && _super.apply(this, arguments) || this;
}
Device = __decorate([
Plugin({
pluginName: 'Device',
plugin: 'cordova-plugin-device',
pluginRef: 'device',
repo: 'https://github.com/apache/cordova-plugin-device',
platforms: ['Android', 'Browser', 'iOS', 'macOS', 'Windows'],
}),
Injectable()
], Device);
return Device;
}(IonicNativePlugin));
通過生成的程式碼可知,@Plugin({...})
和 @Injectable()
最終會被轉換成普通的方法呼叫,它們的呼叫結果最終會以陣列的形式作為引數傳遞給 __decorate
函式,而在 __decorate
函式內部會以 Device
類作為引數呼叫各自的型別裝飾器,從而擴充套件對應的功能。
此外,如果你有使用過 Angular,相信你對以下的程式碼並不會陌生。
const API_URL = new InjectionToken('apiUrl');
@Injectable()
export class HttpService {
constructor(
private httpClient: HttpClient,
@Inject(API_URL) private apiUrl: string
) {}
}
在 Injectable
類裝飾器修飾的 HttpService
類中,我們通過構造注入的方式注入了用於處理 HTTP 請求的 HttpClient
依賴物件。而通過 Inject
引數裝飾器注入了 API_URL
對應的物件,這種方式我們稱之為依賴注入(Dependency Injection)。
關於什麼是依賴注入,在 TS 中如何實現依賴注入功能,出於篇幅考慮,這裡阿寶哥就不繼續展開了。感興趣的小夥伴可以閱讀 “了不起的 IoC 與 DI” 這篇文章。
五、如何理解函式過載的作用
5.1 可愛又可恨的聯合型別
由於 JavaScript 是一個動態語言,我們通常會使用不同型別的引數來呼叫同一個函式,該函式會根據不同的引數而返回不同的型別的呼叫結果:
function add(x, y) {
return x + y;
}
add(1, 2); // 3
add("1", "2"); //"12"
由於 TypeScript 是 JavaScript 的超集,因此以上的程式碼可以直接在 TypeScript 中使用,但當 TypeScript 編譯器開啟 noImplicitAny
的配置項時,以上程式碼會提示以下錯誤資訊:
Parameter 'x' implicitly has an 'any' type.
Parameter 'y' implicitly has an 'any' type.
該資訊告訴我們引數 x 和引數 y 隱式具有 any
型別。為了解決這個問題,我們可以為引數設定一個型別。因為我們希望 add
函式同時支援 string 和 number 型別,因此我們可以定義一個 string | number
聯合型別,同時我們為該聯合型別取個別名:
type Combinable = string | number;
在定義完 Combinable 聯合型別後,我們來更新一下 add
函式:
function add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
為 add
函式的引數顯式設定型別之後,之前錯誤的提示訊息就消失了。那麼此時的 add
函式就完美了麼,我們來實際測試一下:
const result = add('semlinker', ' kakuqo');
result.split(' ');
在上面程式碼中,我們分別使用 'semlinker'
和 ' kakuqo'
這兩個字串作為引數呼叫 add 函式,並把呼叫結果儲存到一個名為 result
的變數上,這時候我們想當然的認為此時 result 的變數的型別為 string,所以我們就可以正常呼叫字串物件上的 split
方法。但這時 TypeScript 編譯器又出現以下錯誤資訊了:
Property 'split' does not exist on type 'Combinable'.
Property 'split' does not exist on type 'number'.
很明顯 Combinable
和 number
型別的物件上並不存在 split
屬性。問題又來了,那如何解決呢?這時我們就可以利用 TypeScript 提供的函式過載。
5.2 函式過載
函式過載或方法過載是使用相同名稱和不同引數數量或型別建立多個方法的一種能力。
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
// type Combinable = string | number;
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
在以上程式碼中,我們為 add 函式提供了多個函式型別定義,從而實現函式的過載。在 TypeScript 中除了可以過載普通函式之外,我們還可以過載類中的成員方法。
方法過載是指在同一個類中方法同名,引數不同(引數型別不同、引數個數不同或引數個數相同時引數的先後順序不同),呼叫時根據實參的形式,選擇與它匹配的方法執行操作的一種技術。所以類中成員方法滿足過載的條件是:在同一個類中,方法名相同且引數列表不同。下面我們來舉一個成員方法過載的例子:
class Calculator {
add(a: number, b: number): number;
add(a: string, b: string): string;
add(a: string, b: number): string;
add(a: number, b: string): string;
add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
}
const calculator = new Calculator();
const result = calculator.add('Semlinker', ' Kakuqo');
這裡需要注意的是,當 TypeScript 編譯器處理函式過載時,它會查詢過載列表,嘗試使用第一個過載定義。 如果匹配的話就使用這個。 因此,在定義過載的時候,一定要把最精確的定義放在最前面。另外在 Calculator 類中,add(a: Combinable, b: Combinable){ }
並不是過載列表的一部分,因此對於 add 成員方法來說,我們只定義了四個過載方法。
六、interfaces 與 type 之間有什麼區別
6.1 Objects/Functions
介面和型別別名都可以用來描述物件的形狀或函式簽名:
介面
interface Point {
x: number;
y: number;
}
interface SetPoint {
(x: number, y: number): void;
}
型別別名
type Point = {
x: number;
y: number;
};
type SetPoint = (x: number, y: number) => void;
6.2 Other Types
與介面型別不一樣,型別別名可以用於一些其他型別,比如原始型別、聯合型別和元組:
// primitive
type Name = string;
// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };
// union
type PartialPoint = PartialPointX | PartialPointY;
// tuple
type Data = [number, string];
6.3 Extend
介面和型別別名都能夠被擴充套件,但語法有所不同。此外,介面和型別別名不是互斥的。介面可以擴充套件型別別名,而反過來是不行的。
Interface extends interface
interface PartialPointX { x: number; }
interface Point extends PartialPointX {
y: number;
}
Type alias extends type alias
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };
Interface extends type alias
type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }
Type alias extends interface
interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };
6.4 Implements
類可以以相同的方式實現介面或型別別名,但類不能實現使用型別別名定義的聯合型別:
interface Point {
x: number;
y: number;
}
class SomePoint implements Point {
x = 1;
y = 2;
}
type Point2 = {
x: number;
y: number;
};
class SomePoint2 implements Point2 {
x = 1;
y = 2;
}
type PartialPoint = { x: number; } | { y: number; };
// A class can only implement an object type or
// intersection of object types with statically known members.
class SomePartialPoint implements PartialPoint { // Error
x = 1;
y = 2;
}
6.5 Declaration merging
與型別別名不同,介面可以定義多次,會被自動合併為單個介面。
interface Point { x: number; }
interface Point { y: number; }
const point: Point = { x: 1, y: 2 };
七、object, Object 和 {} 之間有什麼區別
7.1 object 型別
object 型別是:TypeScript 2.2 引入的新型別,它用於表示非原始型別。
// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
create(o: object | null): any;
// ...
}
const proto = {};
Object.create(proto); // OK
Object.create(null); // OK
Object.create(undefined); // Error
Object.create(1337); // Error
Object.create(true); // Error
Object.create("oops"); // Error
7.2 Object 型別
Object 型別:它是所有 Object 類的例項的型別,它由以下兩個介面來定義:
- Object 介面定義了 Object.prototype 原型物件上的屬性;
// node_modules/typescript/lib/lib.es5.d.ts
interface Object {
constructor: Function;
toString(): string;
toLocaleString(): string;
valueOf(): Object;
hasOwnProperty(v: PropertyKey): boolean;
isPrototypeOf(v: Object): boolean;
propertyIsEnumerable(v: PropertyKey): boolean;
}
- ObjectConstructor 介面定義了 Object 類的屬性。
// node_modules/typescript/lib/lib.es5.d.ts
interface ObjectConstructor {
/** Invocation via `new` */
new(value?: any): Object;
/** Invocation via function calls */
(value?: any): any;
readonly prototype: Object;
getPrototypeOf(o: any): any;
// ···
}
declare var Object: ObjectConstructor;
Object 類的所有例項都繼承了 Object 介面中的所有屬性。
7.3 {} 型別
{} 型別描述了一個沒有成員的物件。當你試圖訪問這樣一個物件的任意屬性時,TypeScript 會產生一個編譯時錯誤。
// Type {}
const obj = {};
// Error: Property 'prop' does not exist on type '{}'.
obj.prop = "semlinker";
但是,你仍然可以使用在 Object 型別上定義的所有屬性和方法,這些屬性和方法可通過 JavaScript 的原型鏈隱式地使用:
// Type {}
const obj = {};
// "[object Object]"
obj.toString();
八、數字列舉與字串列舉之間有什麼區別
8.1 數字列舉
在 JavaScript 中布林型別的變數含有有限範圍的值,即 true
和 false
。而在 TypeScript 中利用列舉,你也可以自定義相似的型別:
enum NoYes {
No,
Yes,
}
No
和 Yes
被稱為列舉 NoYes
的成員。每個列舉成員都有一個 name 和一個 value。數字列舉成員值的預設型別是 number 型別。也就是說,每個成員的值都是一個數字:
enum NoYes {
No,
Yes,
}
assert.equal(NoYes.No, 0);
assert.equal(NoYes.Yes, 1);
除了讓 TypeScript 為我們指定列舉成員的值之外,我們還可以手動賦值:
enum NoYes {
No = 0,
Yes = 1,
}
這種通過等號的顯式賦值稱為 initializer
。如果列舉中某個成員的值使用顯式方式賦值,但後續成員未顯示賦值, TypeScript 會基於當前成員的值加 1 作為後續成員的值。
8.2 字串列舉
除了數字列舉,我們還可以使用字串作為列舉成員值:
enum NoYes {
No = 'No',
Yes = 'Yes',
}
assert.equal(NoYes.No, 'No');
assert.equal(NoYes.Yes, 'Yes');
8.3 數字列舉 vs 字串列舉
數字列舉與字串列舉有什麼區別呢?這裡我們來分別看一下數字列舉和字串列舉編譯的結果:
數字列舉編譯結果
"use strict";
var NoYes;
(function (NoYes) {
NoYes[NoYes["No"] = 0] = "No";
NoYes[NoYes["Yes"] = 1] = "Yes";
})(NoYes || (NoYes = {}));
字串列舉編譯結果
"use strict";
var NoYes;
(function (NoYes) {
NoYes["No"] = "No";
NoYes["Yes"] = "Yes";
})(NoYes || (NoYes = {}));
通過觀察以上結果,我們知道數值列舉除了支援 從成員名稱到成員值 的普通對映之外,它還支援 從成員值到成員名稱 的反向對映。另外,對於純字串列舉,我們不能省略任何初始化程式。而數字列舉如果沒有顯式設定值時,則會使用預設值進行初始化。
8.4 為數字列舉分配越界值
講到數字列舉,這裡我們再來看個問題:
const enum Fonum {
a = 1,
b = 2
}
let value: Fonum = 12; // Ok
相信很多讀者看到 let value: Fonum = 12;
這一行,TS 編譯器並未提示任何錯誤會感到驚訝。很明顯數字 12 並不是 Fonum 列舉的成員。 為什麼會這樣呢?我們來看一下 TypeScript issues 26362 中 DanielRosenwasser 大佬的回答:
The behavior is motivated by bitwise operations. There are times when SomeFlag.Foo | SomeFlag.Bar is intended to produce another SomeFlag. Instead you end up with number, and you don't want to have to cast back to SomeFlag.該行為是由按位運算引起的。有時 SomeFlag.Foo | SomeFlag.Bar 用於生成另一個 SomeFlag。相反,你最終得到的是數字,並且你不想強制回退到 SomeFlag。
瞭解完上述內容,我們再來看一下 let value: Fonum = 12;
這個語句,該語句 TS 編譯器不會報錯,是因為數字 12 是可以通過 Fonum 已有的列舉成員計算而得。
let value: Fonum =
Fonum.a << Fonum.b << Fonum.a | Fonum.a << Fonum.b; // 12
九、使用 #
定義的私有欄位與 private
修飾符定義欄位有什麼區別
在 TypeScript 3.8 版本就開始支援 ECMAScript 私有欄位,使用方式如下:
class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
greet() {
console.log(`Hello, my name is ${this.#name}!`);
}
}
let semlinker = new Person("Semlinker");
semlinker.#name;
// ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.
與常規屬性(甚至使用 private
修飾符宣告的屬性)不同,私有欄位要牢記以下規則:
- 私有欄位以
#
字元開頭,有時我們稱之為私有名稱; - 每個私有欄位名稱都唯一地限定於其包含的類;
- 不能在私有欄位上使用 TypeScript 可訪問性修飾符(如 public 或 private);
- 私有欄位不能在包含的類之外訪問,甚至不能被檢測到。
說到這裡使用 #
定義的私有欄位與 private
修飾符定義欄位有什麼區別呢?現在我們先來看一個 private
的示例:
class Person {
constructor(private name: string){}
}
let person = new Person("Semlinker");
console.log(person.name);
在上面程式碼中,我們建立了一個 Person 類,該類中使用 private
修飾符定義了一個私有屬性 name
,接著使用該類建立一個 person
物件,然後通過 person.name
來訪問 person
物件的私有屬性,這時 TypeScript 編譯器會提示以下異常:
Property 'name' is private and only accessible within class 'Person'.(2341)
那如何解決這個異常呢?當然你可以使用型別斷言把 person 轉為 any 型別:
console.log((person as any).name);
通過這種方式雖然解決了 TypeScript 編譯器的異常提示,但是在執行時我們還是可以訪問到 Person
類內部的私有屬性,為什麼會這樣呢?我們來看一下編譯生成的 ES5 程式碼,也許你就知道答案了:
var Person = /** @class */ (function () {
function Person(name) {
this.name = name;
}
return Person;
}());
var person = new Person("Semlinker");
console.log(person.name);
這時相信有些小夥伴會好奇,在 TypeScript 3.8 以上版本通過 #
號定義的私有欄位編譯後會生成什麼程式碼:
class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
greet() {
console.log(`Hello, my name is ${this.#name}!`);
}
}
以上程式碼目標設定為 ES2015,會編譯生成以下程式碼:
"use strict";
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet)
|| function (receiver, privateMap, value) {
if (!privateMap.has(receiver)) {
throw new TypeError("attempted to set private field on non-instance");
}
privateMap.set(receiver, value);
return value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet)
|| function (receiver, privateMap) {
if (!privateMap.has(receiver)) {
throw new TypeError("attempted to get private field on non-instance");
}
return privateMap.get(receiver);
};
var _name;
class Person {
constructor(name) {
_name.set(this, void 0);
__classPrivateFieldSet(this, _name, name);
}
greet() {
console.log(`Hello, my name is ${__classPrivateFieldGet(this, _name)}!`);
}
}
_name = new WeakMap();
通過觀察上述程式碼,使用 #
號定義的 ECMAScript 私有欄位,會通過 WeakMap
物件來儲存,同時編譯器會生成 __classPrivateFieldSet
和 __classPrivateFieldGet
這兩個方法用於設定值和獲取值。
以上提到的這些問題,相信一些小夥伴們在學習 TS 過程中也遇到了。如果有表述不清楚的地方,歡迎你們給我留言或直接與我交流。之後,阿寶哥還會繼續補充和完善這一方面的內容,感興趣的小夥伴可以一起參與喲。
十、參考資源
- how-do-you-explicitly-set-a-new-property-on-window-in-typescript
- how-do-i-dynamically-assign-properties-to-an-object-in-typescript
- typescript-interfaces-vs-types