阅读 1880

Vue3都用ts重构了,TypeScript咱也不能掉队(第二篇)

泛型(难点)

泛型就是在编译期间不确定方法的类型(广泛之意思),在方法调用时,由程序员指定泛型具体指向什么类型
使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件

假设我们要创建一个identity函数,这个函数返回他传入的任意参数。首先每个类型写一种是不行的,使用any类型会导致这个函数可以接收任何类型的arg参数,丢失了传入类型

因此我们使用到泛型:

function identity<T>(arg: T): T {
    return arg;
}
复制代码

使用:

let output = identity<string>('I am Bob')
复制代码
这里在T中指定了传入的是string类型

另一种方法更普遍,利用了类型推论 -- 编译器会根据传入的参数自动地确定T的类型:

let output = identity('I am Bob')
复制代码

类型推论帮助我们保持代码精简和高可读性

泛型类型

泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样:
function identity<T>(arg: T): T {
    return arg;
}

let myIdentity1: <T>(arg: T) => T = identity;

let myIdentity2: <U>(arg: U) => U = identity; // 也可以使用不同的泛型参数名,只要数量和使用方式对得上

let myIdentity3: {<T>(arg: T): T} = identity; // 还可以使用带签名的对象字面量来定义泛型函数
复制代码

看到这里,是不是想到可以写一个泛型接口呢? 那我们赶紧把上面的字面量拿出来做一个接口

