TypeScript学习笔记(二)

ES6中的类

回顾ES6中的,为ts中的作基础铺垫。

ES5创建实例

ES5通过创建构造函数,调用new创建实例对象。

//构造函数Point,当调用new创建实例对象时,this指向创建的实例,给对象设置属性x和y
function Point(x,y){
    this.x = x;
    this.y = y;
}
//在Point的原型对象上定义getPosition方法,实例会继承这个方法
Point.prototype.getPosition = function (){
    return '{'+this.x+', '+this.y+'}'
}
var p1 = new Point(2,3)
console.log(p1);
console.log(p1.getPosition());
var p2 = new Point(4,5)
console.log(p2.getPosition());

ES6创建实例

class Point{
    //如果没有定义constructor,系统会默认一个空的constructor
    // constructor(){}
    constructor(x,y){
        // 如果想给生成的实例上添加属性或者方法,都需要显示地添加到this上
        this.x = x;
        this.y = y;
        // return { a:'a' }
    };
    getPosition(){
        return `{${this.x},${this.y}}`
    };
}
const p1 = new Point(1,2) //一定要用new创建类的实例
console.log(p1 instanceof Point); //如果constructor有返回值,则p1就不会是Point类的实例对象



// 检测实例是否拥有某个属性或方法
console.log(p1.hasOwnProperty('x')); //true
console.log(p1.hasOwnProperty('getPosition')); //false
console.log(p1.__proto__.hasOwnProperty('getPosition'));//true 
//p1.__proto__ 隐式原型  一个对象的隐式原型指向构造该对象的构造函数的原型
console.log(p1.__proto__ === Point.prototype); //true

取值函数和存值函数

取值函数存值函数就是把一个属性拆开成2个存取值函数

//es5
var info = {
    _age:18,
    set age(newValue){
        if(newValue>18){
            console.log('怎么变老了');
        }else{
            console.log('哈哈我还年轻');
        }
    },
    get age(){
        console.log('你问我年龄干嘛');
        return this._age
    }
}
console.log(info.age); 
info.age = 17
info.age = 18
//es6
class Info{
    constructor(age){
        this._age = age
    }
    set age(newVal){
        console.log('new age is '+newVal);
        this._age = newVal
    }
    get age(){
        return this._age
    }
}
const infos = new Info(12)
infos.age = 13
console.log(infos.age); 

class表达式

//第一种形式
class Infos{ 
     constructor(){}
 }

//第二种形式
const Infos = class {  //匿名类
    constructor(){}
}

const Infos = class info{ // 使用该类时类名是Infos而不是info,info这个类名只能在类体内部访问
    constructor(){ }
}

const testInfo = new Infos()

静态方法

静态方法就是只能被类本身使用,不能被类的实例对象调用

class Point {
    constructor(x,y){
        this.x = x;
        this.y = y;
    }
    getPosition(){
        return `{ ${this.x}, ${this.y} }`
    }
    static getClassName(){
        return Point.name
    }
}
const p = new Point(1,2)
console.log(p.getPosition()); // { 1, 2 }
console.log(p.getClassName()); //error
console.log(Point.getClassName()); // Point


//ES6明确规定,Class内部只有静态方法,没有静态属性,但是可以通过以下写法实现静态属性
class Point {
    constructor (){
        this.x = 0
    }
}
Point.y = 2
const p = new Point()
console.log(p.x); //0
console.log(p.y); //undefined

私有方法

类中的私有方法有3种实现方法。

1.通过命名区分 下划线_一般说明是一个私有方法

class Point {
    func1(){

    }
    _func2(){

    }
}

2.将私有方法移出模块

const _func2 = ()=>{}
class Point{
    func1(){
        _func2.call(this)
    }
}

const p = new Point()
p._func2() //报错

3.利用Symbol值的唯一性

//a.js
const func1 = Symbol('func1')
export default class Point{
    static [func1](){

    }
}

//b.js
import Point from "./a";
const p = new Point();
console.log(p);

new.target

new.target属性 一般用于构造函数中,返回new命令作用域的构造函数。

function Point(){
    console.log(new.target);
}
const p = new Point()
const p2 = Point() //undefined

class Point {
    constructor(){
        console.log(new.target);
    }
}
const p3 = new Point()

继承类中, new.target会返回子类而不是父类 利用这一特性,实现类不能直接创建实例,只能通过继承它的子类来实例化。

