記錄--盤點 TypeScript 那些奇怪的符號

来源:https://www.cnblogs.com/smileZAZ/archive/2023/07/17/17560907.html
-Advertisement-
Play Games

這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 TypeScript是一種由微軟開發的自由和開源的編程語言。它是JavaScript的一個超集,而且本質上向這個語言添加了可選的靜態類型和基於類的面向對象編程。 一、! 非空斷言操作符 在上下文中當類型檢查器無法斷定類型時,一個新的尾碼表 ...


這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助

TypeScript是一種由微軟開發的自由和開源的編程語言。它是JavaScript的一個超集,而且本質上向這個語言添加了可選的靜態類型和基於類的面向對象編程。

一、! 非空斷言操作符

在上下文中當類型檢查器無法斷定類型時,一個新的尾碼表達式操作符 ! 可以用於斷言操作對象是非 null 和非 undefined 類型。具體而言,x! 將從 x 值域中排除 null 和 undefined 。

那麼非空斷言操作符到底有什麼用呢?下麵我們先來看一下非空斷言操作符的一些使用場景。

1.1 忽略 undefined 和 null 類型

function myFunc(maybeString: string | undefined | null) {
  // Type 'string | null | undefined' is not assignable to type 'string'.
  // Type 'undefined' is not assignable to type 'string'. 
  const onlyString: string = maybeString; // Error
  const ignoreUndefinedAndNull: string = maybeString!; // Ok
}

1.2 調用函數時忽略 undefined 類型

type NumGenerator = () => number;

function myFunc(numGenerator: NumGenerator | undefined) {
  // Object is possibly 'undefined'.(2532)
  // Cannot invoke an object which is possibly 'undefined'.(2722)
  const num1 = numGenerator(); // Error
  const num2 = numGenerator!(); //OK
}

因為 ! 非空斷言操作符會從編譯生成的 JavaScript 代碼中移除,所以在實際使用的過程中,要特別註意。比如下麵這個例子:

const a: number | undefined = undefined;
const b: number = a!;
console.log(b); 

以上 TS 代碼會編譯生成以下 ES5 代碼:

"use strict";
const a = undefined;
const b = a;
console.log(b);

雖然在 TS 代碼中,我們使用了非空斷言,使得 const b: number = a!; 語句可以通過 TypeScript 類型檢查器的檢查。但在生成的 ES5 代碼中,! 非空斷言操作符被移除了,所以在瀏覽器中執行以上代碼,在控制台會輸出 undefined。

二、?. 運算符

TypeScript 3.7 實現了呼聲最高的 ECMAScript 功能之一:可選鏈(Optional Chaining)。有了可選鏈後,我們編寫代碼時如果遇到 null 或 undefined 就可以立即停止某些表達式的運行。可選鏈的核心是新的 ?. 運算符,它支持以下語法:

obj?.prop
obj?.[expr]
arr?.[index]
func?.(args)
這裡我們來舉一個可選的屬性訪問的例子:
const val = a?.b;

為了更好的理解可選鏈,我們來看一下該 const val = a?.b 語句編譯生成的 ES5 代碼:

var val = a === null || a === void 0 ? void 0 : a.b;

上述的代碼會自動檢查對象 a 是否為 null 或 undefined,如果是的話就立即返回 undefined,這樣就可以立即停止某些表達式的運行。你可能已經想到可以使用 ?. 來替代很多使用 && 執行空檢查的代碼:

if(a && a.b) { } 

if(a?.b){ }
/**
* if(a?.b){ } 編譯後的ES5代碼
* 
* if(
*  a === null || a === void 0 
*  ? void 0 : a.b) {
* }
*/

但需要註意的是,?. 與 && 運算符行為略有不同,&& 專門用於檢測 falsy 值,比如空字元串、0、NaN、null 和 false 等。而 ?. 只會驗證對象是否為 null 或 undefined,對於 0 或空字元串來說,並不會出現 “短路”。

2.1 可選元素訪問

可選鏈除了支持可選屬性的訪問之外,它還支持可選元素的訪問,它的行為類似於可選屬性的訪問,只是可選元素的訪問允許我們訪問非標識符的屬性,比如任意字元串、數字索引和 Symbol:

