“类型思维”之Typescript,你掌握了吗?

技术文章 1年前 (2020) 完美者
264 0

标签:系统   成员修饰符   src   好的   mic   继承   之一   完全   each   

(一)背景

JavaScript是一门动态弱类型语言 对变量的类型非常宽容 而且不会在这些变量和它们的调用者之间建立结构化的契约。

试想有这么几个场景:

 

1: 你调用一个别人写的函数,但是这个人没有写注释,为了搞清楚参数类型,只能去看里面的逻辑

2: 为了保证代码的健壮性,你需要对一个函数的输入参数进行各种假设判断

3: 让你维护一个重要的底层类库,你不小心更换了一个参数类型,但是不知道有多少处的引用

4: 明明定义好的接口,可一连调就报错了,TypeError:Cannot read property ‘length’of undefined,于是你就找后端理论,这个接口定义不是数组么?

以上场景归根结底就是因为 javascript 是动态 弱类型语言,长时间在没有类型约束的环境下开发,就会造成“类型思维”的缺失,养成不良的编程习惯,这也是做前端开发的短板之一

1、强类型语言

在强类型语言中,当一个对象从调用函数传递到被调用函数时,其类型必须与被调用函数中声明的类型兼容 ——Liskov, Zilles 19

 

技术图片

上图解释:有两个方法,方法A和方法B。方法A调用方法B的时候,x类型必须要和y类型兼容。兼容就意味着,y可以赋值给x,而且程序还可以正常运行 这是比较宽泛的定义,没有产生具体的规则,现在我们对强类型语言的定义会更精确一些 。

强类型语言:不允许改变变量的数据类型, 除非进行强制类型转换

 

 技术图片           技术图片

以Java语言为例:

(左图)报错:提示类型不兼容,不能将布尔类型转换为整型

(右图)打印 x = 97。java将字符a进行了强制类型转换,将a的ACSLL码 强制给了x,这样x类型依然是整型

2、弱类型语言

弱类型语言: 在弱类型语言中, 变量可以被赋予不同的数据类型

技术图片

以js语言为例。图解:把z赋值给x 7、打印 x 为 字符串‘a‘ 。从这里就可以看出JS是一门弱类型语言

3、静态类型语言与动态类型语言

静态类型语言:在编译阶段确定所有变量的类型

动态类型语言:在执行阶段确定所有变量的类型

技术图片                   技术图片

左图解 JS代码 :当编译器在看add方法这行代码的时候无法知道变量a、b的数据类型,只有在程序执行的时候,才能根据实际传递进来的参数再确定

右图解 C++代码: C++ 和JS不同的是,它在编译的时候就能够确定变量的数据类型,而且a、b类型一定是整型

4、静态类型与动态类型比较

技术图片

1、 从对比来看,静态类型语言的优势要大于动态类型语言。如果是一门动态弱类型语言,就更会被打入鄙视链的底端。

2、 动态类型语言的支持者也有自己的理由

  • 性能是可以改善的(V8引擎),V8在性能方便做得很好,相对损失的性能而言,语言的灵活性更重要
  • 隐藏的错误可以通过单元测试发现
  • 文档可以通过工具生成

其实,这种争论一直存在,这说明任何语言的特点都具有两面性,也是一个发展和进化的过程,不能一概而论,要看具体的场景和性价比。

比如js就是一门动态弱类型语言,但是它目前还是被广泛应用。这也引申出一道面试题:大家也可以一起思考一下: TS会替代JS么?

我认为TypeScript更多地是对JavaScript的补充,而不是替代品,而且两者的普及都在增长。

5、语言类型象限

技术图片

横轴代表 从动到静。纵轴代表 从弱到强

这个图可以帮助我们理解 动态类型、静态类型、强类型和弱类型语言有哪些。

值得庆幸的是,开源社区也一直努力的解决这个问题,早在2014年 Facebook 推出了 Flow,微软也在同一年发布了 TypeScript 1.0 版本,它们都是致力于为JS提供静态类型检查,如今过去6年了。TS发展好像是更好一些,多个团队,比如 Angular和Vue,开始全面使用TS重构代码,甚至连Facebook自家的产品 比如 Jest和Yarn,都在从Flow向TS转移。 可以说在ECMScript标准推出静态类型检查之前,TS是当下解决此问题的最佳方案。

(二)Typescript是什么?

Typescript是拥有类型系统的JavaScript的超集。

可以编译成纯JavaScript。需要注意三点:

1. 类型检查。typescript会在编译代码时进行严格的静态类型检查,这就意味着,你可以在编写代码的阶段,发现可能存在的隐患,而不是上完线才发现

