关于前端:细数-TS-中那些奇怪的符号

8次阅读

共计 18061 个字符,预计需要花费 46 分钟才能阅读完成。

TypeScript 是一种由微软开发的自在和开源的编程语言。它是 JavaScript 的一个超集,而且实质上向这个语言增加了可选的动态类型和基于类的面向对象编程。

本文阿宝哥将分享这些年在学习 TypeScript 过程中,遇到的 10 大“奇怪”的符号。其中有一些符号,阿宝哥第一次见的时候也感觉“一脸懵逼”,心愿本文对学习 TypeScript 的小伙伴能有一些帮忙。

好的,上面咱们来开始介绍第一个符号 —— ! 非空断言操作符

一、! 非空断言操作符

在上下文中当类型查看器无奈判定类型时,一个新的后缀表达式操作符 ! 能够用于断言操作对象是非 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)。有了可选链后,咱们编写代码时如果遇到 nullundefined 就能够立刻进行某些表达式的运行。可选链的外围是新的 ?. 运算符,它反对以下语法:

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 是否为 nullundefined,如果是的话就立刻返回 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 等。而 ?. 只会验证对象是否为 nullundefined,对于 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 的值是否为 nullundefined,从而保障了咱们代码的健壮性。

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 短路

当空值合并运算符的左表达式不为 nullundefined 时,不会对右表达式进行求值。

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 接口,它蕴含了两个必填的属性 nameage。在初始化 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<T>

在理论我的项目开发过程中,为了进步代码复用率,咱们能够利用 TypeScript 内置的工具类型 Partial<T> 来疾速把某个接口类型中定义的属性变成可选的:

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

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

是不是感觉 Partial<T> 很不便,上面让咱们来看一下它是如何实现的:

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

既然能够疾速地把某个接口中定义的属性全副申明为可选,那能不能把所有的可选的属性变成必选的呢?答案是能够的,针对这个需要,咱们能够应用 Required<T> 工具类型,具体的应用形式如下:

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

type PullDownRefreshOptions = Partial<PullDownRefreshConfig>

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

同样,咱们来看一下 Required<T> 工具类型是如何实现的:

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

原来在 Required<T> 工具类型外部,通过 -? 移除了可选属性中的 ?,使得属性从可选变为必选的。

五、& 运算符

在 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 的类型是不是能够是 stringnumber 类型呢?比方上面的例子:

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)示意取值能够为多种类型中的一种,联结类型应用 | 分隔每个类型。联结类型通常与 nullundefined 一起应用:

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

以上示例中 name 的类型是 string | undefined 意味着能够将 stringundefined 的值传递给 sayHello 函数。

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

此外,对于联结类型来说,你可能会遇到以下的用法:

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

示例中的 12'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

八、<Type> 语法

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 泛型的读者来说,首次看到 <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,而不须要开发人员显式指定它们。

九、@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 这两个办法用于设置值和获取值。

十一、参考资源

  • ES proposal: numeric separators
  • typescriptlang.org

十二、举荐浏览

  • 了不起的 TypeScript 入门教程
  • 一文读懂 TypeScript 泛型及利用
  • 你不晓得的 WebSocket
  • 你不晓得的 Blob
  • 你不晓得的 WeakMap
正文完
 0