interface GenericIdentityFn {
    <T>(arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity4: GenericIdentityFn = identity;
复制代码

当然,我们还可以传入一个泛型参数来指定泛型类型,锁定之后使用的类型

interface GenericIdentityFn {
    <T>(arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity4: GenericIdentityFn<number> = identity;
复制代码

泛型类

泛型类使用(<>)括起泛型类型,跟在类名后面。看上去与泛型接口差不多。
class GenericValue <T> {
    value: T
    add:(x:T,y:T) => T
}

let newGenericValue = new GenericValue<number>()
newGenericValue.value = 100
newGenericValue.add = function(x,y){return x + y}

console.log(newGenericValue.add(newGenericValue.value,900)) // 1000
复制代码

当然在创建GenericValue实例后面可以限制number类型,也可以限制其他类型,或者复杂的类型
类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。

泛型约束

当有了泛型之后,函数或者类中能处理的类型一下被扩大到了无限大,看起来有点失控。所以产生了泛型约束这个概念,用于声明对参数类型进行约束。

举个栗子,想访问value的length,但编译器并不能证明所有类型都有length属性,所以将报错

let apples = <T>(value: T): T => {
    console.log(value.length); // TS2339: Property 'length' does not exist on type 'T'.
    return value;
}
复制代码

所以我们将限制函数去处理任意带有.length属性的所有类型。
只要传入的类型至少包含这一属性,我们就允许。 但需要列出对于T的约束要求,为此,我们定义一个接口来描述约束条件。
创建一个包含 .length属性的接口,使用这个接口和extends关键字来实现约束:

interface Length {
    length: number;
}

let apples = <T extends Length>(value: T): T => {
    console.log(value.length);
    return value;
}
复制代码

妥!现在这个泛型函数被定义了约束,因此它不再是适用于任意类型~ 我们需要传入符合约束类型的值,必须包含必须的属性,这就是泛型约束。

类描述了所创建的对象共同的属性和方法。
类的定义

定义类的关键字为 class,后面紧跟类名,类可以包含以下几个模块(类的数据成员):

  • 字段: 字段是类里面声明的变量。字段表示对象的有关数据。

  • 构造函数: 类实例化时调用,可以为类的对象分配内存。

  • 方法: 方法为对象要执行的操作。

class Students { 
    // 字段 
    name:string; 
 
    // 构造函数 
    constructor(name:string) { 
        this.name = name 
    }

    // 方法 
    call():void { 
        console.log("hello~   " + this.name) 
    } 
}
复制代码
类的继承
类继承使用关键字 extends,子类除了不能继承父类的私有成员(方法和属性)和构造函数,其他的都可以继承。 TypeScript 一次只能继承一个类,不支持继承多个类,但 TypeScript 可以多重继承(A 继承 B,B 继承 C)。
class Students { 
   name:string;

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

class Chinese extends Students {
   constructor(name){
       super(name); //调用父类的构造方法,将父类的name属性赋值
   }
    showName(){
        super.showName();//调用父类的方法,输出父类name属性的值
    }
    call():void {
      console.log("你好! "+ this.name)
   }
}

var Bob = new Chinese('Bob'); 
Bob.call() // 你好! Bob
复制代码

类的继承:使用extends实现继承,通过super()或super.XX调用父类的构造方法或属性和普通方法

(存取器)存取属性setter,getter:会拦截住name属性的取值和存值

我们可以随意的设置 fullName,这是非常方便的,但是这也可能会带来麻烦。下面这个版本里我们先检查用户密码是否正确,然后再允许其修改人物信息

let password = "TypeScript";

class Students { 
   private _name: string;
    
   get name(): string {
       return this._name;
   }
   set name(newName: string) {
       password && password == "TypeScript"
       ? this._name = newName
       :console.log("Error: Unauthorized update of employee!");
   }
}

let Bob = new Students()
Bob.name = 'Bob'
Bob.name && console.log(Bob.name) // Bob
复制代码

若运行 tsc classDemo.ts 出错(demo 是我的文件名)
需要指定编译到版本ES5或以上
运行 tsc demo.ts -t es5

class的修饰符
javascript class不同于其他面向对象语言让人经常吐槽的是缺少修饰符,语义化不清晰

公共,私有与受保护的修饰符,分别是:

  • public(默认) 公有,可以在任何地方被访问。
// 例子
class Animal {
    public name: string;
    public constructor(theName: string) { this.name = theName; }
    public move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}
复制代码
  • protected 受保护,可以被其自身以及其子类和父类访问。
// 例子
class Person {
    protected name: string;
    constructor(name: string) { this.name = name; }
}

class Employee extends Person {
    private department: string;

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

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // 错误
复制代码
  • private 私有,只能被其定义所在的类访问,可以理解为私有的,只能自己使用。
class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

new Animal("Cat").name; // 错误: 'name' 是私有的.
复制代码
readonly
只读的,既只能读取不能改写,只读属性必须在声明时或构造函数里被初始化
static
static 关键字用于定义类的数据成员(属性和方法)为静态的,静态成员可以直接通过类名调用。
class Static{  
   static num:number; 
   
   static disp():void { 
      console.log("num 值为 "+ StaticMem.num) 
   } 
} 
 
Static.num = 12     // 初始化静态变量
Static.disp()       // 调用静态方法
复制代码
小技巧:把类当做接口使用
类定义会创建两个东西:类的实例类型和一个构造函数。 因为类可以创建出类型,所以你能够在允许使用接口的地方使用类。
class Point {
    x: number;
    y: number;
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};
复制代码

函数

TypeScript种给函数的介绍是:函数是JavaScript应用程序的基础。 它帮助你实现抽象层,模拟类,信息隐藏和模块。 在TypeScript里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义 行为的地方。 TypeScript为JavaScript函数添加了额外的功能,让我们可以更容易地使用。

说这么多读的累死了,用自己的理解就是js有的ts也有,ts加了额外的功能

函数的声明和使用就不多赘述了,和js基本一样

说说函数返回值

在使用 return 语句时,函数会停止执行,并返回指定的值

function func():return_type { 
    // 语句
    return value; 
}
复制代码
  • return_type 是返回值的类型。

  • return 关键词后跟着要返回的结果。

  • 一个函数只能有一个 return 语句。

  • 返回值的类型需要与函数定义的返回类型(return_type)一致。

带参数函数
function func_name( param1 [:datatype], param2 [:datatype]) {   }
复制代码
  • param1、param2 为参数名。

  • datatype 为参数类型。

可选参数和默认参数

在 TypeScript 函数里,如果定义了参数,则必须传入这些参数,除非将这些参数设置为可选,可选参数使用问号标识 ?

function buildName(firstName: string, lastName?: string) {
    if (lastName)
        return firstName + " " + lastName;
    else
        return firstName;
}
 
let result1 = buildName("Bob");  // 正确
let result2 = buildName("Bob", "Adams", "Sr.");  // 错误,参数太多了
let result3 = buildName("Bob", "Adams");  // 正确
复制代码

也可以设置参数的默认值,这样在调用函数的时候,如果未传入该参数的值,则使用默认参数

function numAdd(apple:number,pear:number = 2) { 
    let count = apple + pear; 
    console.log("计算结果: ",count); 
} 
numAdd(12) // 计算结果: 14
numAdd(3,23) // 计算结果: 26
复制代码
剩余参数
当我们不知道要向函数传入多少个参数,这时候可以使用剩余参数来定义。
剩余参数语法允许我们将一个不确定数量的参数作为一个数组传入。

如下我们使用...restOfName来表示剩余参数,并都为字符串

function buildName(firstName: string, ...restOfName: string[]) {
    return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("Bob", "ZhangSan", "Luca", "Lily");
复制代码
函数重载

重载是方法名字相同,而参数不同,返回类型可以相同也可以不同。
每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。

// 参数类型不同:
function func1(string):void; 
function func1(number):void;

// 参数数量不同:
function func2(n1:number):void; 
function func2(x:number,y:number):void;

// 参数类型顺序不同:
function func3(n1:number,s1:string):void; 
function func3(s:string,n:number):void;
复制代码

模块

为了与 ECMAScript 2015里的术语保持一致,“内部模块”现在称做“命名空间”。 “外部模块”现在则简称为“模块”

模块是自声明的;两个模块之间的关系是通过在文件级别上使用imports和exports建立的,这点和js一样。

模块导出

添加export关键字来导出

SomeInterface.ts

export interface SomeInterface { 
   // 代码部分
}

export { People };
export { People as Name };
复制代码
引入语句
import { someInterfaceRef } from "./SomeInterface";

import someInterfaceRef = require("./SomeInterface");
复制代码

命名空间

TypeScript 中命名空间使用 namespace 来定义,语法格式如下:

namespace SomeNameSpaceName { 
   export interface ISomeInterfaceName {      }  
   export class SomeClassName {      }  
}
复制代码

以上定义了一个命名空间 SomeNameSpaceName,如果我们需要在外部可以调用 SomeNameSpaceName 中的类和接口,则需要在类和接口添加 export 关键字。

要在另外一个命名空间调用语法格式为:

SomeNameSpaceName.SomeClassName;
复制代码

如果一个命名空间在一个单独的 TypeScript 文件中,则应使用三斜杠 /// 引用它,语法格式如下::

/// <reference path = "SomeFileName.ts" />
复制代码

嵌套命名空间

namespace namespace_name1 { 
    export namespace namespace_name2 {
        export class class_name {    } 
    } 
}
复制代码

合并命名空间

与接口相似,同名的命名空间也会合并其成员。
namespace Animals {
    export class Zebra { }
}

namespace Animals {
    export interface Legged { numberOfLegs: number; }
    export class Dog { }
}
复制代码

等同于:

namespace Animals {
    export interface Legged { numberOfLegs: number; }

    export class Zebra { }
    export class Dog { }
}
复制代码

装饰器

装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。
  • 装饰器本质上就是一个方法,可以注入到类、方法、属性、参数上,扩展其功能;
  • 常见的装饰器:类装饰器、属性装饰器、方法装饰器、参数装饰器等;
  • 装饰器在写法上有:普通装饰器(无法传参)、装饰器工厂(可传参)
  • 装饰器已是ES7的标准特性之一,是过去几年JS最大的成就之一!

注意: 装饰器是一项实验性特性,在未来的版本中可能会发生改变。若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json里启用experimentalDecorators编译器选项:

"compilerOptions": {
    "experimentalDecorators": true
}
复制代码

属性装饰器

方法装饰器会在运行时传入2个参数: 1.target —— 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。 2.attr —— 成员的名字。

function myProp(params:any) {
    return function(target:any, attr:any) {
        console.log(target)  // { constructor:f, getData:f } 
        console.log(attr) 
        target[attr] = params;  //通过原型对象修改属性值 = 装饰器传入的参数
        target.src = 'https://juejin.im/';  //扩展属性
        target.say = function() {  //扩展方法
            console.log('hello');
        }
    }
}

class demo {
    @myProp('http://baidu.com')
    public url:any|undefined;
    getUrl() {
        console.log(this.url);
    }
}

var a:any = new demo();
console.log(a.src);  // 'https://juejin.im/'
a.getData();  // http://baidu.com
a.say();  // hello
复制代码

方法装饰器

方法装饰器会在运行时传入3个参数: 1.target —— 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。 2.propertyKey —— 属性的名称。 3.descriptor —— 方法的属性描述符。


function enumerable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.enumerable = value;
    };
}

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }

    @enumerable(false)
    greet() {
        return "Hello, " + this.greeting;
    }
}

复制代码

参数装饰器

参数装饰器接收的三个装饰器:
-对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。 -成员的名字。 -参数在函数参数列表中的索引。

// 原型,方法名,参数所在位置
function paramDecorator(target: any, method: string, paramIndex: number) {
  console.log(target, method, paramIndex);
}

class Student {
  getInfo(@paramDecorator name: string, age: number) {
    console.log(name, age);
  }
}

const Bob = new Student();

Bob.getInfo('apple', 30)

复制代码

类装饰器

类装饰器在类声明之前被声明(一般写在class上面),应用于类构造函数,可以监视、修改、替换类的定义

function logClz(params:any) {
    params.prototype.url = 'https://juejin.im/';
    params.prototype.run = function() {
        console.log('into https://juejin.im/');
    };
}
@logClz
class Client {
    constructor() {

    }
}

var a:any = new Client();
a.run(); // into https://juejin.im/
复制代码

@logClz 为HttpClient动态扩展属性属性和方法

装饰器工厂:闭包,返回的函数才是真正的装饰器。 就像一个函数执行,可以不传参,但不能丢掉小括号

function logClz(params:string) {
    console.log('params:', params);  //params: hello
    return function(target:any) {
        console.log('target:', target);  //target: class HttpClient
        target.prototype.url = params;  //扩展一个url属性
    }
}
@logClz('hello')
class HttpClient {
    constructor() { }
}
var http:any = new HttpClient();
console.log(http.url);  //hello
复制代码

TS支持多个装饰器同时装饰到一个声明上,语法支持从左到右,或从上到下书写; 不同装饰器的执行顺序:属性装饰器 > 方法装饰器 > 参数装饰器 > 类装饰器

总结

对于现在的前端形式,ts这块再不啃,心中越是牵挂。 保持对技术的热爱和追求,技术也会越来愈善待自己 在学习的路上也希望能以内容产出的形式增加记忆。 编写这些内容涉及到 TypeScript 等基本知识,希望这篇文章能对大家以及对自己有所启发,也欢迎大家多多指教,大佬轻虐~~~