class Parent {
    constructor(){
        // console.log(new.target);
        if(new.target === Parent){
            throw new Error['不能实例化']
        }
    }
}

class Child extends Parent {
    constructor(){
        super()
    }
}
const c = new Child()  //输出子类

TS中的类

基础使用

ts中的类跟es6一样也是使用 class 定义类,使用 constructor 定义构造函数。

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

class Point{
    x:number
    y:number
    constructor(x:number,y:number){
        this.x = x
        this.y = y
    }
    getPosition(){
        return `(${this.x},${this.y})`
    }
}

const point = new Point(1,2)
console.log(point);
//继承
class Parent {
    name:string
    constructor(name:string){
        this.name = name
    }
}
class Child extends Parent {
    constructor(name:string){
        super(name)
    }
}

访问修饰符

ts中可以使用三种访问修饰符,分别是publicprivateprotected

public

public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public

priavte

private 修饰的属性或方法是私有的,不能在声明它的类的外部访问

class Parent {
    private age:number
    constructor(age:number){
        this.age = age
    }
}
const p = new Parent(18)
console.log(p);
console.log(p.age); // 报错,属性“age”为私有属性,只能在类“Parent”内部中访问

在继承的子类里也访问不到父类私有属性

class Child extends Parent {
    constructor(age:number){
        super(age)
        console.log(super.age); // 属性“age”为私有属性,只能在类“Parent”中访问
        // console.log(super.age); // 没有报错,但是实际拿不到属性,显示undefined
        console.log(super.getAge()); //报错,只能在类“Parent”中访问
    }
}

private也可以修饰类的constructor函数,此时该类不允许被继承或者实例化

class Fruit {
    public name;
    private constructor(name) {
      this.name = name;
    }
  }
  // 报错 无法扩展类“Fruit”。类构造函数标记为私有。
  class Apple extends Fruit {
    constructor(name) {
      super(name);
    }
  }

  let f = new Fruit('Apple'); // 报错 类“Fruit”的构造函数是私有的,仅可在类声明中访问。

protected

protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中是允许被访问的

class Parent {
    protected age:number
    constructor(age:number){
        this.age = age
    }
    protected getAge(){
        return this.age
    }
}
const p = new Parent(18)
console.log(p);
// console.log(p.age); // 报错,属性“age”为保护属性,只能在类“Parent”中内部访问
// console.log(Parent.age); // 报错,在类上也访问不到

class Child extends Parent {
    constructor(age:number){
        super(age)
        console.log(super.age); // 没有报错,显示undefined
        console.log(super.getAge()); //没有报错
    }
}

当类的构造函数修饰为 protected 时,该类只允许被继承。

class Fruit {
    public name;
    protected constructor(name) {
      this.name = name;
    }
  }

  class Apple extends Fruit {
    constructor(name) {
      super(name);
    }
  }

  let f = new Fruit('Apple'); // 报错 类“Fruit”的构造函数是受保护的,仅可在类声明中访问

readonly

readonly修饰符可以在类中使用将属性设置为只读的。

class UserInfo {
    readonly name : string
    constructor(name:string){
        this.name = name
    }
}
const userInfo = new UserInfo('zzz')
console.log(userInfo);
// userInfo.name = 'haha' //报错 无法分配到 "name" ,因为它是只读属性。

参数属性

访问修饰符readonly还可以使用在构造函数参数中,等同于类中定义该属性同时给该属性赋值,使代码更简洁。

class Animal {
  // public name: string;
  public constructor(public name) {
    // this.name = name;
  }
}
const a = new A('zzz')
console.log(a); //{name: 'zzz'}

静态属性

在属性前添加static后,实例不会添加静态属性也不会继承静态属性,只能通过类本身访问

class Parent {
    static age:number = 18
    static getAge(){
        return Parent.age
    }
    constructor(){}
}
const p = new Parent()
console.log(p.age); // 报错
console.log(Parent.age); // 18
console.log(Parent.getAge()); // 18

可选属性

可选类属性 也是使用?标记

class Info{
    name:string
    age?:number
    constructor(name:string,age?:number,public sex?:string){
        this.name = name
        this.age = age
    }
}
const info1 = new Info('zzz')
const info2 = new Info('zzz',18)
const info3 = new Info('zzz',18,'女')

console.log(info1); // Info {sex: undefined, name: 'zzz', age: undefined}
console.log(info2); // Info {sex: undefined, name: 'zzz', age: 18}
console.log(info3); // Info {sex: '女', name: 'zzz', age: 18}