2. 语言拓展。typescript会包括来自ES6和未来提案中的特性,比如异步操作和装饰器,还有就是,也会从java语言借鉴某些特性,比如接口和抽象类。

3. 工具属性。typescript可以编译成标准的javascript,可以在任何浏览器操作系统上运行,无需任何运行时的额外开销,从这个角度上讲,typescript更像是一个工具,而不是一门独立的语言

1. 基本类型 

ES6的数据类型:• Boolean • Number • String • Symbol • undefined • null • Array • Function • Object

Typescript的数据类型在ES6的基础上新增了几种: • void • any • never • 元组  • 枚举  • 高级类型

  // 原始数据类型
  let bool: boolean = true
  let num: number = 123
  let str: string = ‘abc‘

  // 数组
  let arr: number[] = [1, 2, 3]
  let arr: Array<number|string> = [1, 2, 3, ‘4‘]

  // 元组(特殊的数组,限定了数组元素的类型和个数)
  let tuple2: [number, string] = [0, ‘1‘]
  // 元组越界问题:
  tuple2.push(2)
  console.log(tuple2)
  tuple[2] // 报错,不能进行越界访问

  // 函数
  let add = (x, y) => x + y; // 报错。需要为参数加上类型注解
  let add = (x: number, y: number): number => x + y
  let add = (x: number, y: number) => x + y
  add11 = add22 // 类型推断可以省略返回值number  
  let compute: (x: number, y: number) => number
  compute = (a, b) => a + b

  // 对象
  let obj: object = { x: 1, y: 2 }
  obj.x = 3 // 报错。
  let obj: { x: number, y: number } = { x: 1, y: 2 }
  obj.x = 3

  // symbol
  let s1: symbol = Symbol()
  let s2 = Symbol()
  console.log(s1 === s2) // false

  // undefined, null
  let un: undefined = undefined // 被声明undefined,就不能赋值任何其他数据类型,只能赋值它本身
  let un: undefined = ‘1‘ // 报错
  let nu: null = null
  num = undefined // 报错。ts官方文档中,undefined和null是任何类型的子类型,说明可以赋值给其他类型,可以设置strictNullChecks:false
  num = null


  // void 可以确保任何表达式返回值一定是undefined
  console.log(void 0) // undefined。
  // 原因:undefined在js中不是保留字,也可以自定义undefined变量覆盖全局undefined
  // 如下:控制台打印输出
  (function (){
     var undefined = 0;
    console.log(undefined)
  })()
  let noReturn = () => {}

  // any
  let x
  x = 1
  x = [[]]
  x = () => {}

  // never
  let error = () => {
     throw new Error(‘error‘)
  }
  let endless = () => {
     while(true) {}
  }

2. 类型注解

作用:相当于强类型语言中的类型声明

语法:(变量/函数):类型名称

3. 枚举

场景:如下图伪代码为例:权限操作判断函数,不同权限对应不同UI界面,用户登陆系统,一般会做初始化的工。

这种代码存在两个问题: 1) 可读性差:很难记住数字的含义。 2) 可维护差:硬编码,牵一发动全身

技术图片

解决方案:TS的枚举类型。枚举可以理解成手机里的通讯录,拨打电话的时候只需要记住人名,不需要记住电话号码,而且,号码是可变性的,人名是不可变的

 

// 数字枚举,有反向映射
  enum Role {
    Reporter, // 0
    Developer, // 1
    Maintainer, // 2
    Owner, // 3
    Guest // 4
  }
  Role.Reporter = 2  // 报错。枚举成员只读不可修改

// 字符串枚举,没有反向映射
enum Message {
    Success = ‘恭喜你,成功了‘,
    Fail = ‘抱歉,失败了‘
}

// 异构枚举(字符串/数字枚举混用)不建议使用
enum Answer {
    N,
    Y = ‘Yes‘
}

// 枚举成员的分类
enum Char {
    // const member 常量枚举,会在编译的时候计算结果,以常量的形式出现在运行时环境
    a,          // 1.无初始值
    b = Char.a, // 2.对已有成员的引用
    c = 1 + 3,  // 3.常量表达式
    
    // computed member 计算枚举,非常量表达式,不会在编译阶段计算,而是会保留到程序的执行阶段
    d = Math.random(),
    e = ‘123‘.length,
}

// 常量枚举。会在编译阶段被移除
// 使用场景:不需要对象,只是需要对象值时
const enum Month {
    Jan,
    Feb,
    Mar,
    Apr = Month.Mar + 1
}
let month = [Month.Jan, Month.Feb, Month.Mar]
// console.log(month, ‘month==‘)

