TS学习笔记六:高级类型/Symbols

  本节介绍ts中的高级类型及Symbols相关内容,高级类型包括交叉类型、联合类型、类型保护、类型别名等内容,Symbols是ECMAScript 2015后的原生类型,像其他的基础类型number和string一样,通过Symbol构造函数创建,Symbol是不可改变且唯一的。

  1. 讲解视频

    TS学习笔记九:高级类型

  2. B站视频

    TS学习笔记九:高级类型

  3. 西瓜视频
    https://www.ixigua.com/7322652210817729062
    TS学习笔记六:高级类型/Symbols_第1张图片

一、高级类型

  ts中除了基础类型、类类型、枚举、泛型等外还有一些特有的高级类型,包括交叉类型、联合类型等。

1.交叉类型

  交叉类型是将多个类型进行联合,联合后新成一个新的类型,新的类型包含被联合的多个类型的所有特性,如:A&B&C同时具有A和B和C的特性,并且都能兼容。
  在mixins中交叉类型比较常见,mixins在后续章节中会介绍,示例如下:

function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U>{};
    for (let id in first) {
        (<any>result)[id] = (<any>first)[id];
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            (<any>result)[id] = (<any>second)[id];
        }
    }
    return result;
}

class A{
    constructor(public name: string) { }
}
interface B{
    log(): void;
}
class C implements B{
    log() {
        // ...
    }
}
var jim = extend(new A("Jim"), new C());
var n = jim.name;
jim.log();

  上述示例中对A和C进行了mixins处理,即将A类型中的所有成员及C类型中的所有成员进行合并,合并到新的成员中,最后的结果变量jim包含了A和B及C三个类型的所有成员,合并是通过对类原型进行复制操作来实现,最终的结果是传入的T和U的联合类型。

2.联合类型

  联合类型是标识几种类型之一,使用|分隔每个类型,如:number|string|boolean表示既可以是number,也可以是string,也可是boolean。如果一个值是联合类型,只能访问联合类型的所有类型里的共有成员,如下:

interface A{
	a();
	b();
}
interface B{
	b();
	c();
}
function C():A|B{}
let c = C();
c.b()//可以调用
c.a()//报错,因为a不是共同具有的成员。

  也可以使用any传递参数,any传递时可以传递任何类型的参数,但是如果传入的参数将没有限制,在函数处理时就有可能出现问题。所以最好使用联合类型,使用了联合类型之后,将限定参数的类型范围,只能是指定的几种类中的一种,这样参数的类型就是可预测的,而不像any一样参数的类型不可预测,会导致后续代码处理时出现不确定性。

3.类型保护和区分类型

  使用联合类型时,由于类型有可能是联合类型的任何一种,当想明确的知道具体的类型的到底是那种的时候,需要进行特出判断或者类型断言,如下:

interface A{
	a();
	b();
}
interface B{
	b();
	c();
}
function C():A|B{}
let c = C();
if(c.a){
	c.a();
}else if(c.c){
	c.c();
}

  上述实例js中没有问题,但是ts中会报错,因为c的类型不确定,即使使用了if判断,类型也不确定,此时可以使用类型断言进行类型判断:

interface A{
	a();
	b();
}
interface B{
	b();
	c();
}
function C():A|B{}
let c = C();
if((<A>c).a){
	(<A>c).a();
}else if((<B>c).c){
	(<B>c).c();
}

  使用了类型断言之后,就确定了变量c的具体类型,ts中将不会报错。

  • 自定义类型保护
      上述类型保护的时候,若代码运行需要多次声明类型断言,ts中的类型保护机制可以只检查一次类型后,就能在后面的每个分支中都能确定具体的类型了。
      类型保护就是一些表达式,会在运行时检查以确保某个作用域里的类型,定时方式是定义一个函数,返回一个类型断言,示例如下:
function isA(pet: A| B): pet is A {
    return (<A>pet).a!== undefined;
}

  上述实例中pet is A就是类型谓词,谓词是pama is Type的格式,pama必须是当前函数里的一个参数名。
  调用isA时,ts会将变量缩减为具体的类型,只要类型与变量的原始类型是兼容的就可以,ts不仅会知道is分支中变量的类型,还知道else分支里的类型,如下:

if(isA(c)){
	c.a();
}else{
	c.c();
}
  • typeof类型保护
      可以使用typeof判断类型,进行类型保护的判断,在上面的自定义类型保护中定了判断类型的函数isA,可以使用typeof进行类型的检查,如下:
function isNumber(x: any): x is number {
    return typeof x === "number";
}

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

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

  此实例中使用typeof进行参数类型的判断,判断后就可以确定参数具体的类型了,但是这样每个原始类型的判断都需要定义一个对应的函数,ts中可以简化,不必要实现isString和isNumber函数,使用typeof x === “number”后ts会自动识别为一个类型保护,如下:

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并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。

  • instanceof类型保护
      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;
    }
}
function getRandomPadder() {
    return Math.random() < 0.5 ?new SpaceRepeatingPadder(4) : new StringPadder("  ");
}
// 类型为SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();
if (padder instanceof SpaceRepeatingPadder) {
    padder; // 类型细化为'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
    padder; // 类型细化为'StringPadder'
}

  上述实例中getRandomPadder函数的返回值有两种类型,SpaceRepeatingPadder或StringPadder,在使用返回的值的时候通过instanceof判断具体的类型,instanceof的右侧需要时一个构造函数,上述实例中SpaceRepeatingPadder的构造函数的参数类型是number,而StringPadder的构造函数的参数类型是string,所以可以通过instanceof进行判断具体的类型,ts检测是将按以下要求检测:

  1. 如果它的类型不是any,将检测此构造函数的prototype属性的类型
  2. 构造函数返回值的类型的联合。

4.类型别名

  类型别名顾名思义,就是给一个类型起一个新的名字,类型别名有时候和接口很像,但可以作用与原始值,联合类型,元组及其它任何需要手写的类型,如下:

type str = string;
type strResolver = () => string;
type strOrResolver = str | strResolver;
function getStr(n: strOrResolver): str {
    if (typeof n === 'string') {
        return n;
    }else {
        return n();
    }
}

  别名并不会新建类型,而是创建了一个新的名字来引用对应的类型,和接口一样,类型别名也可以泛型,可以添加类型参数并且在别名声明的右侧传入:

type Ct<T> = { value: T };

  也可以使用类型的别名来在属性里引用自己:

type Tree<T> = {
    value: T;
    left: Tree<T>;
    right: Tree<T>;
}

  类型别名不能出现在声明语句的右侧:

type LinkedList<T> = T & { next: LinkedList<T> };
interface Person {
    name: string;
}
var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;

  类型别名不能出现在声明右侧的任何地方:

type Yikes = Array<Yikes>; // error

  接口和类型别名的区别:类型别名可以和接口一样,但是也有一些区别,具体如下:

  1. 接口创建了一个新的名字,可以在其它任何地方调用,类型别名并不创建新名字,如下:
type Alias = { num: number }
interface Interface {
    num: number;
}
declare function aliased(arg: Alias): Alias;//此处鼠标悬浮时提示的是对象字面量类型
declare function interfaced(arg: Interface): Interface;//此处鼠标悬浮时提示的是Interface类型
  1. 类型别名不能被extends和implement,自己也不能extends和implement其它类型,因为面向对象编程应对于扩展是开放的,对于修改时封闭的,所以应该尽量使用接口代替类型别名。
  2. 如果无法使用接口来描述一个类型并且需要使用联合类型或元组类型,此时通常使用类型别名。

5.字符串字面量类型

  字符串字面量类型允许指定字符串必须的固定值,可以和联合类型,类型保护和类型别名很好的配合,通过结合可以实现类似枚举类型的字符串:

type Op = "in" | "out" | "in-out";
class UIElement {
    animate(dx: number, dy: number, op: Op) {
        if (op=== "in") { }
        else if (op=== "out") {}
        else if (op=== "in-out") {}
        else {}
    }
}
let button = new UIElement();
button.animate(0, 0, "in");
button.animate(0, 0, "unop"); // 异常,此处不能运行unop类型

  上述示例中只能从三种允许的字符串中选择其中一个来作为参数传递,传入其它的值则会报错,提示传入的值不是指定的类型范围内。
  字符串子面量类型也可以用于区分函数重载:

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
    // ... code goes here ...
}

6.可辨识联合

  可以合并字符串字面量类型,联合类型,类型保护和类型别名来创建可辨识联合的高级模式,被称作标签联合或代数数据类型,具有以下要素:

  1. 具有普通的字符串字面量属性—可辨识的特征。
  2. 一个类型别名包含了那些类型的联合—联合。
  3. 此属性上的类型保护。
interface A{
    kind: "a";
    size: number;
}
interface B{
    kind: "b";
    width: number;
    height: number;
}
interface C{
    kind: "c";
    radius: number;
}

  首先声明将要联合的接口,每个接口都有相同的属性,但有不同的字符串字面量类型,相同的属性称做可辨识的特征或标签,其它的属性则特定于各个接口,将类型进行联合,联合后进行使用可辨识联合,如下:

type D= A|B| C;
function fun(s: A) {
    switch (s.kind) {
        case "a": return s.size * s.size;
        case "b": return s.height * s.width;
        case "c": return Math.PI * s.radius ** 2;
    }
}

7.完整性检查

  当没有涵盖所有的可辨识联合变化时,想让编译器可以通知,如上述的示例中再添加E类型,同时需要更新fun函数:

type D= A| B| C| E;
function area(s: D) {
    switch (s.kind) {
        case "a": return s.size * s.size;
        case "b": return s.height * s.width;
        case "c": return Math.PI * s.radius ** 2;
    }
    // 此处应该有异常,因为缺少E类型的case操作
}

  要实现让编译器可以通知有两种方式:

  1. 启用–strictNullChecks并指定一个返回值类型
function fun(s: D): number { // 因为没有caseE类型,所以返回值应该是number|undefined
    switch (s.kind) {
        case "a": return s.size * s.size;
        case "b": return s.height * s.width;
        case "c": return Math.PI * s.radius ** 2;
    }
}

2.使用never类型,进行完整性检查:

function assertNever(x: never): never {
    throw new Error("Unexpected object: " + x);
}
function fun(s: D) {
    switch (s.kind) {
        case "a": return s.size * s.size;
        case "b": return s.height * s.width;
        case "c": return Math.PI * s.radius ** 2;
        default: return assertNever(s); // 如果有忽略的case,此处将有异常
    }
}

  上述示例中assertNever检查s是否为never类型,即为除去所有可能情况后剩下的类型,如果忘记了某个case,那么s将具有一个赶写的类型,因此会得到一个错误。
8. 多态的this类型
  多态的this类型表示某个包含类或接口的子类型,能很容易的表现连贯接口间的继承,如下:

class Base{
    public constructor(protected value: number = 0) { }
    public currentValue(): number {
        return this.value;
    }
    public add(operand: number): this {
        this.value += operand;
        return this;
    }
    public multiply(operand: number): this {
        this.value *= operand;
        return this;
    }
}
let v = new Base(2).multiply(5).add(1).currentValue();

  由于Base类使用了this类型,可以继承它,新的类可以直接使用之前的方法,不需要做任何的改变:

class A extends Base{
    public constructor(value = 0) {
        super(value);
    }
    public sin() {
        this.value = Math.sin(this.value);
        return this;
    }
}
let v = new A(2).multiply(5).sin().add(1).currentValue();

  如果没有this类型,A就不能在继承Base的同时还保持接口的连贯性,multiply方法会返回Base,并没有sin方法,但使用this类型,multiply会返回this,此处的this就是A类型。