function tryGetArrayElement<T>(arr?: T[], index: number = 0) {
  return arr?.[index];
}

以上代碼經過編譯後會生成以下 ES5 代碼:

"use strict";
function tryGetArrayElement(arr, index) {
    if (index === void 0) { index = 0; }
    return arr === null || arr === void 0 ? void 0 : arr[index];
}

通過觀察生成的 ES5 代碼,很明顯在 tryGetArrayElement 方法中會自動檢測輸入參數 arr 的值是否為 null 或 undefined,從而保證了我們代碼的健壯性。

2.2 可選鏈與函數調用

當嘗試調用一個可能不存在的方法時也可以使用可選鏈。在實際開發過程中,這是很有用的。系統中某個方法不可用,有可能是由於版本不一致或者用戶設備相容性問題導致的。函數調用時如果被調用的方法不存在,使用可選鏈可以使表達式自動返回 undefined 而不是拋出一個異常。

可選調用使用起來也很簡單,比如:

let result = obj.customMethod?.();

該 TypeScript 代碼編譯生成的 ES5 代碼如下:

var result = (_a = obj.customMethod) === null
  || _a === void 0 ? void 0 : _a.call(obj);

另外在使用可選調用的時候,我們要註意以下兩個註意事項:

  • 如果存在一個屬性名且該屬性名對應的值不是函數類型,使用 ?. 仍然會產生一個 TypeError 異常。
  • 可選鏈的運算行為被局限在屬性的訪問、調用以及元素的訪問 —— 它不會沿伸到後續的表達式中,也就是說可選調用不會阻止 a?.b / someMethod() 表達式中的除法運算或 someMethod 的方法調用。

三、?? 空值合併運算符

在 TypeScript 3.7 版本中除了引入了前面介紹的可選鏈 ?. 之外,也引入了一個新的邏輯運算符 —— 空值合併運算符 ??。當左側操作數為 null 或 undefined 時,其返回右側的操作數,否則返回左側的操作數。

與邏輯或 || 運算符不同,邏輯或會在左操作數為 falsy 值時返回右側操作數。也就是說,如果你使用 || 來為某些變數設置預設的值時,你可能會遇到意料之外的行為。比如為 falsy 值(''、NaN 或 0)時。

這裡來看一個具體的例子:

const foo = null ?? 'default string';
console.log(foo); // 輸出:"default string"

const baz = 0 ?? 42;
console.log(baz); // 輸出:0

以上 TS 代碼經過編譯後,會生成以下 ES5 代碼:

"use strict";
var _a, _b;
var foo = (_a = null) !== null && _a !== void 0 ? _a : 'default string';
console.log(foo); // 輸出:"default string"

var baz = (_b = 0) !== null && _b !== void 0 ? _b : 42;
console.log(baz); // 輸出:0

通過觀察以上代碼,我們更加直觀的瞭解到,空值合併運算符是如何解決前面 || 運算符存在的潛在問題。下麵我們來介紹空值合併運算符的特性和使用時的一些註意事項。

3.1 短路

當空值合併運算符的左表達式不為 null 或 undefined 時,不會對右表達式進行求值。

function A() { console.log('A was called'); return undefined;}
function B() { console.log('B was called'); return false;}
function C() { console.log('C was called'); return "foo";}

console.log(A() ?? C());
console.log(B() ?? C());

// 上述代碼運行後,控制台會輸出以下結果:
A was called 
C was called 
foo 
B was called 
false 

3.2 不能與 && 或 || 操作符共用

若空值合併運算符 ?? 直接與 AND(&&)和 OR(||)操作符組合使用 ?? 是不行的。這種情況下會拋出 SyntaxError。

// '||' and '??' operations cannot be mixed without parentheses.(5076)
null || undefined ?? "foo"; // raises a SyntaxError

// '&&' and '??' operations cannot be mixed without parentheses.(5076)
true && undefined ?? "foo"; // raises a SyntaxError

