• TypeScript语法基础


    什么是TypeScript?

    TypeScript是微软开发的一门编程语言,它是JavaScript的超集,即它基于JavaScript,拓展了JavaScript的语法,遵循ECMAScript规范(ES6/7/8+)。

    TypeScript = Type + Script(标准JS),它可以编译成纯JavaScript,已经存在的JavaScript也可以不加改动地在TS的环境上运行。

    目前, Angular 已经使用 TypeScript 重构了代码,另一大前端框架 Vue 的3.0版本也将使用 TypeScript 进行重构。在可预见的未来,TypeScript 将成为前端开发者必须掌握的开发语言之一。

    为什么要使用TypeScript?

    1. 提升开发效率。
    2. 提升可维护性。
    3. 提升线上运行质量。TS有编译期的静态检查,加上IDE的智能纠错,尽可能的将BUG消灭在编译器上,线上运行时质量更稳定可控。
    4. 可读性强,适合团队协作

    TypeScript开发环境

    npm install -g typescript  // 安装ts编译器
    tsc hello.ts // 手动编译ts文件,会生成同名js文件
    tsc --init  // 生成tsconfig.js文件

    当然,我们可以配置webpack,开启node服务,进行热更新开发。

    TypeScrip数据类型

    学习数据类型前,要先明白两个概念:

    强类型和弱类型

    强类型指一个变量一旦声明,就确定了它的类型,此后不能改变它的类型。弱类型可以随便转换。TypeScript是强类型语言,JavaScript是弱类型语言。

    静态类型和动态类型

    静态类型语言:编译阶段检查所有数据的类型。动态类型语言:将检查数据类型的工作放在程序的执行阶段,也就是说,只有在程序执行时才能确定数据类型。

    基本类型

    在ES6的基础上,新增了void、any、never、元组、枚举、高级类型。

    布尔、数字、字符串

    let bool: boolean = true;
    let num: number = 10;
    let str: string = "abc";
    let abc: number | boolean | null; // 可以为一个变量声明多种类型,除非有特殊需求,一般不建议这样做。

    数组

    TypeScript的数组,所有元素只能是同一种数据类型。

    let arr1: number[] = [1,2,3];
    let arr2: Array<number> = [4,5,6]; // 数组的第二种声明方式,与前面等价
    let arr3: string[] = ["hello","array","object"];

    元组

    元组是特殊的数组,限制了元素的个数和类型。

    let tuple: [number,string,boolean] = [10,"hello",true]; 
    // tuple.push(5); // 元组越界,但不会报错。原则上不能对tuple push,这应该是一个缺陷
    // console.log(tuple[3]); // 新增的元素无法访问。

    函数

    • 函数的声明定义(三种方式)
    • 函数传参
    1.  可选参数必须放在必选参数的后面
    2.  使用ES6的默认参数,不需要声明类型
    3.  使用ES6的剩余参数,需要声明类型
    // 方式一 箭头函数:声明和定义分开
    let compute: (x:number, y:number) => number; // 函数声明,规定了传入参数、返回值的数据类型
    compute = (a, b) => a+b;  // 函数定义时,参数名称不必与声明时的相同
    // 方式二:箭头函数:声明的同时定义
    let add = (x:number, y:number) => { return x+y }; // 返回值的类型可省略,这利用了ts的类型推断功能
    // 方式三:function关键字:声明和定义分开
    function add (x: number,y: number): number;
    // 方式四:function关键字:声明的同时定义
    function add (x: number,y: number): number{ retrun x+y; }
    //函数传参:
    function add789(x: number, y?: number, z=1, ...rest: number[]) {
        console.log(rest);
        return y ? x+y : x
    }

    对象

    // 正确的写法
    let obj1: {x:number, y:number} = {x: 1, y: 2};
    obj1.x = 3;
    
    // 不建议的写法
    let obj: object = {x: 1, y: 2}; 
    obj.x = 3; // 报错,因为定义的时候绕过了声明检查,此时不知道是什么类型。

    symbol

    let s1: symbol = Symbol();
    let s2 = Symbol();

    undefind、null

    let un: undefined = undefined;
    let nu: null = null;
    nu = null; // 这样是允许的,需要将tsconfig中“strictNullChecks”置为false
    un = undefined;

    void

    是一种操作符,可以让任意一个表达式返回undefined。之所以引进void,是因为undefined不是一个保留字,可以在局部作用域内将其覆盖掉。

    let noReturn = () => {};

    any

    any 表示变量可以为任何类型,在TS中一般不用它。如果使用它,也便失去了使用TS的意义,与前面不建议为变量声明多种类型是一个道理。

    let x: any;
    x = 1;
    x = "str";

    never

    表示永远不会有返回值的类型

    let error = () => {
        throw new Error("errors")
    };
    let endless = () => {
        while(true)
        {}
    };
    // 以上两个例子永远不会有返回值

    枚举类型 enum

    枚举主要来定义一些常量,方便记忆,减少硬编码,增加可读性。

    基本使用:

    // 数字枚举
    enum Role {
        Reporter, // 默认从0开始
        Developer=5, // 也可指定某个值
        Maintainer,
    }
    console.log(Role); // 可以看到数据结构,能够进行反向映射,即通过值来访问成员
    console.log(Role.Developer);  // 访问枚举成员
    
    // 字符串枚举 不可以进行反向映射
    enum message {
        success = "成功了",
        fail = "失败了"
    }
    console.log(message);
    
    // 异构枚举,将数字和字符串混用
    enum Answer {
        N = 0,
        Y = "yes"
    }

        注意:不能修改枚举成员的值

    枚举成员的分类:

    枚举成员的分类:
        (1)常量枚举成员
        (2)对已有枚举成员的引用
        (3)常量表达式
        (4)非常量表达式。这种成员的值不会在编译阶段确定,在执行阶段才会有
    例:
    enum Char {
        a,
        b = 9,
        c = message.success,
        d = b,
        e = 1 + 3,
        f = Math.random(),
        g = "123".length,
    }
    console.log(Char);

    常量枚举和枚举类型

    // 常量枚举 用const声明的枚举都是常量枚举。特性:在编译阶段被移除,编译后不会出现
    // 作用:当我们不需要对象,只需要对象的值的时候
    const enum Month{
        Jan,
        Feb,
        Mar,
    }
    let month = [Month.Jan,Month.Feb,Month.Mar];
    console.log(month);
    
    // 枚举类型 枚举可以作为一种类型
    let e: Role = 2;
    let e1: Role.Developer = 12;  // 数字枚举类型与number类型相互兼容,因此可以复制
    console.log(e,e1); // 按照Role的类型去声明新变量
    
    let g1: message.success = "hello"; // 报错,字符串枚举类型message.success与string类型不兼容
    
    e === e1; // 可比较
    e === g1; // 不可比较,因为类型不一样

    interface接口

    接口可以用来约束对象、函数、类的结构和类型,是一种契约,并且声明之后不可改变。

    1.定义 (interface关键字)

    interface List {
        id: number;
        name: string;
    }
    
    interface Result {
        data: List[]; // 表示由List接口组成的数组
    }
    
    function render(result:Result) {
        result.data.forEach((value)=>{
            console.log(value);
        })
    }
    
    let result = {
        data:[
            {id:1,name:"a"}, 
            {id:2,name:"b"},
        ],
    };
    render(result);

    2.内部规范了什么?

    通过上述例子,看到接口规范了成员名称、成员的的类型、值的类型。

    此外,还可以规范成员属性。

    3.成员属性

    可选属性和只读属性

    interface List {
        readonly id: number; // readonly表示只读属性
        name: string;
        age?: number; // ?表示可选属性
    }

    4.索引签名

    当不确定接口中有多少属性的时候,可以用索引签名。

    格式:[x: attrType]: valueType  分别规定了成员的类型和值的类型,即通过什么来索引和访问的值的类型。

    一般通过数字和字符串来索引,也可以两者混合索引。

    // 用数字索引
    interface StringArray {
        [index: number]: string; // 表示,用任意的数字去索引StringArray,都会得到一个string。这就相当于声明了一个字符串类型的数组
    }
    let chars: StringArray = ["A","B"]; // 此时,chars就是一个字符串数组,我们可以用下标去访问每个元素
    console.log(chars,chars[0]);
    
    // 用字符串和数字混合索引
    interface Names {
        [x: string]: string; // 用任意的字符串去索引Names,得到的结果都是string。
        // y: number; // 此时不能声明number类型的成员
        // [y: number]: number // 报错,因为x和y的值string和number类型不兼容
        [z: number]: any; // 两个签名的返回值类型之间要相互兼容。为了能保持类型的兼容性。
    }
    let names: Names = {"ming":"abc",1:"45"};
    console.log(names[1],names["ming"]); // 通过数字索引、通过字符串索引

    ※ 注意值的类型要兼容

      (1)索引签名和普通成员

        如果设置了[x: string]: string,不能再设置y: number。如果设置了[x: string]: number不能再设置y: string

      (2)索引签名和索引签名

        如果多个索引签名的值不同,要注意相互兼容,比方any和string

    5.函数传参时如何绕过类型检查

    如果在接收的后端数据中,比约定好的接口多了一个字段,能否通过类型检查?会不会报错?

    let result = {
        data:[
            {id:1,name:"a",sex:"man"}, 
            {id:2,name:"b"},
        ],
    };
    render(result); // 这样是不会报错的,只要满足接口约定的必要条件即可
    
    render({
        data:[
            {id:1,name:"a",sex:"man"},
            {id:2,name:"b"},
        ],
    }); // 但如果这样调用,会报错,因为无法通过sex:"man"的类型检查。这时候需要用其他方法

    我们有三种方法:

    1. 通过接口定义变量,函数调用时传入变量名(只对必要的约定条件进行检查,多余的数据不做检查)
    2. 类型断言(所有约定都不做类型检查,失去了ts类型检查的意义)
    3. 索引签名

     第一种方法已经在上面做了示例,我们看后面两种方法如何做:

    // 类型断言
    render({
        data:[
            {id:"b",name:3,sex:"man"},
            {id:2,name:"b"},
        ],
    }as Result); // 明确告诉编译器,数据符合Result,这样,编译器会绕过类型检查
    render(<Result>{ 
        data:[
            {id:1,name:"a",sex:"man"},
            {id:2,name:"b"},
        ],
    }); // 与上等价,但在React中容易引起歧义。不建议使用
    
    // 索引签名
    interface List {
        id: number;
        name: string;
        [x: string]: any; // 字符串索引签名。用任意字符串去索引List,可以得到任意的结果,这样List接口可以支持多个未知属性
    }

    在什么场景下用什么方法,需要我们熟知这三种方法的特性

    6.接口和函数

    接口可以用来定义函数的传参、返回值的类型

    interface Add1 {
        (x: number,y: number): number;
    }
    let add1: Add1 = (a,b) => a+b;

    此外,还可以用类型别名来定义函数

    type Add2 = (x: number,y: number) => number;
    let add2: Add2 = (a,b) => a+b; // 声明+定义

    我们再来总结一下函数的声明定义方式:

    1. 普通声明定义(function、箭头函数)
    2. 接口定义类型
    3. 类型别名

    另外,接口内也可以定义函数

    // 混合类型接口
    interface Lib {
        abc(): void;
        version: string;
        doSomething(): void;
    }
    
    function getLib(){
        let lib: Lib = {
            abc: ()=>{},
            version: "1.0",
            doSomething: ()=>{}
        };
        // let lib: Lib = {} as Lib; // 定义的时候,这种方式更方便
        lib.version = "1.0";
        lib.doSomething = () => {};
        return lib;
    }
    let lib1 = getLib();
    console.log(lib1,lib1.version,lib1.doSomething());

    class类

    关于类的成员:

    1.属性必须有类型注解

    2.属性必须有初始值

    3.属性修饰符:

      (1)公有 public

        所有成员默认都是public。可以通过各种方式访问。

      (2)私有 private

        私有成员只能在类中被访问,不能被实例和子类访问。如果给构造函数加上私有属性,表示这个类既不能被实例化也不能被继承。

      (3)受保护 protected

        受保护成员只能在类和子类中访问,不能通过它们的实例访问。如果给构造函数加上受保护属性,表示这个类不能被实例化只能被继承。也就是声明了一个基类。

      (4)静态 static

        静态成员只能通过类名和子类名访问,不能被实例访问。

      (5)只读 readonly

        只读成员不能被修改。

    4.以上属性除了可以修饰成员,也可以修饰构造函数中的参数(static除外)。这样可以省去构造函数之中外的类型注解,简化代码。

    class Dog{
        constructor(name: string){
            this.name = name; // 属性必须赋初值
        }
        name: string; // 必须要为属性添加类型注解。
        run(){
            console.log("running");
            this.pri(); // 只能在类内部访问私有成员
            this.pro();
        }
        private  pri(){ // 私有成员只能被类本身调用,不能被类的实例和子类调用
            console.log("pri是dog类的私有属性");
        }
        protected pro(){ // 受保护成员只能在类和子类中访问
            console.log("pro是dog类的受保护属性");
        }
        readonly logs: string = "new";
        static food: string = "food"; // 静态修饰后,只能通过类名调用,不能被子类和实例调用。静态成员可以被继承
    }
    let dog = new Dog("dog1");
    dog.run(); 
    console.log(Dog.food); // 通过类名访问静态成员

    抽象类和多态

    所谓抽象类(abstract),是只能被继承,不能被实例化的类。

     抽象方法

    在抽象类中,不必定义方法的具体实现,这就构成了抽象方法。在抽象类中使用abstratct关键字修饰后,不能定义该方法的具体实现。

    抽象方法的好处是:实现多态。

    interface AnimalParam{
        bigClass: string,
        environment: string,
        [x: string]: string;
    }
    abstract class Animal{  // 抽象类用abstract关键字修饰
        constructor(params: AnimalParam) {  // 构造函数参数使用接口是为了其子类在定义的时候方便传参
            this.bigClass = params.bigClass;
            this.environment = params.environment;
        }
        bigClass: string;
        environment: string;
        abstract sleep(): void;
    }
    
    class Dogs extends Animal{
        constructor(props: AnimalParam){
            super(props);
            this.name = props.name;
        }
        name: string;
        run(){
            console.log("running");
        }
        sleep() {
            console.log("dog sleep")
        }
    }
    class Cat extends Animal{
        sleep(): void {
            console.log("cat sleep")
        }
    }
    let dog1 = new Dogs({bigClass:"a",environment:"b",name:"xiaoqi"});
    let cat = new Cat({bigClass:"a",environment:"b"});
    
    let animals: Animal[] = [dog1,cat];
    animals.forEach(i=>{
        i.sleep();
    });
     this

    我们可以在方法中返回this,可以进行链式调用,非常方便。

    class WorkFlow{
        step1(){
            return this;
        }
        step2(){
            return this;
        }
    }
    class Myflow extends WorkFlow{
        next(){
            return this;
        }
    }
    console.log(new WorkFlow().step1().step2());
    console.log(new Myflow().next().step1().step2());

    类和接口

    类类型接口

    • 接口约束类成员有哪些属性,以及它们的类型
    • 类在实现接口约束的时候,必须实现接口中描述的所有内容。可以多,不可以少
    • 接口只能约束类的公有成员,即不可以将接口中规定的成员置为非公有的属性
    • 接口不能约束类的构造函数
    interface Human {
        name: string;
        eat(): void;
    }
    class Asian implements Human{
        constructor(name: string){
            this.name = name;
        }
        name: string;
        eat(){}
        sleep(){}
    }

    接口继承接口

    interface  Man extends Human{
        run(): void
    }
    interface Child {
        cry(): void
    }
    interface Boy extends Man,Child{} // 多继承,将多个接口合并成一个接口

    接口继承类

    可以理解为,将类转化成接口。接口集成类的时候,不仅抽离了公有成员,也抽离了私有成员、受保护成员。

    如何理解呢?这么做的目的是限定接口的使用范围,并不会真正为这个接口添加类的私有和受保护属性,而这个限定范围就是:只能由子类来实现这个接口。

    class Auto{
        state = 1;
        private state2 = 0;
        protected state3 = 3;
    }
    interface AutoInterface extends Auto{} // 接口继承类
    
    class C implements AutoInterface{ // C在实现这个接口的时候,无法实现接口中的私有成员和受保护成员,因此报错
        state = 1
    }
    class Bus extends Auto implements AutoInterface{ // Auto的子类Bus,遵循AutoInterface接口
        showMsg(){
            // console.log(this.state2);
            console.log(this.state3);
        }
    }
    
    let bus = new Bus();
    bus.showMsg();
    console.log(bus);

    非空断言操作符

    它会排除掉变量中的 null 和 undefined,只需要从变量后面添加 ! 即可。

    function simpleExample(a: number | undefined) {
       const b: number = a; // COMPILATION ERROR: undefined is not assignable to number.
       const c: number = a!; // OK
    }

    非空断言操作符会经常在程序中用到,例如获取 ref,它会被规范为 HTMLElement | null,默认值为 null,在需要使用的时候经常会被编译器报错,提示你这个变量有可能是 null。往往我们比编译器更懂程序,在这个时候需要我们告诉编译器,这个地方一定有值。

    // 不使用 !,显得有些啰嗦
    const goToInput = () => ref.current && ref.current.scrollIntoView();
    // 使用 !,代码更简洁
    const goToInput = () => ref.current!.scrollIntoView();
  • 相关阅读:
    数据库设计>相片管理设计思路小讨论
    ADO.NET数据库连接的几种方式
    ADO.NET两种事务处理方法
    ADO.NET调用存储过程
    [sql server] 得到连续日期查询(转)
    数据库设计基础>范式
    repeater控件的事件
    小偷程序的学习总结
    《道德经》程序员版第十章
    数据库设计基础>ER图(转)
  • 原文地址:https://www.cnblogs.com/V587Chinese/p/11455862.html
Copyright © 2020-2023  润新知