// 枚举类型
enum E { a, b }
enum F { a = 0, b = 1 }
enum G { a = ‘apple‘, b = ‘banana‘ }

let e: E = 3
let f: F = 3
console.log(e === f) // 报错 不同类型的枚举不可比较

let e1: E.a = 3
let e2: E.b = 3
let e3: E.a = 3
console.log(e1 === e2) // 报错console.log(e1 === e3) // 正确

// 字符串枚举取值只能是枚举成员的类型
let g1: G = G.a
let g2: G.a = G.a

4. 接口

4.1 作用:接口可以用来约束对象、函数以及类的结构的类型,是一种代码编写的契约,我们严格遵守,而且不能改变

4.2 语法:interface (变量名称) { }

4.3 分类:

  1. 对象类型接口
  2. 函数类型接口
  3. 混合类型接口
// 定义一个List接口
interface List { readonly id: number; name: string; [x: string]: any;
// 字符串索引签名,不确定接口返回个数 age?: number; // 可索引接口 } interface Result { data: List[] } function render(result: Result) { result.data.forEach((value) => { console.log(value) if (value.age) { console.log(value.age, ‘---‘) } }) } let result = { data: [ {id: 1, name: ‘A‘, sex: ‘male‘}, {id: 2, name: ‘B‘} ] } render(result)

4.4 数字索引接口

interface StringArray {
    [index: number]: string
}
let chars: StringArray = [‘a‘, ‘b‘]

4.5 字符串索引接口

interface Names {
    [x: string]: string;
    // [x: string]: any;
    // y: number;
    [z: number]: string; // 数字索引签名的返回值必须要是字符串索引签名返回值的子类型,因为javascript会进行类型转换,将number转换成string,这样就能保证类型的兼容性
    // [z: number]: number;
}

使用接口的好处:可以思考变量的类型,也可以思考接口的边界问题,这个过程非常有利于培养‘类型思维’ 

5. 函数

5.1 函数定义几种方式:

  • function, function a(x: number, y: number) => number
  • 变量, let a:(x: number, y: number) => number
  • 类型别名, type a = (x: number, y: number) => number
  • 接口, interface a { (x: number, y: number): number }
// function
function
add1(x: number, y: number) { return x + y }
// 变量 let add2: (x: number, y: number)
=> number
// 类型别名 type add3
= (x: number, y: number) => number
// 接口 interface add4 { (x: number, y: number): number } add1(
1, 2, 3) // 报错。 ts中形参和实参必须一一对应

5.2 可选参数

function add5(x: number, y?: number) {
    return y ? x + y : x
}
add5(1)

5.3 增加默认值

function add6(x: number, y = 0, z: number, q = 1) {
    return x + y + z + q
}
add6(1, undefined, 3)
console.log(add6(1, undefined, 3))

5.4 扩展运算符

function add7(x: number, ...rest: number[]) {
    return x + rest.reduce((pre, cur) => pre + cur);
}
add7(1, 2, 3, 4, 5)
console.log(add7(1, 2, 3, 4, 5))

5.4 函数重载 

函数重载的好处:不需要为了相似功能的函数选用不同的函数名称,增强了函数的可读性
function add8(...rest: number[]): number;
function add8(...rest: string[]): string;
function add8(...rest: any[]) {
    let first = rest[0];
    if (typeof first === ‘number‘) {
        return rest.reduce((pre, cur) => pre + cur);
    }
    if (typeof first === ‘string‘) {
        return rest.join(‘‘);
    }
}
console.log(add8(1, 2))
console.log(add8(‘a‘, ‘b‘, ‘c‘))

6. 类

我们知道es6引入了class关键字,我们也终于可以像传统的面向对象语言那样去创建一个类。 总体上来说,ts的类覆盖了es6的类,同时也引入了其他的特性, 那么两者之间有什么不同呢?

6.1  类的实现

注意1: 类成员属性都是实例属性,不是原型属性
注意2: 类成员方法都是实例方法,不是原型方法
class Dog {
    constructor(name: string) { // 自动配对为类的本身 Dog
        this.name = name
    }
    public name: string = ‘dog‘
    run() {}
}
// console.log(Dog.prototype) // 报错。类成员属性都是实例属性,不是原型属性let dog = new Dog(‘Dog‘)console.log(dog)

6.2  类的继承

类的继承,extends关键字

class Dog {
    constructor(name: string) {
        this.name = name
    }
    public name: string = ‘dog‘
    run() {}
}
let dog = new Dog(‘Dog‘)
console.log(dog)


class Husky extends Dog {
    constructor(name: string, color: string) {
        super(name)
        this.color = color
    }
    color: string
}