// 但當使用括弧來顯式表明優先順序時是可行的,比如:
(null || undefined ) ?? "foo"; // 返回 "foo"

3.3 與可選鏈操作符 ?. 的關係

空值合併運算符針對 undefined 與 null 這兩個值,可選鏈式操作符 ?. 也是如此。可選鏈式操作符,對於訪問屬性可能為 undefined 與 null 的對象時非常有用。

interface Customer {
  name: string;
  city?: string;
}

let customer: Customer = {
  name: "Semlinker"
};

let customerCity = customer?.city ?? "Unknown city";
console.log(customerCity); // 輸出:Unknown city

前面我們已經介紹了空值合併運算符的應用場景和使用時的一些註意事項,該運算符不僅可以在 TypeScript 3.7 以上版本中使用。當然你也可以在 JavaScript 的環境中使用它,但你需要藉助 Babel,在 Babel 7.8.0 版本也開始支持空值合併運算符。

四、?: 可選屬性

在面向對象語言中,介面是一個很重要的概念,它是對行為的抽象,而具體如何行動需要由類去實現。 TypeScript 中的介面是一個非常靈活的概念,除了可用於對類的一部分行為進行抽象以外,也常用於對「對象的形狀(Shape)」進行描述。

在 TypeScript 中使用 interface 關鍵字就可以聲明一個介面:

interface Person {
  name: string;
  age: number;
}

let semlinker: Person = {
  name: "semlinker",
  age: 33,
};

在以上代碼中,我們聲明瞭 Person 介面,它包含了兩個必填的屬性 name 和 age。在初始化 Person 類型變數時,如果缺少某個屬性,TypeScript 編譯器就會提示相應的錯誤信息,比如:

// Property 'age' is missing in type '{ name: string; }' but required in type 'Person'.(2741)
let lolo: Person  = { // Error
  name: "lolo"  
}

為瞭解決上述的問題,我們可以把某個屬性聲明為可選的:

interface Person {
  name: string;
  age?: number;
}

let lolo: Person  = {
  name: "lolo"  
}

4.1 工具類型

4.1.1 Partial

在實際項目開發過程中,為了提高代碼復用率,我們可以利用 TypeScript 內置的工具類型 Partial 來快速把某個介面類型中定義的屬性變成可選的:

interface PullDownRefreshConfig {
  threshold: number;
  stop: number;
}

/**
 * type PullDownRefreshOptions = {
 *   threshold?: number | undefined;
 *   stop?: number | undefined;
 * }
 */ 
type PullDownRefreshOptions = Partial<PullDownRefreshConfig>

是不是覺得 Partial 很方便,下麵讓我們來看一下它是如何實現的:

/**
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P];
};

4.1.2 Required

既然可以快速地把某個介面中定義的屬性全部聲明為可選,那能不能把所有的可選的屬性變成必選的呢?答案是可以的,針對這個需求,我們可以使用 Required 工具類型,具體的使用方式如下:

interface PullDownRefreshConfig {
  threshold: number;
  stop: number;
}

type PullDownRefreshOptions = Partial<PullDownRefreshConfig>

/**
 * type PullDownRefresh = {
 *   threshold: number;
 *   stop: number;
 * }
 */
type PullDownRefresh = Required<Partial<PullDownRefreshConfig>>

同樣,我們來看一下 Required 工具類型是如何實現的:

/**
 * Make all properties in T required
 */
type Required<T> = {
  [P in keyof T]-?: T[P];
};

原來在 Required 工具類型內部,通過 -? 移除了可選屬性中的 ?,使得屬性從可選變為必選的。

五、& 運算符

在 TypeScript 中交叉類型是將多個類型合併為一個類型。通過 & 運算符可以將現有的多種類型疊加到一起成為一種類型,它包含了所需的所有類型的特性。

type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };

let point: Point = {
  x: 1,
  y: 1
}

在上面代碼中我們先定義了 PartialPointX 類型,接著使用 & 運算符創建一個新的 Point 類型,表示一個含有 x 和 y 坐標的點,然後定義了一個 Point 類型的變數並初始化

5.1 同名基礎類型屬性的合併