存取器

在类中使用gettersetter

class Info{
    name:string
    age?:number
    private _infoStr:string
    constructor(name:string,age?:number,public sex?:string){
        this.name = name
        this.age = age
    }
    get infoStr(){
        return this._infoStr
    }
    set infoStr(value){
        console.log(`setter:${value}`);
        this._infoStr = value
    }
}

抽象类

abstract 用于定义抽象类和其中的抽象方法。

抽象类不能创建自己的实例

abstract class People{
    constructor(public name:string){}
    public abstract printName():void
}
// 抽象类不能创建自己的实例
const p1 = new People() // 报错

抽象类中的抽象方法必须被子类实现,如果没有实现抽象方法,会编译报错

class Man extends People {
    constructor(name:string){
        super(name)
        this.name = name
    }
    public printName() {
        console.log(this.name);
    }
}
const m = new Man('zzz')
m.printName() // zzz

abstract还可以用在存取器

abstract class People{
    abstract _name:string
    abstract get insideName():string
    abstract set insideName(value:string)
}

class P extends People {
    _name : string
    insideName:string
}

实例的类型

在声明class XX时,除了会创建一个名为 XX 的类之外,同时也创建了一个名为 XX 的类型(实例的类型)。可以给类的实例加上类型

class People {
    constructor(public name:string){}
}
// 明确指定实例就是People类
let p2:People = new People('zzz')

类实现接口

接口可以对类的一部分行为进行抽象,类用 implements 关键字来实现接口。

interface FoodInterface{
    type:string
}
// 类实现接口
class FoodClass implements FoodInterface{
    type: string
}

接口检测的是使用该接口定义的类创建的实例,所以不能将该接口定义的类需要定义的属性设置为static

接口继承类

在ts中,接口可以继承类,只会继承它的实例属性和实例方法。

接口还会继承privateprotected修饰的成员(接口继承的类中包含这2个修饰符修饰成员时接口只能被这个类和子类实现)

class A {
    protected name :string
}
interface I extends A{}
class B extends A implements I{
    name:string
}

在泛型中使用类类型

创建函数,传入的参数是一个类。

//new()=>T new()调用类的构造函数  它的类型就是类创建实例后的实例的类型
const create = <T>(c:new() =>T):T=>{
    return new c()
}
class Infos{
    age:number
    constructor(){
        this.age = 18
    }
}
console.log(create<Infos>(Infos)); // Infos {age: 18}
console.log(create<Infos>(Infos).age); // 18

枚举

枚举(Enum)类型用于取值被限定在一定范围内的场景。

数字枚举

枚举成员会被赋值为从 0 开始递增的数字

enum Status{
    Uploading,
    Success,
    Failed
}
console.log(Status.Uploading);  //写法一
console.log(Status["Success"]); //写法二 如果有引入tslint会自动转成上面的写法一

可以给枚举项手动赋值序号

enum Status{
    Uploading,
    Success = 3,
    Failed  //这里就会根据前一项的序号递增
}
console.log(Status.Uploading); // 0
console.log(Status.Success); // 3
console.log(Status.Failed); // 4

当某个字段手动赋值使用的是常量或计算值,该字段后面紧接着的字段必须设置初始序号值,不能使用默认的递归值。

//使用常量
const test = 1
enum Status{
    Uploading,
    Success = test,
    Failed =5 // 不设置会提示枚举成员必须具有初始化表达式
}
console.log(Status.Uploading); // 0
console.log(Status.Success); // 1
console.log(Status.Failed); // 5

//使用计算值
const getIndex = ()=>{
    return 2
}
enum Status{
    Uploading,
    Success = getIndex(),
    Failed =5 // 不设置会提示枚举成员必须具有初始化表达式
}

反向映射

枚举不仅可以通过字段名得到枚举值,还可以反过来通过枚举值得到字段名。

enum Status{
    Uploading,
    Success,
    Failed
}

这个例子实际会被编译为:

var Status;
(function (Status) {
    Status[Status["Uploading"] = 0] = "Uploading";
    Status[Status["Success"] = 1] = "Success";
    Status[Status["Failed"] = 2] = "Failed";
})(Status || (Status = {}));

既添加了字段名到值的映射也添加了值到字段名的映射。

打印上面的Status可以看到这样一个对象

{
     0: "Uploading"
     1: "Success"
     2: "Failed"
     Failed: 0
     Success: 1
     Uploading: 2
}