6.3  类的成员修饰符

成员修饰符:public、private、protected、readonly、static

 

// 父类
class Dog1 {
    constructor(name: string) {
        this.name = name
        this.pri()
    }
    name: string = ‘dog1‘        // 类的所有属性默认是public
    run() {}
    private pri() {}                // 私有成员,只能被类的本身调用不能被类的实例和子类调用
    protected pro() {}              // 受保护成员,只能在类或者子类中访问,不能在类的实例中访问
    readonly legs: number = 4       // 只读属性
    static food: string = ‘bones‘   // 静态成员。
    sleep() {
        // console.log(‘Dog sleep‘)
    }
}
let dog1 = new Dog1(‘Dog1‘)
console.log(dog1.pri) // 报错
console.log(dog1.pro) // 报错
console.log(Dog1.food)
console.log(dog1.food) // 报错。静态成员只能通过类名调用,不能通过子类来调用

// 子类调用
class Husky extends Dog1 {
    // 构造函数参数也可以添加修饰符,变成实例的属性
    constructor(name: string, public color: string) { 
        super(name)
        this.color = color
        // this.pri() // 报错
        this.pro()
    }
    // color: string
}
console.log(Husky.food) // 类的静态成员也可以被继承

6.4  类的抽象类

抽象类:abstract 关键字。ES中没有引入抽象类的概念,这是ts对es的又一次扩展。所谓抽象类就是只能被继承,而不能被实例化的类。

优点:1. 方法的复用

   2. 可以实现多态

abstract class Animal {
    eat() {
        // console.log(‘eat‘)
    }
    abstract sleep(): void // 抽象方法。好处:明确知道子类可以有其他的实现,不必在父类中实现
}
let animal = new Animal(); // 报错。只能被继承,而不能被实例化

class Dog2 extends Animal {
    constructor(name: string) {
        super()
        this.name = name
    }
    name: string
    run() {}
    sleep() {
        console.log(‘Dog2 sleep‘)
    }
}
let dog2 = new Dog2(‘Dog2‘)
console.log(dog2.eat()) // ‘eat‘。子类可以调用抽象类里的方法
console.log(dog2.sleep()) // ‘Dog2 sleep‘

6.4  类的多态

多态:在父类中定义一个抽象方法,在多个子类中对这个方法有不同的实现,在程序运行的时候,会根据多个对象,执行不同操作。

// 父类

abstract class Animal {

eat() {
 console.log(‘eat‘)  } abstract sleep(): void }
// 子类中对这个抽象方法有不同的实现
class Cat extends Animal {
    sleep() {
        console.log(‘Cat sleep‘)
    }
}
let cat = new Cat()
console.log(cat.sleep()) // ‘Cat sleep‘

6.5  类的this类型(特殊类型)

定义:类的成员方法会返回一个this,这样就可以方便实现链式调用。

class Workflow {
    step1() {
        console.log(this, ‘step1‘)
        return this
    }
    step2() {
        console.log(this, ‘step2‘)
        return this
    }
}
new Workflow().step1().step2()

// 继承的时候,this也可以表现为多态。this既可以是父类型,也可以是子类型
// 作用:保证了父类调用和子类调用的连贯性
class MyFlow extends Workflow {
    next() {
        console.log(this, ‘next‘)
        return this
    }
}
new MyFlow().next().step1().next().step2() // 父类和子类之间调用的连贯性

7. 类与接口的关系

接口可以像类一样,相互继承,一个接口可以继承多个接口。 但是,接口继承类时需注意,接口在抽离类的成员的时候,不仅抽离了公共成员,而且抽离了私有成员和受保护成员。

技术图片

图解:

1、首先接口之间是可以相互继承的,这样可以实现接口的复用

2、类之间也可以相互继承,可以实现方法和属性的复用

3、接口可以通过类来实现,但是接口只能约束类的共有成员,不能约束类的私有成员

4、接口也可以抽离出类的成员。抽离的时候会包括共有成员,私有成员,受保护成员

7.1 类类型接口

一个类通过关键字implements声明自己使用一个或者多个接口。

注意:
1. 类实现的接口,必须要实现类接口中声明定义的所有的属性
2. 类型接口不能约束类的构造函数
3. 类接口只能约束类的共有成员,不能约束类的私有成员

interface Human {
    // new (name: string): void; // 2.报错。类型接口也不能约束类的构造函数
    name: string;
    eat(): void;
}
// implements 可以实现多个接口,用逗号分开就行,接口的方法一般为空的, 必须重写才能使用
class Asian implements Human {
    constructor(name: string) {
        this.name = name;
    }
    name: string
    // private name: string // 3.报错 类接口只能声明共有成员
    eat() {}
    // 可以定义自己的属性
    // age: number = 0
    // sleep() {}
}