那麼現在問題來了,假設在合併多個類型的過程中,剛好出現某些類型存在相同的成員,但對應的類型又不一致,比如:

interface X {
  c: string;
  d: string;
}

interface Y {
  c: number;
  e: string
}

type XY = X & Y;
type YX = Y & X;

let p: XY;
let q: YX;
在上面的代碼中,介面 X 和介面 Y 都含有一個相同的成員 c,但它們的類型不一致。對於這種情況,此時 XY 類型或 YX 類型中成員 c 的類型是不是可以是 string 或 number 類型呢?比如下麵的例子:
p = { c: 6, d: "d", e: "e" }; 

q = { c: "c", d: "d", e: "e" }; 

為什麼介面 X 和介面 Y 混入後,成員 c 的類型會變成 never 呢?這是因為混入後成員 c 的類型為 string & number,即成員 c 的類型既可以是 string 類型又可以是 number 類型。很明顯這種類型是不存在的,所以混入後成員 c 的類型為 never。

5.2 同名非基礎類型屬性的合併

在上面示例中,剛好介面 X 和介面 Y 中內部成員 c 的類型都是基本數據類型,那麼如果是非基本數據類型的話,又會是什麼情形。我們來看個具體的例子:

interface D { d: boolean; }
interface E { e: string; }
interface F { f: number; }

interface A { x: D; }
interface B { x: E; }
interface C { x: F; }

type ABC = A & B & C;

let abc: ABC = {
  x: {
    d: true,
    e: 'semlinker',
    f: 666
  }
};

console.log('abc:', abc);
以上代碼成功運行後,控制台會輸出以下結果:

由上圖可知,在混入多個類型時,若存在相同的成員,且成員類型為非基本數據類型,那麼是可以成功合併。

六、| 分隔符

在 TypeScript 中聯合類型(Union Types)表示取值可以為多種類型中的一種,聯合類型使用 | 分隔每個類型。聯合類型通常與 null 或 undefined 一起使用:

const sayHello = (name: string | undefined) => { /* ... */ };

以上示例中 name 的類型是 string | undefined 意味著可以將 string 或 undefined 的值傳遞給 sayHello 函數。

sayHello("semlinker");
sayHello(undefined);

此外,對於聯合類型來說,你可能會遇到以下的用法:

let num: 1 | 2 = 1;
type EventNames = 'click' | 'scroll' | 'mousemove';

示例中的 1、2 或 'click' 被稱為字面量類型,用來約束取值只能是某幾個值中的一個。

6.1 類型保護

當使用聯合類型時,我們必須儘量把當前值的類型收窄為當前值的實際類型,而類型保護就是實現類型收窄的一種手段。

類型保護是可執行運行時檢查的一種表達式,用於確保該類型在一定的範圍內。換句話說,類型保護可以保證一個字元串是一個字元串,儘管它的值也可以是一個數字。類型保護與特性檢測並不是完全不同,其主要思想是嘗試檢測屬性、方法或原型,以確定如何處理值

目前主要有四種的方式來實現類型保護:

6.1.1 in 關鍵字

interface Admin {
  name: string;
  privileges: string[];
}

interface Employee {
  name: string;
  startDate: Date;
}

type UnknownEmployee = Employee | Admin;

function printEmployeeInformation(emp: UnknownEmployee) {
  console.log("Name: " + emp.name);
  if ("privileges" in emp) {
    console.log("Privileges: " + emp.privileges);
  }
  if ("startDate" in emp) {
    console.log("Start Date: " + emp.startDate);
  }
}

6.1.2 typeof 關鍵字

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
      return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
      return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}

typeof 類型保護只支持兩種形式:typeof v === "typename" 和 typeof v !== typename,"typename" 必須是 "number", "string", "boolean" 或 "symbol"。 但是 TypeScript 並不會阻止你與其它字元串比較,語言不會把那些表達式識別為類型保護。

6.1.3 instanceof 關鍵字

interface Padder {
  getPaddingString(): string;
}

class SpaceRepeatingPadder implements Padder {
  constructor(private numSpaces: number) {}
  getPaddingString() {
    return Array(this.numSpaces + 1).join(" ");
  }
}