字符串枚举

字符串枚举要求每个字段的枚举值都是字符串字面量或者是使用该枚举里的另一个字符串枚举成员

enum Message{
    Error = 'Sorry,error',
    Success = 'Hoho,success',
    Failed = Error
}
console.log(Message.Error);  // Sorry,error
console.log(Message.Failed); // Sorry,error

字符串枚举不能使用常量计算值的。

异构枚举

异构枚举简单来说就是既包含数字枚举值又包含字符串枚举值。(不建议使用)

enum Result {
    Failed = 0,
    Success = 'success'
}

枚举成员类型和联合枚举类型

当一个枚举满足一定条件的时候,这个枚举的每个成员可以作为类型来使用。

条件:
​ 1.不带初始值
​ 2.枚举值为字符串字面量
​ 3.枚举值为数值字面量

将枚举成员作为类型来使用

enum Animals{
    Dog = 1,
    Cat = 2
}
interface Dog {
    type:Animals.Dog
}
const dog:Dog = {
    type:1
    //type:Animals.Cat //报错
}

当一个枚举的每个成员枚举值符合三种条件之一,这个枚举本身就可以看做是包含所有成员的联合类型(联合类型是高级类型,之前有简单介绍,既可以是XX类型又可以是XX类型)

enum Status{
    Off,
    On,
}
interface Light{
    status:Status // status的类型只能是Status.Off或是Status.On
}
const light:Light = {
    status:Status.On
    // status:Animals.Dog //报错
}

const enum

正常的enum会在编译完后生成一个对象,如果使用const enumenum就会在编译后消失

const enum Animals {
    Dog = 1
}
const dog = Animals.Dog

// 编译后会变成
let dog = 1 //没有Animals这个对象了,Animals.Dog变成了常量1

类型推论

基础

如果没有明确的指定类型,但是在定义时赋值了,TypeScript会根据赋的值推断出是什么类型

let name = 'zzz'
name = 123 // 不能将类型“number”分配给类型“string”

事实上,它等价于:

let name:string = 'zzz'
name = 123 // 不能将类型“number”分配给类型“string”

TypeScript会在没有明确的指定类型的时候推测出一个类型,这就是类型推论。

如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 **any** 类型而完全不被类型检查

let name; // 变量name隐式具有any类型
name = 'zzz'
name = 123

多类型联合

如果定义这样一个数组

let arr = [1,'a']

根据类型推论,会等价于

let arr:Array<number|string> = [1,'a']

对该多类型联合数组赋值

arr = [2,'b']
// arr = [2,'b',false] //报错 ,不能将类型“boolean”分配给类型“string | number”。

上下文类型

上面2个例子都是根据等号右边的具体值推断出变量是什么类型。上下文类型是根据等号左边去推断等号右边的类型。

//在ts 3.x版本
window.onmousedown = (mouseEvent)=>{
    console.log(mouseEvent.pageX); // 没有报错
    console.log(mouseEvent.a); // 报错
}

注:本人ts版本是4.6.2,没有出现上下文类型推断,mouseEvent还是隐式具有any类型,没有报错。

类型兼容性

类型兼容性用于确定一个类型是否能赋值给其他类型,TypeScript里的类型兼容性是基于结构子类型的。TypeScript结构化类型系统的基本规则是,如果x要兼容y,那么y至少具有与x相同的属性。比如:

interface Info {
    name:string,
    info:{
        age:number
    }
}
let infos:Info
const infos1 = { name:'zzz',info:{ age:18 } }
const infos2 = { age:18 }
const infos3 = { name:'zzz',age:18 }
infos = infos1;
// infos = infos2; // infos2缺少name属性
// infos = infos3; // info3 缺少info属性

这个检测过程是递归检测,深层次属性也会检测到。

函数的兼容性

如何判断两个函数是兼容的,考虑到的东西比较多。

参数个数

要查看函数y是否能赋值函数x,首先看它们的参数列表。

如果函数y想赋值给函数xy参数个数必须要小于等于x的参数个数,也就是,y的每个参数必须能在x里找到对应类型的参数。

let x = (a:number)=>0
let y = (a:number,c:string)=>0
 y = x //可以赋值
 x = y //报错

参数类型

要查看函数y是否能赋值函数x,要看它们的参数类型是否对应

函数y的参数类型为string,而函数x的参数类型为number,两者参数类型不对应,因此不兼容。

 let x = ( a: number) => 0
 let y = ( b: string) => 0
 x = y //参数类型不对应