二、Symbols

1.symbols类型:

  ES6即ECMAScript 2015之后,新加了symbol成为了一个新的原生类型,像其它nubmer和string一样。通过Symbol函数创建:

let s = Symbol();
let b = Symbol(3232);

  Symbol类型是不可改变且唯一的,即使值一样也是唯一的,如下:

let a = Symbol(1);
let b = Symbol(1);

  a === b;//此处比较时会得到false,因为symbol的值是唯一的
  symbols也可以被用做对象属性的键:

let a = Symbol();
let obj = {
[a]:111}
obj[a];

  Symbols也可以与计算出的属性名声明相结合来声明对象的属性和类成员:

const getClassNameSymbol = Symbol();
class C {
    [getClassNameSymbol](){
       return "C";
    }
}
let c = new C();
let className = c[getClassNameSymbol](); // "C"

2.常见的Syms:

  • Symbol.hasInstance:方法,会被instanceof运算符调用。构造器对象用来识别一个对象是否是其实例。
  • Symbol.isConcatSpreadable:布尔值,表示当在一个对象上调用Array.prototype.concat时,这个对象的数组元素是否可展开。
  • Symbol.iterator:方法,被for-of语句调用。返回对象的默认迭代器。
  • Symbol.match:方法,被String.prototype.match调用。正则表达式用来匹配字符串。
  • Symbol.replace:方法,被String.prototype.replace调用。正则表达式用来替换字符串中匹配的子串。
  • Symbol.search:方法,被String.prototype.search调用。正则表达式返回被匹配部分在字符串中的索引。
  • Symbol.species:函数值,为一个构造函数。用来创建派生对象。
  • Symbol.split:方法,被String.prototype.split调用。正则表达式来用分割字符串。
  • Symbol.toPrimitive:方法,被ToPrimitive抽象操作调用。把对象转换为相应的原始值。
  • Symbol.toStringTag:方法,被内置方法Object.prototype.toString调用。返回创建对象时默认的字符串描述。
  • Symbol.unscopables:对象,它自己拥有的属性会被with作用域排除在外。

三、迭代器/生成器

1.可迭代性

  当对象实现了Symbol.iterator属性时,就认为它是可迭代的,一些类型已经内置了此属性如:Array、Map、Set、String、Int32Array、Uint32Array等已经实现了各自的Symbol.interator,对象上的Symbol.interator函数负责返回供迭代的值。
for…of语句:
  for…of会遍历可迭代的对象,会调用对象上的Symbol.interator方法:

let arr = [1,2,false];
for(let e of arr){
e;//1,’2’,false
}

for…in语句:
  和for…of语句一样,都可迭代一个列表,但获取的值却不同,for…in迭代的是对象的键列表,for…of则迭代对象键对应的值。

let list = [4, 5, 6];
for (let i in list) {
    console.log(i); // "0", "1", "2",
}
for (let i of list) {
    console.log(i); // "4", "5", "6"
}

  for…in可以操作任何对象,可以查看对象的属性,但是for…of只能迭代对象的值:

let pets = new Set(["Cat", "Dog", "Hamster"]);
pets["species"] = "mammals";
for (let pet in pets) {
    console.log(pet); // "species"
}

for (let pet of pets) {
    console.log(pet); // "Cat", "Dog", "Hamster"
}

2.代码生成

  当编译目标是ES5或ES3,迭代器只允许在Array类型上使用,在非数组值上使用时,即使实现了Symbol.interator,for…of语句也会得到一个错误。对于Array,编译器会生成一个简单的for循环代替:

var numbers = [1, 2, 3];
for (var _i = 0; _i < numbers.length; _i++) {
    var num = numbers[_i];
    console.log(num);
}

  目标是ES6或更高的时候,编译器会生成相应引擎的for…of内置迭代器实现。

你可能感兴趣的:(#,Ts基础知识及面试题汇总,学习,笔记,ts,typescript,symbol,前端,前端框架)