class StringPadder implements Padder {
  constructor(private value: string) {}
  getPaddingString() {
    return this.value;
  }
}

let padder: Padder = new SpaceRepeatingPadder(6);

if (padder instanceof SpaceRepeatingPadder) {
  // padder的類型收窄為 'SpaceRepeatingPadder'
}

6.1.4 自定義類型保護的類型謂詞(type predicate)

function isNumber(x: any): x is number {
  return typeof x === "number";
}

function isString(x: any): x is string {
  return typeof x === "string";
}

七、_ 數字分隔符

TypeScript 2.7 帶來了對數字分隔符的支持,正如數值分隔符 ECMAScript 提案中所概述的那樣。對於一個數字字面量,你現在可以通過把一個下劃線作為它們之間的分隔符來分組數字:

const inhabitantsOfMunich = 1_464_301;
const distanceEarthSunInKm = 149_600_000;
const fileSystemPermission = 0b111_111_000;
const bytes = 0b1111_10101011_11110000_00001101;
 分隔符不會改變數值字面量的值,但邏輯分組使人們更容易一眼就能讀懂數字。以上 TS 代碼經過編譯後,會生成以下 ES5 代碼:
"use strict";
var inhabitantsOfMunich = 1464301;
var distanceEarthSunInKm = 149600000;
var fileSystemPermission = 504;
var bytes = 262926349;

7.1 使用限制

雖然數字分隔符看起來很簡單,但在使用時還是有一些限制。比如你只能在兩個數字之間添加 _ 分隔符。以下的使用方式是非法的:

// Numeric separators are not allowed here.(6188)
3_.141592 // Error
3._141592 // Error

// Numeric separators are not allowed here.(6188)
1_e10 // Error
1e_10 // Error

// Cannot find name '_126301'.(2304)
_126301  // Error
// Numeric separators are not allowed here.(6188)
126301_ // Error

// Cannot find name 'b111111000'.(2304)
// An identifier or keyword cannot immediately follow a numeric literal.(1351)
0_b111111000 // Error

// Numeric separators are not allowed here.(6188)
0b_111111000 // Error

// 當然你也不能連續使用多個 _ 分隔符,比如:
// Multiple consecutive numeric separators are not permitted.(6189)
123__456 // Error

7.2 解析分隔符

此外,需要註意的是以下用於解析數字的函數是不支持分隔符:

  • Number()
  • parseInt()
  • parseFloat()

這裡我們來看一下實際的例子:

Number('123_456')
NaN
parseInt('123_456')
123
parseFloat('123_456')
123

很明顯對於以上的結果不是我們所期望的,所以在處理分隔符時要特別註意。當然要解決上述問題,也很簡單隻需要非數字的字元刪掉即可。這裡我們來定義一個 removeNonDigits 的函數:

const RE_NON_DIGIT = /[^0-9]/gu;

function removeNonDigits(str) {
  str = str.replace(RE_NON_DIGIT, '');
  return Number(str);
}

該函數通過調用字元串的 replace 方法來移除非數字的字元,具體的使用方式如下:

removeNonDigits('123_456')
123456
removeNonDigits('149,600,000')
149600000
removeNonDigits('1,407,836')
1407836

八、 語法

8.1 TypeScript 斷言

有時候你會遇到這樣的情況,你會比 TypeScript 更瞭解某個值的詳細信息。通常這會發生在你清楚地知道一個實體具有比它現有類型更確切的類型。

通過類型斷言這種方式可以告訴編譯器,“相信我,我知道自己在乾什麼”。類型斷言好比其他語言里的類型轉換,但是不進行特殊的數據檢查和解構。它沒有運行時的影響,只是在編譯階段起作用。

類型斷言有兩種形式:

8.1.1 “尖括弧” 語法

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

8.1.2 as 語法

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

8.2 TypeScript 泛型

對於剛接觸 TypeScript 泛型的讀者來說,首次看到 語法會感到陌生。其實它沒有什麼特別,就像傳遞參數一樣,我們傳遞了我們想要用於特定函數調用的類型。

 