可选参数和剩余参数

比较函数兼容性的时候,可选参数必须参数是可互换的。

如果函数x的函数参数包含剩余参数,赋值的函数y可以使用任意个数的参数代替,但是参数类型需要对应

const getSum = (arr:number[],callback:(...args:number[])=>number):number =>{
    return callback(...arr)
}
const res = getSum([1,2,3],(...args:number[]):number=>args.reduce((a,b)=>a+b,0))  //6
const res2 = getSum([1,2,3],(arg1:number,arg2:number,arg3:number):number=>arg1+arg2+arg3) //6

函数参数双向协变

函数参数双向协变即参数类型无需绝对相同。

函数A的参数类型是一个联合类型,既可以是number也可以是string,而函数B的参数类型是number类型,这两个函数兼容。但在严格模式下,funcA = funcB不兼容,funcB = funcA兼容。

let funcA = (arg:number|string):void => {}
let funcB = (arg:number):void =>{}

 funcA = funcB //兼容

 funcB = funcA //兼容

返回值类型

两个函数比较时,除了要看参数之外,还要看两者的返回值类型。

let x = ():string | number =>0
let y = ():string=>'a'
let z = ():boolean => false

 x = y //兼容 返回值类型可以都为string

 y = x //不兼容 x的返回值类型可能是number,而y的返回值类型不可能是number

 y = z //不兼容

函数重载

带有重载的函数,要求被赋值的函数的每个重载都能在用来赋值的函数上找到对应的签名。

例如:函数A2个重载情况,函数B1个重载情况,如果B赋值A,会出现不兼容,因为B缺少了1个函数重载情况。

function merge(arg1:number,arg2:number):number  // 函数重载
function merge(arg1:string,arg2:string):string  // 函数重载
function merge(arg1:any,arg2:any){
    return arg1 + arg2
}

function sum(arg1:number,arg2:number):number
function sum(arg1:any,arg2:any):any{
    return arg1 + arg2
}
let func = merge
func = sum // 不兼容,sum缺少一个函数重载情况

枚举

  • 枚举类型与数字类型相互兼容

    enum StatusEnum{
        On,
        Off,
    }
    let s = StatusEnum.On
    s = 2 // 与数值类型兼容
    
  • 不同枚举类型之间是不兼容的

    enum StatusEnum{
        On,
        Off,
    }
    enum AnimalEnum{
        Dog,
        Cat
    }
    let s = StatusEnum.On
    s = 2 // 与数值类型兼容
    s = AnimalEnum.Dog // 不同枚举值不兼容
    

比较两个类类型的对象时,只比较实例的成员静态成员构造函数不在比较的范围内。

class AnimalClass{
    public static age:number
    constructor(public name:string){}
}
class PeopleClass{
    public static age:string
    constructor(public name:string){}
}
class FoodClass{
    constructor(public name:number){}
}

let animal:AnimalClass
let people:PeopleClass
let food:FoodClass
animal = people // 兼容,不会检测构造函数和静态成员,实例成员的类型兼容
animal = food  // 不兼容,实例成员的类型不兼容

类的私有成员受保护成员会影响兼容性。私有成员受保护成员必须来自于相同的类,否则不兼容。

class ParentClass{
    private age:number
    constructor(){}
}
class ChildrenClass extends ParentClass{
    constructor(){
        super()
    }
}
class OtherClass{
    private age:number
    constructor(){}
}
const children:ParentClass = new ChildrenClass() //兼容
const other:ParentClass = new OtherClass() //不兼容

泛型

TypeScript 类型系统基于变量的结构,仅当类型参数在被一个成员使用时,才会影响兼容性。

例如

interface Data<T> {
   
}
let data1:Data<number>
let data2:Data<string>
data1 = data2 //兼容

上面代码中,data1data2是兼容的,因为它们的结构使用类型参数时并没有什么不同。

泛型T被成员使用时,它将在实例化泛型后影响兼容性:

interface Data<T> {
    data:T
}
let data1:Data<number>
let data2:Data<string>
data1 = data2 // 不兼容

对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any比较。 然后用结果类型进行比较

let indetity = <T>(x:T): T => {
    return
}
let reverse = <U>(y:U):U=>{
    return
}
indetity = reverse //兼容

posted @ 2022-04-17 16:18  小风车吱呀转  阅读(187)  评论(0编辑  收藏  举报