新聞中心
本文轉(zhuǎn)載自微信公眾號(hào)「全棧修仙之路」,作者阿寶哥 。轉(zhuǎn)載本文請(qǐng)聯(lián)系全棧修仙之路公眾號(hào)。

我們提供的服務(wù)有:成都網(wǎng)站制作、網(wǎng)站設(shè)計(jì)、外貿(mào)網(wǎng)站建設(shè)、微信公眾號(hào)開發(fā)、網(wǎng)站優(yōu)化、網(wǎng)站認(rèn)證、華寧ssl等。為成百上千企事業(yè)單位解決了網(wǎng)站和推廣的問(wèn)題。提供周到的售前咨詢和貼心的售后服務(wù),是有科學(xué)管理、有技術(shù)的華寧網(wǎng)站制作公司
TypeScript 是一種由微軟開發(fā)的自由和開源的編程語(yǔ)言。它是 JavaScript 的一個(gè)超集,而且本質(zhì)上向這個(gè)語(yǔ)言添加了可選的靜態(tài)類型和基于類的面向?qū)ο缶幊獭?/p>
TypeScript 提供最新的和不斷發(fā)展的 JavaScript 特性,包括那些來(lái)自 2015 年的 ECMAScript 和未來(lái)的提案中的特性,比如異步功能和 Decorators,以幫助建立健壯的組件。
阿寶哥第一次使用 TypeScript 是在 Angular 2.x 項(xiàng)目中,那時(shí)候 TypeScript 還沒有進(jìn)入大眾的視野。然而現(xiàn)在學(xué)習(xí) TypeScript 的小伙伴越來(lái)越多了,本文阿寶哥將分享這些年在學(xué)習(xí) TypeScript 過(guò)程中,曾被困擾過(guò)的一些 TS 問(wèn)題,希望本文對(duì)學(xué)習(xí) TypeScript 的小伙伴能有一些幫助。
好的,下面我們來(lái)開始介紹第一個(gè)問(wèn)題 —— 如何在 window 對(duì)象上顯式設(shè)置屬性。
一、如何在 window 對(duì)象上顯式設(shè)置屬性
對(duì)于使用過(guò) JavaScript 的開發(fā)者來(lái)說(shuō),對(duì)于 window.MyNamespace = window.MyNamespace || {}; 這行代碼并不會(huì)陌生。為了避免開發(fā)過(guò)程中出現(xiàn)沖突,我們一般會(huì)為某些功能設(shè)置獨(dú)立的命名空間。
然而,在 TS 中對(duì)于 window.MyNamespace = window.MyNamespace || {}; 這行代碼,TS 編譯器會(huì)提示以下異常信息:
- Property 'MyNamespace' does not exist on type 'Window & typeof globalThis'.(2339)
以上異常信息是說(shuō)在 Window & typeof globalThis 交叉類型上不存在MyNamespace 屬性。那么如何解決這個(gè)問(wèn)題呢?最簡(jiǎn)單的方式就是使用類型斷言:
- (window as any).MyNamespace = {};
雖然使用 any 大法可以解決上述問(wèn)題,但更好的方式是擴(kuò)展 lib.dom.d.ts 文件中的Window 接口來(lái)解決上述問(wèn)題,具體方式如下:
- declare interface Window {
- MyNamespace: any;
- }
- window.MyNamespace = window.MyNamespace || {};
下面我們?cè)賮?lái)看一下 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 {
- // 已省略大部分內(nèi)容
- readonly devicePixelRatio: number;
- readonly document: Document;
- readonly top: Window;
- readonly window: Window & typeof globalThis;
- addEventListener(type: string, listener: EventListenerOrEventListenerObject,
- options?: boolean | AddEventListenerOptions): void;
- removeEventListener
(type: K, - listener: (this: Window, ev: WindowEventMap[K]) => any,
- options?: boolean | EventListenerOptions): void;
- [index: number]: Window;
- }
在上面我們聲明了兩個(gè)相同名稱的 Window 接口,這時(shí)并不會(huì)造成沖突。TypeScript 會(huì)自動(dòng)進(jìn)行接口合并,即把雙方的成員放到一個(gè)同名的接口中。
二、如何為對(duì)象動(dòng)態(tài)分配屬性
在 JavaScript 中,我們可以很容易地為對(duì)象動(dòng)態(tài)分配屬性,比如:
- let developer = {};
- developer.name = "semlinker";
以上代碼在 JavaScript 中可以正常運(yùn)行,但在 TypeScript 中,編譯器會(huì)提示以下異常信息:
- Property 'name' does not exist on type '{}'.(2339)
{} 類型表示一個(gè)沒有包含成員的對(duì)象,所以該類型沒有包含 name 屬性。為了解決這個(gè)問(wèn)題,我們可以聲明一個(gè) LooseObject 類型:
- interface LooseObject {
- [key: string]: any
- }
該類型使用 索引簽名 的形式描述 LooseObject 類型可以接受 key 類型是字符串,值的類型是 any 類型的字段。有了 LooseObject 類型之后,我們就可以通過(guò)以下方式來(lái)解決上述問(wèn)題:
- interface LooseObject {
- [key: string]: any
- }
- let developer: LooseObject = {};
- developer.name = "semlinker";
對(duì)于 LooseObject 類型來(lái)說(shuō),它的約束是很寬松的。在一些應(yīng)用場(chǎng)景中,我們除了希望能支持動(dòng)態(tài)的屬性之外,也希望能夠聲明一些必選和可選的屬性。
比如對(duì)于一個(gè)表示開發(fā)者的 Developer 接口來(lái)說(shuō),我們希望它的 name 屬性是必填,而 age 屬性是可選的,此外還支持動(dòng)態(tài)地設(shè)置字符串類型的屬性。針對(duì)這個(gè)需求我們可以這樣做:
- interface Developer {
- name: string;
- age?: number;
- [key: string]: any
- }
- let developer: Developer = { name: "semlinker" };
- developer.age = 30;
- developer.city = "XiaMen";
其實(shí)除了使用 索引簽名 之外,我們也可以使用 TypeScript 內(nèi)置的工具類型 Record來(lái)定義 Developer 接口:
- // type Record
= { [P in K]: T; } - interface Developer extends Record
{ - name: string;
- age?: number;
- }
- let developer: Developer = { name: "semlinker" };
- developer.age = 30;
- developer.city = "XiaMen";
三、如何理解泛型中的
對(duì)于剛接觸 TypeScript 泛型的讀者來(lái)說(shuō),首次看到 語(yǔ)法會(huì)感到陌生。其實(shí)它沒有什么特別,就像傳遞參數(shù)一樣,我們傳遞了我們想要用于特定函數(shù)調(diào)用的類型。
參考上面的圖片,當(dāng)我們調(diào)用 identity (1) ,Number 類型就像參數(shù) 1 一樣,它將在出現(xiàn) T 的任何位置填充該類型。圖中 內(nèi)部的 T 被稱為類型變量,它是我們希望傳遞給 identity 函數(shù)的類型占位符,同時(shí)它被分配給 value 參數(shù)用來(lái)代替它的類型:此時(shí) T 充當(dāng)?shù)氖穷愋?,而不是特定?Number 類型。
其中 T 代表 Type,在定義泛型時(shí)通常用作第一個(gè)類型變量名稱。但實(shí)際上 T 可以用任何有效名稱代替。除了 T 之外,以下是常見泛型變量代表的意思:
- K(Key):表示對(duì)象中的鍵類型;
- V(Value):表示對(duì)象中的值類型;
- E(Element):表示元素類型。
其實(shí)并不是只能定義一個(gè)類型變量,我們可以引入希望定義的任何數(shù)量的類型變量。比如我們引入一個(gè)新的類型變量 U,用于擴(kuò)展我們定義的 identity 函數(shù):
- function identity
(value: T, message: U) : T { - console.log(message);
- return value;
- }
- console.log(identity
(68, "Semlinker"));
除了為類型變量顯式設(shè)定值之外,一種更常見的做法是使編譯器自動(dòng)選擇這些類型,從而使代碼更簡(jiǎn)潔。我們可以完全省略尖括號(hào),比如:
- function identity
(value: T, message: U) : T { - console.log(message);
- return value;
- }
- console.log(identity(68, "Semlinker"));
對(duì)于上述代碼,編譯器足夠聰明,能夠知道我們的參數(shù)類型,并將它們賦值給 T 和 U,而不需要開發(fā)人員顯式指定它們。
四、如何理解裝飾器的作用
在 TypeScript 中裝飾器分為類裝飾器、屬性裝飾器、方法裝飾器和參數(shù)裝飾器四大類。裝飾器的本質(zhì)是一個(gè)函數(shù),通過(guò)裝飾器我們可以方便地定義與對(duì)象相關(guān)的元數(shù)據(jù)。
比如在 ionic-native 項(xiàng)目中,它使用 Plugin 裝飾器來(lái)定義 IonicNative 中 Device 插件的相關(guān)信息:
- @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 函數(shù)被稱為裝飾器工廠,調(diào)用該函數(shù)之后會(huì)返回類裝飾器,用于裝飾 Device 類。Plugin 工廠函數(shù)的定義如下:
- // 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對(duì)象中屬性,作為靜態(tài)屬性添加到cls類上
- for (let prop in config) {
- cls[prop] = config[prop];
- }
- cls['installed'] = function(printWarning?: boolean) {
- return !!getPlugin(config.pluginRef);
- };
- // 省略其他內(nèi)容
- return cls;
- };
- }
通過(guò)觀察 Plugin 工廠函數(shù)的方法簽名,我們可以知道調(diào)用該函數(shù)之后會(huì)返回 ClassDecorator 類型的對(duì)象,其中 ClassDecorator 類型的聲明如下所示:
- declare type ClassDecorator =
(target: TFunction) - => TFunction | void;
類裝飾器顧名思義,就是用來(lái)裝飾類的。它接收一個(gè)參數(shù) —— target: TFunction,表示被裝飾器的類。介紹完上述內(nèi)容之后,我們來(lái)看另一個(gè)問(wèn)題 @Plugin({...}) 中的 @ 符號(hào)有什么用?
其實(shí) @Plugin({...}) 中的 @ 符號(hào)只是語(yǔ)法糖,為什么說(shuō)是語(yǔ)法糖呢?這里我們來(lái)看一下編譯生成的 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));
通過(guò)生成的代碼可知,@Plugin({...}) 和 @Injectable() 最終會(huì)被轉(zhuǎn)換成普通的方法調(diào)用,它們的調(diào)用結(jié)果最終會(huì)以數(shù)組的形式作為參數(shù)傳遞給 __decorate 函數(shù),而在 __decorate 函數(shù)內(nèi)部會(huì)以 Device 類作為參數(shù)調(diào)用各自的類型裝飾器,從而擴(kuò)展對(duì)應(yīng)的功能。
此外,如果你有使用過(guò) Angular,相信你對(duì)以下代碼并不會(huì)陌生。
- const API_URL = new InjectionToken('apiUrl');
- @Injectable()
- export class HttpService {
- constructor(
- private httpClient: HttpClient,
- @Inject(API_URL) private apiUrl: string
- ) {}
- }
在 Injectable 類裝飾器修飾的 HttpService 類中,我們通過(guò)構(gòu)造注入的方式注入了用于處理 HTTP 請(qǐng)求的 HttpClient 依賴對(duì)象。而通過(guò) Inject 參數(shù)裝飾器注入了API_URL 對(duì)應(yīng)的對(duì)象,這種方式我們稱之為依賴注入(Dependency Injection)。
關(guān)于什么是依賴注入,在 TS 中如何實(shí)現(xiàn)依賴注入功能,出于篇幅考慮,這里阿寶哥就不繼續(xù)展開了。感興趣的小伙伴可以閱讀 “了不起的 IoC 與 DI” 這篇文章。
五、如何理解函數(shù)重載的作用
5.1 可愛又可恨的聯(lián)合類型
由于 JavaScript 是一個(gè)動(dòng)態(tài)語(yǔ)言,我們通常會(huì)使用不同類型的參數(shù)來(lái)調(diào)用同一個(gè)函數(shù),該函數(shù)會(huì)根據(jù)不同的參數(shù)而返回不同的類型的調(diào)用結(jié)果:
- function add(x, y) {
- return x + y;
- }
- add(1, 2); // 3
- add("1", "2"); //"12"
由于 TypeScript 是 JavaScript 的超集,因此以上的代碼可以直接在 TypeScript 中使用,但當(dāng) TypeScript 編譯器開啟 noImplicitAny 的配置項(xiàng)時(shí),以上代碼會(huì)提示以下錯(cuò)誤信息:
- Parameter 'x' implicitly has an 'any' type.
- Parameter 'y' implicitly has an 'any' type.
該信息告訴我們參數(shù) x 和參數(shù) y 隱式具有 any 類型。為了解決這個(gè)問(wèn)題,我們可以為參數(shù)設(shè)置一個(gè)類型。因?yàn)槲覀兿M?add 函數(shù)同時(shí)支持 string 和 number 類型,因此我們可以定義一個(gè) string | number 聯(lián)合類型,同時(shí)我們?yōu)樵撀?lián)合類型取個(gè)別名:
- type Combinable = string | number;
在定義完 Combinable 聯(lián)合類型后,我們來(lái)更新一下 add 函數(shù):
- function add(a: Combinable, b: Combinable) {
- if (typeof a === 'string' || typeof b === 'string') {
- return a.toString() + b.toString();
- }
- return a + b;
- }
為 add 函數(shù)的參數(shù)顯式設(shè)置類型之后,之前錯(cuò)誤的提示消息就消失了。那么此時(shí)的 add 函數(shù)就完美了么,我們來(lái)實(shí)際測(cè)試一下:
- const result = add('semlinker', ' kakuqo');
- result.split(' ');
在上面代碼中,我們分別使用 'semlinker' 和 ' kakuqo' 這兩個(gè)字符串作為參數(shù)調(diào)用 add 函數(shù),并把調(diào)用結(jié)果保存到一個(gè)名為 result 的變量上,這時(shí)候我們想當(dāng)然的認(rèn)為此時(shí) result 的變量的類型為 string,所以我們就可以正常調(diào)用字符串對(duì)象上的 split 方法。但這時(shí) TypeScript 編譯器又出現(xiàn)以下錯(cuò)誤信息了:
- Property 'split' does not exist on type 'Combinable'.
- Property 'split' does not exist on type 'number'.
很明顯 Combinable 和 number 類型的對(duì)象上并不存在 split 屬性。問(wèn)題又來(lái)了,那如何解決呢?這時(shí)我們就可以利用 TypeScript 提供的函數(shù)重載。
5.2 函數(shù)重載
函數(shù)重載或方法重載是使用相同名稱和不同參數(shù)數(shù)量或類型創(chuàng)建多個(gè)方法的一種能力。
- 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;
- }
在以上代碼中,我們?yōu)?add 函數(shù)提供了多個(gè)函數(shù)類型定義,從而實(shí)現(xiàn)函數(shù)的重載。在 TypeScript 中除了可以重載普通函數(shù)之外,我們還可以重載類中的成員方法。
方法重載是指在同一個(gè)類中方法同名,參數(shù)不同(參數(shù)類型不同、參數(shù)個(gè)數(shù)不同或參數(shù)個(gè)數(shù)相同時(shí)參數(shù)的先后順序不同),調(diào)用時(shí)根據(jù)實(shí)參的形式,選擇與它匹配的方法執(zhí)行操作的一種技術(shù)。所以類中成員方法滿足重載的條件是:在同一個(gè)類中,方法名相同且參數(shù)列表不同。下面我們來(lái)舉一個(gè)成員方法重載的例子:
- 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');
這里需要注意的是,當(dāng) TypeScript 編譯器處理函數(shù)重載時(shí),它會(huì)查找重載列表,嘗試使用第一個(gè)重載定義。 如果匹配的話就使用這個(gè)。 因此,在定義重載的時(shí)候,一定要把最精確的定義放在最前面。另外在 Calculator 類中,add(a: Combinable, b: Combinable){ } 并不是重載列表的一部分,因此對(duì)于 add 成員方法來(lái)說(shuō),我們只定義了四個(gè)重載方法。
六、interfaces 與 type 之間有什么區(qū)別
6.1 Objects/Functions
接口和類型別名都可以用來(lái)描述對(duì)象的形狀或函數(shù)簽名:
接口
- 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
與接口類型不一樣,類型別名可以用于一些其他類型,比如原始類型、聯(lián)合類型和元組:
- // 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
接口和類型別名都能夠被擴(kuò)展,但語(yǔ)法有所不同。此外,接口和類型別名不是互斥的。接口可以擴(kuò)展類型別名,而反過(guò)來(lái)是不行的。
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
類可以以相同的方式實(shí)現(xiàn)接口或類型別名,但類不能實(shí)現(xiàn)使用類型別名定義的聯(lián)合類型:
- 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
與類型別名不同,接口可以定義多次,會(huì)被自動(dòng)合并為單個(gè)接口。
- interface Point { x: number; }
- interface Point { y: number; }
- const point: Point = { x: 1, y: 2 };
七、object, Object 和 {} 之間有什么區(qū)別
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 類的實(shí)例的類型,它由以下兩個(gè)接口來(lái)定義:
- Object 接口定義了 Object.prototype 原型對(duì)象上的屬性;
- // 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 類的所有實(shí)例都繼承了 Object 接口中的所有屬性。
7.3 {} 類型
{} 類型描述了一個(gè)沒有成員的對(duì)象。當(dāng)你試圖訪問(wèn)這樣一個(gè)對(duì)象的任意屬性時(shí),TypeScript 會(huì)產(chǎn)生一個(gè)編譯時(shí)錯(cuò)誤。
- // Type {}
- const obj = {};
- // Error: Property 'prop' does not exist on type '{}'.
- obj.prop = "semlinker";
但是,你仍然可以使用在 Object 類型上定義的所有屬性和方法,這些屬性和方法可通過(guò) JavaScript 的原型鏈隱式地使用:
- // Type {}
- const obj = {};
- // "[object Object]"
- obj.toString();
八、數(shù)字枚舉與字符串枚舉之間有什么區(qū)別
8.1 數(shù)字枚舉
在 JavaScript 中布爾類型的變量含有有限范圍的值,即 true 和 false。而在 TypeScript 中利用枚舉,你也可以自定義相似的類型:
- enum NoYes {
- No,
- Yes,
- }
No 和 Yes 被稱為枚舉 NoYes 的成員。每個(gè)枚舉成員都有一個(gè) name 和一個(gè) value。數(shù)字枚舉成員值的默認(rèn)類型是 number 類型。也就是說(shuō),每個(gè)成員的值都是一個(gè)數(shù)字:
- enum NoYes {
- No,
- Yes,
- }
- assert.equal(NoYes.No, 0);
- assert.equal(NoYes.Yes, 1);
除了讓 TypeScript 為我們指定枚舉成員的值之外,我們還可以手動(dòng)賦值:
- enum NoYes {
- No = 0,
- Yes = 1,
- }
這種通過(guò)等號(hào)的顯式賦值稱為 initializer。如果枚舉中某個(gè)成員的值使用顯式方式賦值,但后續(xù)成員未顯示賦值, TypeScript 會(huì)基于當(dāng)前成員的值加 1 作為后續(xù)成員的值。
8.2 字符串枚舉
除了數(shù)字枚舉,我們還可以使用字符串作為枚舉成員值:
- enum NoYes {
- No = 'No',
- Yes = 'Yes',
- }
- assert.equal(NoYes.No, 'No');
- assert.equal(NoYes.Yes, 'Yes');
8.3 數(shù)字枚舉 vs 字符串枚舉
數(shù)字枚舉與字符串枚舉有什么區(qū)別呢?這里我們來(lái)分別看一下數(shù)字枚舉和字符串枚舉編譯的結(jié)果:
數(shù)字枚舉編譯結(jié)果
- "use strict";
- var NoYes;
- (function (NoYes) {
- NoYes[NoYes["No"] = 0] = "No";
- NoYes[NoYes["Yes"] = 1] = "Yes";
- })(NoYes || (NoYes = {}));
字符串枚舉編譯結(jié)果
- "use strict";
- var NoYes;
- (function (NoYes) {
- NoYes["No"] = "No";
- NoYes["Yes"] = "Yes";
- })(NoYes || (NoYes = {}));
通過(guò)觀察以上結(jié)果,我們知道數(shù)值枚舉除了支持 從成員名稱到成員值 的普通映射之外,它還支持 從成員值到成員名稱 的反向映射。另外,對(duì)于純字符串枚舉,我們不能省略任何初始化程序。而數(shù)字枚舉如果沒有顯式設(shè)置值時(shí),則會(huì)使用默認(rèn)值進(jìn)行初始化。
8.4 為數(shù)字枚舉分配越界值
講到數(shù)字枚舉,這里我們?cè)賮?lái)看個(gè)問(wèn)題:
- const enum Fonum {
- a = 1,
- b = 2
- }
- let value: Fonum = 12; // Ok
相信很多讀者看到 let value: Fonum = 12; 這一行,TS 編譯器并未提示任何錯(cuò)誤會(huì)感到驚訝。很明顯數(shù)字 12 并不是 Fonum 枚舉的成員。 為什么會(huì)這樣呢?我們來(lái)看一下 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.
該行為是由按位運(yùn)算引起的。有時(shí) SomeFlag.Foo | SomeFlag.Bar 用于生成另一個(gè) SomeFlag。相反,你最終得到的是數(shù)字,并且你不想強(qiáng)制回退到 SomeFlag。
了解完上述內(nèi)容,我們?cè)賮?lái)看一下 let value: Fonum = 12; 這個(gè)語(yǔ)句,該語(yǔ)句 TS 編譯器不會(huì)報(bào)錯(cuò),是因?yàn)閿?shù)字 12 是可以通過(guò) Fonum 已有的枚舉成員計(jì)算而得。
- let value: Fonum =
- Fonum.a << Fonum.b << Fonum.a | Fonum.a << Fonum.b; // 12
九、使用 # 定義的私有字段與 private 修飾符定義字段有什么區(qū)別
在 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.
與常規(guī)屬性(甚至使用 private 修飾符聲明的屬性)不同,私有字段要牢記以下規(guī)則:
- 私有字段以 # 字符開頭,有時(shí)我們稱之為私有名稱;
- 每個(gè)私有字段名稱都唯一地限定于其包含的類;
- 不能在私有字段上使用 TypeScript 可訪問(wèn)性修飾符(如 public 或 private);
- 私有字段不能在包含的類之外訪問(wèn),甚至不能被檢測(cè)到。
說(shuō)到這里使用 # 定義的私有字段與 private 修飾符定義字段有什么區(qū)別呢?現(xiàn)在我們先來(lái)看一個(gè) private 的示例:
- class Person {
- constructor(private name: string){}
- }
- let person = new Person("Semlinker");
- console.log(person.name);
在上面代碼中,我們創(chuàng)建了一個(gè) Person 類,該類中使用 private 修飾符定義了一個(gè)私有屬性 name,接著使用該類創(chuàng)建一個(gè) person 對(duì)象,然后通過(guò) person.name 來(lái)訪問(wèn) person 對(duì)象的私有屬性,這時(shí) TypeScript 編譯器會(huì)提示以下異常:
- Property 'name' is private and only accessible within class 'Person'.(2341)
那如何解決這個(gè)異常呢?當(dāng)然你可以使用類型斷言把 person 轉(zhuǎn)為 any 類型:
- console.log((person as any).name);
通過(guò)這種方式雖然解決了 TypeScript 編譯器的異常提示,但是在運(yùn)行時(shí)我們還是可以訪問(wèn)到 Person 類內(nèi)部的私有屬性,為什么會(huì)這樣呢?我們來(lái)看一下編譯生成的 ES5 代碼,也許你就知道答案了:
- var Person = /** @class */ (function () {
- function Person(name) {
- this.name = name;
- }
- return Person;
- }());
- var person = new Person("Semlinker");
- console.log(person.name);
這時(shí)相信有些小伙伴會(huì)好奇,在 TypeScript 3.8 以上版本通過(guò) # 號(hào)定義的私有字段編譯后會(huì)生成什么代碼:
- class Person {
- #name: string;
- constructor(name: string) {
- this.#name = name;
- }
- greet() {
- console.log(`Hello, my name is ${this.#name}!`);
- }
- }
以上代碼目標(biāo)設(shè)置為 ES2015,會(huì)編譯生成以下代碼:
- "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();
通過(guò)觀察上述代碼,使用 # 號(hào)定義的 ECMAScript 私有字段,會(huì)通過(guò) WeakMap 對(duì)象來(lái)存儲(chǔ),同時(shí)編譯器會(huì)生成 __classPrivateFieldSet 和 __classPrivateFieldGet這兩個(gè)方法用于設(shè)置值和獲取值。
以上提到的這些問(wèn)題,相信一些小伙伴們?cè)趯W(xué)習(xí) TS 過(guò)程中也遇到了。如果有表述不清楚的地方,歡迎你們給我留言或直接與我交流。之后,阿寶哥還會(huì)繼續(xù)補(bǔ)充和完善這一方面的內(nèi)容,感興趣的小伙伴可以一起參與喲。
十、參考資源
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
網(wǎng)頁(yè)名稱:細(xì)數(shù)這些年被困擾過(guò)的TS問(wèn)題
本文地址:http://m.fisionsoft.com.cn/article/cdocjeg.html


咨詢
建站咨詢