參考上面的圖片,當我們調用 identity(1) ,Number 類型就像參數 1 一樣,它將在出現 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,而不需要開發人員顯式指定它們。

九、@XXX 裝飾器

9.1 裝飾器語法

對於一些剛接觸 TypeScript 的小伙伴來說,在第一次看到 @Plugin({...}) 這種語法可能會覺得很驚訝。其實這是裝飾器的語法,裝飾器的本質是一個函數,通過裝飾器我們可以方便地定義與對象相關的元數據。

@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 {}

在以上代碼中,我們通過裝飾器來保存 ionic-native 插件的相關元信息,而 @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 類作為參數調用各自的類型裝飾器,從而擴展對應的功能。

9.2 裝飾器的分類

在 TypeScript 中裝飾器分為類裝飾器、屬性裝飾器、方法裝飾器和參數裝飾器四大類

9.2.1 類裝飾器

類裝飾器聲明:

declare type ClassDecorator = <TFunction extends Function>(
  target: TFunction
) => TFunction | void;

類裝飾器顧名思義,就是用來裝飾類的。它接收一個參數:

  • target: TFunction - 被裝飾的類

看完第一眼後,是不是感覺都不好了。沒事,我們馬上來個例子:

function Greeter(target: Function): void {
  target.prototype.greet = function (): void {
    console.log("Hello Semlinker!");
  };
}

@Greeter
class Greeting {
  constructor() {
    // 內部實現
  }
}

let myGreeting = new Greeting();
myGreeting.greet(); // console output: 'Hello Semlinker!';

上面的例子中,我們定義了 Greeter 類裝飾器,同時我們使用了 @Greeter 語法糖,來使用裝飾器。

友情提示:讀者可以直接複製上面的代碼,在 TypeScript Playground 中運行查看結果。

9.2.2 屬性裝飾器

屬性裝飾器聲明:

declare type PropertyDecorator = (target:Object, propertyKey: string | symbol ) => void;

屬性裝飾器顧名思義,用來裝飾類的屬性。它接收兩個參數:

  • target: Object - 被裝飾的類
  • propertyKey: string | symbol - 被裝飾類的屬性名

趁熱打鐵,馬上來個例子熱熱身:

function logProperty(target: any, key: string) {
  delete target[key];

  const backingField = "_" + key;

  Object.defineProperty(target, backingField, {
    writable: true,
    enumerable: true,
    configurable: true
  });

  // property getter
  const getter = function (this: any) {
    const currVal = this[backingField];
    console.log(`Get: ${key} => ${currVal}`);
    return currVal;
  };

  // property setter
  const setter = function (this: any, newVal: any) {
    console.log(`Set: ${key} => ${newVal}`);
    this[backingField] = newVal;
  };

  // Create new property with getter and setter
  Object.defineProperty(target, key, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
}

class Person { 
  @logProperty
  public name: string;

  constructor(name : string) { 
    this.name = name;
  }
}

const p1 = new Person("semlinker");
p1.name = "kakuqo";

以上代碼我們定義了一個 logProperty 函數,來跟蹤用戶對屬性的操作,當代碼成功運行後,在控制台會輸出以下結果:

Set: name => semlinker
Set: name => kakuqo

9.2.3 方法裝飾器

方法裝飾器聲明:

declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol,          
  descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;

方法裝飾器顧名思義,用來裝飾類的方法。它接收三個參數:

  • target: Object - 被裝飾的類
  • propertyKey: string | symbol - 方法名
  • descriptor: TypePropertyDescript - 屬性描述符

廢話不多說,直接上例子:

function LogOutput(tarage: Function, key: string, descriptor: any) {
  let originalMethod = descriptor.value;
  let newMethod = function(...args: any[]): any {
    let result: any = originalMethod.apply(this, args);
    if(!this.loggedOutput) {
      this.loggedOutput = new Array<any>();
    }
    this.loggedOutput.push({
      method: key,
      parameters: args,
      output: result,
      timestamp: new Date()
    });
    return result;
  };
  descriptor.value = newMethod;
}

class Calculator {
  @LogOutput
  double (num: number): number {
    return num * 2;
  }
}

let calc = new Calculator();
calc.double(11);
// console ouput: [{method: "double", output: 22, ...}]
console.log(calc.loggedOutput); 

9.2.4 參數裝飾器

參數裝飾器聲明:

declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, 
  parameterIndex: number ) => void

