美文网首页
Learning TypeScript

Learning TypeScript

作者: FTD止水 | 来源:发表于2020-08-24 20:51 被阅读0次

    前言

    TypeScript(以后简称TS)是JavaScript(以后简称JS)的一个超级,支持当前最新的ES规范,提供了对数据类型的各种约束,使前端开发也能像后端一样使用强类型语言,这种强类型的约束虽然增加了不少前端开发的工作量,但确可以在编译的时候将因数据类型不严谨造成的bug及时暴露出来,以便减少在运行的时候出现错误。所以TS非常适合用于大型项目和团队多人协作开发。现在有不少前端框架都在逐渐支持TS,vue更是用ts进行了重构,TS必将是以后前端开发的趋势,所以是非常有必要学习的。
    自己看过一遍TS官方文档,发先它的查阅价值远远大于学习价值,它每一章都希望能详尽的描述一个概念,导致前面的章节就会包含很多后面才会学习到的内容,如果想从官网上学习TS,那种体验简直跟屎一样,所以自己就想写一篇渐进式学习TS的文章,也方便以后查阅和再次学习。
    学习TS之前,必须要了解JS面向对象原理、了解ES6,了解Node的npm工具,我之后所编写的TS代码都是在node环境下运行的,所以要先安装node.js和ts-node,所用的开发工具是vscode。

    安装 TypeScript

    TypeScript 的命令行工具安装方法如下:

    cnpm install -g typescript
    

    验证是否安装成功,我们可以打开vscode,新建hello.ts文件,然后随便写一些ts代码,打开编译器终端输入运行命令

    tsc hello
    

    就会发先新生成一个hello.js的文件,这说明ts已经成功编译了,也就是说ts安装好了。

    截图.png

    然后要想在node环境下运行TS,就必须安装ts-node,命令如下:

    cnpm install -g ts-node
    

    安装好了之后在终端输入ts-node,ts的运行结果就会显示在终端。

    截图.png
    上述截图Demo中,TS使用:来指定变量的类型,如myName的类型就是string类型,如果我们赋的值类型不为string,编译器就会将静态类型检查的错误结果告诉我们,虽然编译报错了,还是会生成编译结果,我们仍然可以使用这个编译之后的文件。如果要在报错的时候终止 js 文件的生成,可以在 tsconfig.json 中配置 noEmitOnError 即可。因为这里没有构建TS项目,只是用单一文件进行Demo演示,所以没有配置文件,具体如何实现可以查阅官方文档。

    基本数据类型

    JS分为两种数据类型:基本数据类型和引用数据类型。
    基本数据类型包括:布尔值、数值、字符串、nullundefined 以及ES6新增的Symbol
    本节介绍TS中的基本数据类型的使用。

    布尔值
    let flag:boolean=false;
    
    数字
    let decLiteral: number = 9527;
    let hexLiteral: number = 0xf00d;
    // ES6 中的二进制表示法
    let binaryLiteral: number = 0b1010;
    // ES6 中的八进制表示法
    let octalLiteral: number = 0o744;
    let notANumber: number = NaN;
    let infinityNumber: number = Infinity;
    
    字符串
    let myName:string="Joker";
    
    空值

    JS没有void的概念,在TS中表示函数没有任何返回值:

    function calculate():void{
        console.log("打印计算结果")
    } 
    
    Null 和 Undefined
    let u: undefined = undefined;
    let n: null = null;
    

    void的区别是,undefinednull是所有类型的子类型。也就是说 undefined 类型的变量,可以赋值给其它类型的变量。

    // 这样也不会报错
    let u: undefined;
    let num: number = u;
    
    any(任意类型)

    如果是一个普通类型,在赋值过程中改变类型是不被允许的:

    let myName:string="Joker";
    myName=3;
    console.log(myName);
    //error
    

    但如果是any类型,则允许被赋值为任意类型:

    let myName:any="Joker";
    myName=3;
    console.log(myName);
    

    注意:TS中如果变量在声明的时候,未指定其类型,那么它会被识别为任意值类型如:

    let myName="Joker";
    myName=3;
    console.log(myName);
    

    等价于:

    let myName:any="Joker";
    myName=3;
    console.log(myName)
    
    数组类型

    在TS中,数组类型有多种定义方式,比较灵活。

    • 类型 + 方括号表示法
    let arr:number[]=[1,2,3];
    

    数组元素中不可以出现其它类型如:

    let arr:number[]=[1,2,"3"];
    

    也不可以通过操作数组,向数组中添加其它类型如:

    let arr:number[]=[1,2,3];
    arr.push("Joker");
    
    • 数组泛型表示法
    let arr:Array<number>=[1,2,3];
    
    • 用any表示任意类型数组
    let arr:any=[1,2,3,"Joker",{"name":"Link"}];
    
    元组

    元组跟数组很类似,区别只是限制了数组中指定索引的类型,如:
    定义一对值分别为stringnumber的元组:

    let arr:[string,number]=['Link',1];
    

    也可以只赋值其中一项:

    let arr:[string,number];
    arr.push("Joker");
    

    但是当直接对元组类型的变量进行初始化或者赋值的时候,需要提供所有元组类型中指定的项。
    正确写法:

    let arr:[string,number]=['Link',1];
    

    错误写法:

    let arr:[string,number]=['Link'];
    //error
    

    元组越界表示
    当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型:
    正确写法:

    let arr:[string,number]=["Joker",1];
    arr.push("Link");
    

    错误写法:

    let arr:[string,number]=["Joker",1];
    arr.push(true);
    //error
    
    never类型

    never表示函数抛出异常或没有结束方法体如:

    function calIt():never{
        while(true){
            console.log("Joker");
        }
    }
    function conIt():never{
        throw new Error();
    }
    

    同时never类型也是nullundefined的子类

    let n:never;
    let u:undefined=undefined;
    u=n;
    
    枚举

    在实际开发中,我们前端一定会碰到这种情况,当我们对之前的联调进行修改时,发先其中一个参数可能时1,2,3,4,但是我们正好忘记了这4个数分别代表什么,恰好又忘记写注释了,这时如果使用枚举,就可以完美解决某个参数值分别代表什么意思。
    枚举默认赋值

    enum gamesType {JRPG,MUG,FTG,ACT};
    console.log(gamesType.JRPG);//0
    console.log(gamesType.MUG);//1
    console.log(gamesType.FTG);//2
    console.log(gamesType.ACT);//3
    

    正反向都可以获取相应的数值如:

    enum gamesType {JRPG,MUG,FTG,ACT};
    console.log(gamesType[0]);//JRPG
    console.log(gamesType[1]);//MUG
    console.log(gamesType[2]);//FTG
    console.log(gamesType[3]);//ACT
    

    手动赋值
    这里强烈不建议只赋值一部分,要赋值就给每一个元素全都赋值,要么就全都不赋值使用默认赋值,如果只赋值一部分,及其容易出现阅读障碍和bug,所以这里不推荐也不做介绍。
    注意:这里枚举赋值最好是数字类型,这样可以反相获取,如果是其它类型,那只能正向获取了

    enum gamesType {JRPG=10,MUG=20,FTG=30,ACT=40};
    
    console.log(gamesType.JRPG);//10
    console.log(gamesType.MUG);//20
    console.log(gamesType.FTG);//30
    console.log(gamesType.ACT);//40
    
    console.log(gamesType[10]);//JRPG
    console.log(gamesType[20]);//MUG
    console.log(gamesType[30]);//FTG
    console.log(gamesType[40]);//ACT
    

    函数

    在JS中函数有两种定义方式,一种是函数声明:

    function getSum(a,b){
        return a+b;
    }
    

    一种是函数表达式:

    let getSum=function (a,b){
        return a+b;
    }
    

    TS中定义函数,要对函数的参数和返回值进行约束,先看函数声明的写法

    //函数声明
    function getSum(a:number,b:number):number{
        return a+b;
    }
    

    注意:输入多余的参数或者少于要求的参数都是不可以的

    function getSum(a:number,b:number):number{
        return a+b;
    }
    getSum(1);//error
    getSum(1,2,3);//error
    

    函数表达式写法

    let getSum=function (a:number,b:number):number{
        return a+b;
    }
    

    其实这里在等号左侧TS帮我们自动推断出了变量的类型,如果我们手动给getSum添加类型,应该如下:

    let getSum:(a:number,b:number)=>number=function (a:number,b:number):number{
        return a+b;
    }
    

    注意不要混淆了TS中的=>和ES6中的=>
    在TS的类型定义中,=>用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。
    可选参数
    前面的例子函数传入多余的参数是不可以的,TS如何实现可选参数的呢,我们用表示可选参数:

    function getSum(a:number,b:number,c?:number):void{
       if(c){
           console.log(c)
       }else{
           console.log(a+b);
       }
    }
    getSum(1,2);
    getSum(1,2,3);
    getSum(1,2,3,4);//error
    

    注意:可选参数后面不允许再出现必需参数了
    默认参数
    在TS中,我们允许给函数的参数添加默认值

    function getSum(a:number,b:number,c:number=3):void{
       console.log(a+b+c);
    }
    getSum(1,2);//结果是6
    getSum(1,2,10);//结果是13
    

    剩余参数
    TS可以使用...rest的方式获取函数中的剩余参数,rest是一个数值,我们可以用数组类型来定义它:

    function getSum(a:number,b:number,...rest:number[]):void{
       let temp=a+b;
       let box=0;
       for(let i=0;i<rest.length;i++){
            box+=rest[i]
       }
       console.log(temp,box);
    }
    getSum(1,2,3,4,5,6,7,8);//结果 3    33
    

    函数重载
    TS函数重载如果跟Jave的函数重载比起来,会发现TS函数的重载处理的很牵强,所以这里只做代码演示:

    function test(a:number):number;
    function test(a:string):string;
    function test(a:number|string):number|string{
        return a;
    }
    

    上例中,我们重复定义了多次函数 test,前几次都是函数定义,最后一次是函数实现。

    ES5中的类

    es5中是通过构造函数来实现类的,定义如下:

    function Game(){
        this.name="ZELDA";
    }
    let temp=new Game();
    console.log(temp.name);//输出结果ZELDA
    

    也可以给构造函数添加类的方法

    function Game(){
        this.name="ZELDA";
        this.start=function(){
            console.log("start game")
        }
    }
    let temp=new Game();
    console.log(temp.name);//输出结果ZELDA
    temp.start();//输出结果start game
    

    但是这种通过构造函数定义的方法有个缺陷,就是每个类实体含有这个方法,我们希望这个方法是共有的,就可以通过原型链进行优化,将相同的方法进行提取,使其成为共有方法,优化如下:

    function Game(){
        this.name="ZELDA";
    }
    Game.prototype.start=function(){
        console.log("start game")
    }
    let temp=new Game();
    console.log(temp.name);//输出结果ZELDA
    temp.start();//输出结果start game
    

    注意:原型链上定义的属性和方法会被多个实例共享,构造函数中定义的属性和方法不会
    类中的静态方法定义:我们知道实例方法必须通过new 实例化对象才能调用,静态方法是通过类名可以直接调用,方法如下:

    function Game(){
        this.name="ZELDA";
    }
    Game.start=function(){
        console.log("start game")
    }
    Game.start();//输出结果start game
    

    ES5中的继承
    可以通过对象冒称和原型链组合的形式来实现类的继承,先看对象冒充实现继承:

    function Game(){
        this.name="ZELDA";
    }
    Game.prototype.start=function(){
        console.log("start game")
    }
    function JRPG(){
        Game.call(this)
    }
    let temp =new JRPG();
    console.log(temp.name);//输出"ZELDA"
    

    但当我想调用父类的start方法时,会报错,因为对象冒充只能继承构造函数内的属性和方法,如果我们想要继承父类的prototype(原型链)上的属性和方法,可以按照如下方式实现:

    function Game(){
        this.name="ZELDA";
    }
    Game.prototype.start=function(){
        console.log("start game")
    }
    function JRPG(){
        Game.call(this)
    }
    JRPG.prototype= Game.prototype;
    let temp =new JRPG();
    console.log(temp.name);//输出"ZELDA"
    temp.start();//输出"start game"
    

    TS中的类

    TS中类个概念跟绝大多数后端开发语言非常类似,这里对类的相关概念做一个简单的介绍。

    • 类(Class):定义了一件事物的抽象特点,包含它的属性和方法
    • 对象(Object):类的实例,通过 new 生成
    • 面向对象(OOP)的三大特性:封装、继承、多态
    • 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据
    • 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
    • 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 Cat 和 Dog 都继承自 Animal,但是分别实现了自己的 eat 方法。此时针对某一个实例,我们无需了解它是 Cat 还是 Dog,就可以直接调用 eat 方法,程序会自动判断出来应该如何执行 eat
    • 存取器(getter & setter):用以改变属性的读取和赋值行为
    • 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如 public 表示公有属性或方法
    • 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现
    • 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口
      TS中类的简单用法
      定义属性和方法:
      使用class定义类,使用constructor定义构造函数。
      通过new生成新实例的时候,会自动调用构造函数
    class Game {
        name:string;
        constructor(name:string){
            this.name=name;
        }
        play():void{
            console.log(`play ${this.name}`);
        }
    }
    let temp=new Game("ZELDA");
    temp.play();
    //运行结果 play ZELDA
    

    类的继承
    使用extends关键字实现继承,子类中使用super关键字来调用父类的构造函数和方法。

    class Game {
        name:string;
        constructor(name:string){
            this.name=name;
        }
        play():void{
            console.log(`play ${this.name}`);
        }
    }
    class ARPG extends Game{
        constructor(name:string){
            super(name);
        }
        play():void{
            console.log(`${this.name}天下第一`);
        }
    }
    let temp=new ARPG("ZELDA");
    temp.play();
    //运行结果 ZELDA天下第一
    

    存取器
    使用 getter 和 setter 可以改变属性的赋值和读取行为:

    class Game {
        name:string;
        constructor(name:string){
            this.name=name;
        }
        getName():string{
            return this.name;
        }
        setName(name:string):void{
            this.name=name;
        } 
        play():void{
            console.log(`play ${this.name}`);
        }
    }
    let temp=new Game("ZELDA");
    console.log(temp.getName());//输出结果:ZELDA
    temp.setName("异度之刃");
    console.log(temp.getName());//输出结果:异度之刃
    

    静态方法
    使用static修饰符修饰的方法称为静态方法,它们不需要实例化,而是直接通过类来调用,注意:静态方法只能访问静态属性

    class Game {
        name:string;
        static price:number=350;
        constructor(name:string){
            this.name=name;
        }
        static buy():void{
            console.log(`price is ${this.price}`)
        }
        getName():string{
            return this.name;
        }
        setName(name:string):void{
            this.name=name;
        } 
        play():void{
            console.log(`play ${this.name}`);
        }
    }
    Game.buy();//输出:price is 350
    

    TS中三种访问修饰符的用法

    • public修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的
    • private修饰的属性或方法是私有的,不能在声明它的类的外部访问
    • protected修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的
      例如:
    class Game {
        public name:string;
        constructor(name:string){
            this.name=name;
        } 
    }
    class ARPG extends Game{
        constructor(name:string){
            super(name);
        }
        play():void{
            console.log(`${this.name}天下第一`);
        }
    }
    let temp=new ARPG("ZELDA");//输出:ZELDA天下第一
    console.log(temp.name)//输出:ZELDA
    

    上面的例子中,name被设置为了public,所以子类和外部的实例化对象都可以直接访问name属性。
    当我们上述例子中的public属性换成private属性时,编译器就会报错,告诉我们被private修饰的属性和方法为私有,不能被子类和外部调用。如果我们希望父类的方法和属性可以被子类调用而不想被实例外部调用,我们就可以使用protected修饰符来修饰,这里只做文字描述,不再做代码演示。
    抽象类
    abstract用于定义抽象类和其中的抽象方法。抽象类的作用是用于定义一个基础类,这个类要被子类继承和实现。
    注意:抽象类是不允许被实例化的

    abstract class Game {
        public name:string;
        constructor(name:string){
            this.name=name;
        }
        abstract play(); 
    }
    let temp =new Game("ZELDA");//error
    

    上述例子中定义了一个抽象类和一个抽象方法,在实例化时报错了。
    其次,抽象类中的抽象方法必须被子类实现:

    abstract class Game {
        public name:string;
        constructor(name:string){
            this.name=name;
        }
        abstract play(); 
    }
    
    class ARPG extends Game{
        constructor(name:string){
            super(name);
        }
        play():void{
            console.log(`${this.name}天下第一`);
        }
    }
    let temp=new ARPG("ZELDA");
    

    接口

    在面向对象的编程中,接口是一种规范的定义,它定义了行为和动作的规范,,在程序设计里面,接口起到一种限制和规范的作用。接口定义了某一批类所需要遵守的规范,接口不关心这些类的内容状态数据,也不关心这些类里方法的实现细节,它只是规定这些类里必须提供某些方法,提供这些方法的类就可以满足实际需要。TS中的接口类似于java,同时也不同于java中的接口,比如TS中的接口可对属性、函数、可索引类型进行限制和约束。
    接口的实现(implements,也可以理解成对类进行约束)
    一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用implements关键字来实现。这个特性大大提高了面向对象的灵活性。示例如下:

    interface Play{
        name:string;
        up():void;
        down():void;
        left():void;
        right():void;
    }
    class Game implements Play{
        public name:string;
        constructor(name:string){
            this.name=name;
        }
        up():void{
            console.log("ctrl up")
        }
        down():void{
            console.log("ctrl down")
        }
        left():void{
            console.log("ctrl left")
        }
        right():void{
            console.log("ctrl right")
        }            
    }
    let game=new Game("ZELDA")
    game.up();
    game.down();
    game.right();
    game.left();
    
    
    
    
    

    一个类可以实现多个接口:

    interface Music{
        playMusic():void;
    }
    interface Play{
        up():void;
        down():void;
        left():void;
        right():void;
    }
    class Game implements Play,Music{
        public name:string;
        constructor(name:string){
            this.name=name;
        }
        playMusic():void{
            console.log("play music")
        }
        up():void{
            console.log("ctrl up")
        }
        down():void{
            console.log("ctrl down")
        }
        left():void{
            console.log("ctrl left")
        }
        right():void{
            console.log("ctrl right")
        }            
    }
    let game=new Game("ZELDA")
    game.up();
    game.playMusic();
    //运行结果:
    // ctrl up
    // play music
    

    接口也可以继承接口:

    interface Music{
        playMusic():void;
    }
    interface Play extends Music{
        up():void;
        down():void;
        left():void;
        right():void;
    }
    

    接口对批量方法传入的参数进行约束(属性接口)
    我们可以通过常规方法约束函数参数如下:

    function play(info:{"name":string}):void{
        console.log(info.name);
    }
    play("zelda");//错误
    play({"type":"ARPG"});//错误
    play({"name":"zelda"});//正确
    

    但我们要定义多个上述类似于play的函数时,且他们的参数约束都是一样的,这时如果还是使用常规方式来进行约束的话会非常麻烦,我们就可以通过接口来进行约束,如下:

    interface Information{
        name:string;
    }
    function play(info:Information):void{
        console.log(info.name);
    }
    function run(info:Information):void{
        console.log(info.name);
    }
    

    可选属性接口
    如果其中一个参数可以传也可以不传,就用用到接口的可选属性,如下:

    interface Information{
        name:string;
        price?:number
    }
    function play(info:Information):void{
        console.log(info.name);
    }
    play({"name":"zelda"});
    play({"name":"zelda","price":350});
    

    函数类型接口
    有些时候当我们要用函数表达式方式定义函数,并且还要对函数进行约束,这时就要用接口对函数进行约束,示例如下:

    interface Information{
        (name:string):void;
    }
    let play:Information=function(name:string):void{
        console.log(name);
    }
    play("zelda");
    

    可索引接口
    可索引接口主要是对数组和对象进行约束,
    对数组进行约束:

    interface gameArr{
        [index:number]:string
    }
    let gameList:gameArr=['zelda','xenoblade','xenoblade2',"p5s","p5"];
    

    对对象进行约束:

    interface gameInfo{
        [index:string]:string
    }
    let gameInfo:gameInfo={"name":"zelda","price":"rmb350"};
    

    泛型

    在软件工程中,我们不仅要创建一致的、定义良好的API,同时也要考虑可重用性,组建不仅能够支持当前的数据类型,同时也能支持未来的数据类型。所以我们可以使用泛型来创建可重用的组建,一个组件可以支持多种类型的数据。通俗理解泛型就是解决类、接口、方法的复用性以及对不特定数据类型的支持,并且还要保留类型校验。
    泛型函数
    现在我们想实现一个函数,传入什么类型的参数最后就返回什么类型,可能会这样来实现:

    function getVal(value:any):any{
        return value;
    }
    

    这样写会存一个问题,就是any放弃了类型检查,传入和返回什么类型都可以,如果传入的是number类型,也可以返回string类型,就不符合上述的要求(传入类型跟返回类型一致,且传入的类型根据需要而定)。这时我们就要通过泛型来解决这个问题,如下:

    function getVal<T>(value:T):T{
        return value;
    }
    getVal<number>(1);
    getVal<string>("zelda");
    

    这样就可以根据具体情况来限制参数的类型。
    泛型类
    也可以使用泛型对类进行约束,示例如下:

    class GetVal<T>{
        val:T;
        constructor(val){
            this.val=val;
        }
    }
    let temp=new GetVal<number>(11);
    console.log(temp.val);//输出 11
    let box=new GetVal<string>("zelda");
    console.log(box.val);//输出 zelda
    

    泛型接口
    第一种定义泛型接口的方法:

    interface Get{
        <T>(val:T):T
    }
    let getVal:Get=function<T>(val:T):T{
        return val;
    }
    console.log(getVal<number>(350));
    console.log(getVal<string>("zelda"));
    

    第二种定义泛型接口的方法:

    interface Get<T>{
        (val:T):T
    }
    function getVal<T>(val:T):T{
        return val;
    }
    let myGet:Get<number>=getVal;
    myGet(350);
    

    TS模块

    TS中的模块化和ES6中的模块基本相同都是使用exportimport将各种类型的变量如:字符串、数值、函数、类进行导出和导入。例如我们在相同目录中创建a.ts和b.ts两个文件,a.ts文件代码如下:

    class Game{
        name:string;
        constructor(name:string){
            this.name=name;
        }
        show():void{
            console.log(this.name);
        }
    }
    export {Game}//此处可以导出多个变量
    

    在b.ts文件中代码如下:

    import {Game} from "./world";//此处以可引入多个模块
    let game=new Game("zelda");
    game.show();//运行结果:zelad
    

    如果想给导出的变量赋予其它的名称,我们也可以使用as来起别名,方法如下:

    import {Game as Play} from "./world";
    let game=new Play("zelda");
    game.show();//运行结果:zelad
    

    另一种导出模块的方式,export default
    在a.ts文件中代码如下:

    class Game{
        name:string;
        constructor(name:string){
            this.name=name;
        }
        show():void{
            console.log(this.name);
        }
    }
    export default Game
    

    在b.ts文件中代码如下:

    import Game from "./world";
    let game=new Game("zelda");
    game.show();//运行结果:zelad
    

    与第一种导出模块的区别如下:

    • 在一个文件或模块中,export、import 可以有多个,export default 仅有一个
    • export default 中的 default 是对应的导出接口变量
    • 通过 export 方式导出,在导入时要加{ },export default 则不需要
    • export default 向外暴露的成员,可以使用任意变量来接收。
    export let name="ts";
    import 任意变量 from "xxxx/xxxx/xx"
    

    TS中的命名空间

    在多人协作开发大型项目中,为了避免各种变量名冲突,可将相似功能的函数、类、接口等放置到命名空间内,示例如下:

    namespace A{
        export class Game{
            name:string;
            constructor(name:string){
                this.name=name;
            }
            show():void{
                console.log(this.name);
            }
        }
    }
    let game=new A.Game("zelda");
    

    TS中的装饰器(非常有用!)

    装饰器是一种特殊类型的声明,它能够附加到类声明,方法,属性或者参数上,可以修改类的行为。通俗讲,装饰器就是一个方法,可以注入到类、方法、属性参数上来扩展类、属性、方法、参数的功能。常见的装饰器有:类装饰器、属性装饰器、方法装饰器、参数装饰器。装饰器根据有无参数可分为普通装饰器(无参数),装饰器工厂(有参数)
    无参类装饰器
    作用就是不破坏原有类的结构,对类的属性和方法进行修改,示例如下:

    function decorateClass(params:any){
        console.log(params);//params 相当于当前被修饰的类
        params.prototype.price=350;
        params.prototype.showPrice=function(){
            console.log(params.prototype.price)
        }
    }
    
    @decorateClass
    class Game{
        name:string;
        constructor(name:string){
            this.name=name;
        }
        play():void{
            console.log(`${this.name} is run`);
        }
    }
    let game=new Game("zelda");
    game.play();//zelda
    game.showPrice();//350
    

    装饰器工厂(有参数)

    function decorateClass(params:string){
        return function(target:any){
            console.log(params);//输出ARPG
            console.log(target);//相当于当前被修饰的类
            target.prototype.gameType=params;
        }
    }
    
    @decorateClass("ARPG")
    class Game{
        name:string;
        constructor(name:string){
            this.name=name;
        }
        play():void{
            console.log(`${this.name} is run`);
        }
    }
    let game=new Game("zelda");
    game.play();//zelda
    console.log(game.gameType);//输出ARPG
    

    也可以利用装饰器重载类里面的属性和方法:

    function decorateClass(params:any){
        return class extends params{
            name:String;
            play():void{
                console.log("塞尔达传说天下第一");
            }
        }
    }
    
    @decorateClass
    class Game{
        name:string;
        constructor(name:string){
            this.name=name;
        }
        play():void{
            console.log(`${this.name} is run`);
        }
    }
    let game=new Game("zelda");
    game.play();//输出:塞尔达传说天下第一
    

    属性装饰器
    属性装饰器在运行时,传入两个参数,一个参数对于静态成员来说时类的构造函数,对于实例成员是类的原型对象;另一个参数是成员的名字。实例如下:

    function decorateClass(params:any){
        return function(target:any,attr:any){
            console.log(target);//是这个类的原型对象
            console.log(attr);//属性名称url
            target[attr]="zelda"
        }
    }
    
    class Game{
        @decorateClass("game")
        name:string;
        constructor(){
            
        }
        play():void{
            console.log(`${this.name} is run`);
        }
    }
    let game=new Game();
    game.play();//输出:zelda is run
    

    方法装饰器
    他会被应用到方法的属性描述上,可以用来监视、修改、替换方法定义。方法修饰在运行时传入以下3个参数:

    • 对于静态成员来说时类的构造函数,对于实例成员来说时类的原型对象
    • 成员的名字
    • 成员的属性描述
      示例如下:
    function decorateClass(params:any){
        return function(target:any,methodName:any,desc:any){
            console.log(target);//是这个类的原型对象,可用于扩展类的属性和方法
            console.log(methodName);//方法的名称
            console.log(desc);//方法的描述,也就是方法里面的代码
            target.price=350;
            target.up=function(){
                console.log("上");
            }
        }
    }
    
    class Game{
        name:string;
        constructor(){
            
        }
        @decorateClass("zelda")
        play():void{
            console.log(`${this.name} is run`);
        }
    }
    let game=new Game();
    console.log(game.price);//输出 350
    game.up();//输出:上
    

    如何修改被装饰器修饰的方法:

    function decorateClass(params:any){
        return function(target:any,methodName:any,desc:any){
            console.log(target);//是这个类的原型对象,可用于扩展类的属性和方法
            console.log(methodName);//方法的名称
            console.log(desc.value);//方法的描述,也就是方法里面的代码
            let methodBody=desc.value;
            desc.value=function(name:string){
                console.log(name);
                methodBody.apply(this);
            }
    
        }
    }
    
    class Game{
        name:string;
        constructor(){
            
        }
        @decorateClass("zelda")
        play():void{
            console.log(`${this.name} is run`);
        }
    }
    let game=new Game();
    game.play("zelda");//输出:zelda   zelda is run
    

    方法参数装饰器
    用法与上述装饰器类似,这里只做简要说明,不做代码演示了。
    方法参数修饰器表达式会在运行时当作函数被调用,可以使用参数装饰器为类的原型增加一些元素数据,传入下列3个参数:

    • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
    • 参数的名字
    • 参数在函参中的索引
      各种装饰器执行的顺序
      属性装饰器>方法装饰器>方法参数装饰器>类装饰器
      另外相同类型装饰器执行顺序为从下至上(从后向前)

    结束语

    这只是一个TS入门教程,光看懂是完全没有用的,如果想真正掌握TS还是需要在真实的项目中去实践,这样才会暴露自己对该技术中的不足之处,通过不断尝试和总结,相信一定会对TS有更全新的认识。因为自己水平有限,本文难免会有各种各样的错误,希望大家能多批评指正,或者通过留言的方式与我交流,我会在下一个版本中修正一些不足之处。

    止水
    于沈阳
    2020/08/31 22:38

    相关文章

      网友评论

          本文标题:Learning TypeScript

          本文链接:https://www.haomeiwen.com/subject/trqouhtx.html