7.2 接口继承

接口继承接口。接口的继承可以抽离出可重用的接口。

interface Man extends Human {
    run(): void
}
interface Child {
    cry(): void
}
interface Boy extends Man, Child {}
let boy: Boy = {
    name: ‘‘,
    eat() {},
    run() {},
    cry() {}
}

接口继承类。接口把类的成员都抽象了出来,有类的成员结构,没有具体的实现

class Auto {
    state = 1
    // private state1 = 0 // 报错。注意:接口在抽离类的成员的时候,不仅抽离了公共成员,而且抽离了私有成员和受保护成员
}
interface AutoInterface extends Auto {

}
class C implements AutoInterface {
    state = 1
}

8. 泛型

很多时候,我们希望一个函数或者一个类能支持多种数据类型,有很大的灵活性,如下图:

技术图片

 

 

定义一个log函数,接收一个string,最后打印出这个字符串。但是,我希望这个字符串能接收一个字符串数组,根据前面介绍的,应该怎么实现呢?

1、可以通过函数重载实现

技术图片

先定义一个接收字符串的函数,再定义一个接收字符串数组的函数,最后在一个比较宽泛的版本里打印实现

2、也可以通过联合类型实现

技术图片

 

 

看起来比函数重载简便一些。

3、但是,现在我希望这个函数能接收任何类型的参数,实际上从前面的函数重载也可以看出,可以使用any类型。

技术图片

看样子这个函数好像是满足了我们的需求。但是,产生了另外的一个问题,any类型会丢失一些信息,就是类型之间的约束关系,忽略了输入参数的类型和函数返回值的类型必须是一致的这个问题。

当一个调用者看见这个log函数的时候,完全无法获知这种约束关系,这个时候就需要用到“泛型”了!

那么什么是泛型呢???

8.1 什么是泛型

泛型定义:不预先确定的数据类型, 具体的类型在使用的时候才能确定。

8.2 泛型参数

泛型从字面上理解就是“一般的,广泛的,不需要预先定义的的数据类型” 下面我们用泛型改造一下上面的log函数。

function log<T>(value: T): T {
    // console.log(value);
    return value;
}
log<string[]>([‘a‘, ‘,b‘, ‘c‘])
log([‘a‘, ‘,b‘, ‘c‘]) //ts的类型推断,可以省略类型参数,直接传入数组

除了上述的参数可以用泛型表示,函数、接口、类可不可以更灵活的去用泛型呢?答案是,必须能!

8.3 泛型函数

type Log = <T>(value: T) => T
let myLog: Log = log

看样子是不是很简单。

8.4 泛型接口

interface Log<T> { // 约束接口的所有成员
    (value: T): T
}
// let myLog: Log = log; // 报错。注意:当泛型约束了所有的接口之后,使用的时候必须指定一个类型
let myLog: Log<number> = log
myLog(1)

8.3 泛型类

class Log<T> {
    // static run(value: T) { // 报错。注意:泛型不能运用于类的静态成员
    //     console.log(value)
    //     return value
    // } 
    run(value: T) {
        // console.log(value)
        return value
    }
}
let log1 = new Log<number>()
log1.run(1)
let log2 = new Log<string>()
log2.run(‘1‘)
let log3 = new Log(); // 不指定类型参数,value可以是任意类型的值
log3.run({ a: 1 })

8.4 泛型约束

interface Length {
    length: number
}
function log<T extends Length>(value: T): T { // 继承Length接口,受到Length接口的约束
    // console.log(value, value.length);
    return value;
}
log([1])
log(‘123‘)
log({ length: 3 })
log(123) // 报错:number没有length属性

8.5 泛型好处

1) 增强程序的可扩展性:函数或类可以很轻松地支持多种数据类型

2) 增强代码的可读性:不必写多条函数重载,或者冗长的联合类型声明

3) 灵活地控制类型之间的约束

有了泛型,类型就像穿上了变色龙的外衣,可以很友好的融入各种环境,代码的灵活性就大大增强了。

 

下一章介绍TS的类型检查机制,尽情期待!

 

“类型思维”之Typescript,你掌握了吗?

标签:系统   成员修饰符   src   好的   mic   继承   之一   完全   each   

原文地址:https://www.cnblogs.com/xdfapp/p/13644171.html

版权声明:完美者 发表于 2020-09-17 20:42:18。
转载请注明:“类型思维”之Typescript,你掌握了吗? | 完美导航

暂无评论

暂无评论...