參數裝飾器顧名思義,是用來裝飾函數參數,它接收三個參數:

  • target: Object - 被裝飾的類
  • propertyKey: string | symbol - 方法名
  • parameterIndex: number - 方法中參數的索引值
function Log(target: Function, key: string, parameterIndex: number) {
  let functionLogged = key || target.prototype.constructor.name;
  console.log(`The parameter in position ${parameterIndex} at ${functionLogged} has
    been decorated`);
}

class Greeter {
  greeting: string;
  constructor(@Log phrase: string) {
    this.greeting = phrase; 
  }
}

// console output: The parameter in position 0 
// at Greeter has been decorated

十、#XXX 私有欄位

在 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);
  • 私有欄位不能在包含的類之外訪問,甚至不能被檢測到。

10.1 私有欄位與 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 這兩個方法用於設置值和獲取值。

本文轉載於:

https://juejin.cn/post/7255495666666029117

如果對您有所幫助,歡迎您點個關註,我會定時更新技術文檔,大家一起討論學習,一起進步。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # shell腳本-Nginx訪問日誌分析 ## 1.原理 > 可以通過/usr/local/nginx/logs/access.log 文件-查看nginx的日誌 > > ``` > [root@localhost scripts]# tail -f /usr/local/nginx/logs/a ...
  • ## 跟運維學 Linux - 01 ### 運維的誕生 運維工程師有很多叫法:系統運維、Linux 工程師、系統管理員... `網管`可以說是運維工程師最早的雛形。在個人電腦未普及時,大家去網吧玩游戲。 玩家:“網關,我的電腦上不了網了” 網管負責維修電腦、維修系統、維護網路設備... ### 互 ...
  • ![](https://img2023.cnblogs.com/blog/3076680/202307/3076680-20230717125930891-211733509.png) # 1. 計算同一組或分區的行之間的差 ## 1.1. 最終結果集 ### 1.1.1. sql ```sql D ...
  • 一、DQL介紹 DQL全稱:Data Query Language(數據查詢語言),用來查詢資料庫中表的記錄。 關鍵字:select 二、DQL語法 select 欄位列表 from 表名列表 where 條件列表 group by 分組欄位列表 having 分組後條件列表 order by 排序 ...
  • Oracle資料庫 在Oracle資料庫中,''(空字元串)與null是什麼關係呢? ''(空字元串)是否會等同(或者說等價於)於null值呢?''跟' '(長度為零的空字元串或包含一個或多個空格的空字元串)是否又等價?下麵我們測試一下 SQL> create table test(id numbe ...
  • 魔笛活動平臺要記錄每個活動的用戶行為數據,幫助客服、運營、產品、研發等快速處理客訴、解決線上問題併進行相關數據分析和報警。可以預見到需要存儲和分析海量數據,預估至少幾十億甚至上百億的數據量,所以需要選擇一款能存儲海量數據的資料庫。由於是通過接收MQ存儲或者API方式存儲,所以對實時寫入性能也有一定要... ...
  • - 1. GIPKs特性簡介 - 2. GIPKs特性的作用 - 3. 玩轉GIPKs > GIPKs解決了歷史難題 ## 1. GIPKs特性簡介 從MySQL 8.0.30開始,新引入一個叫做GPIKs的特性,其全稱是 **Generated Invisible Primary Keys**,簡 ...
  • 近日,工業和信息化部發佈了2023年第2批侵害用戶權益行為的App(SDK)名單,55款App因涉及強制、頻繁、過度索取許可權等問題而被通報。這一舉措進一步凸顯了合規對於APP發展的重要性。 根據工業和信息化部的通報,被通報的這些App存在著一些消費者難以察覺的問題,如頻繁自啟動和關聯啟動、超範圍索權 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...