TypeScript 语法(二)

168 阅读7分钟

常用运算符

非空类型断言 !.

用来跳过 TypeScript 的强检测。

下面这段代码,在编译时会报错,是因为 message 的值有可能会是 undefined

function showMessage(message?: string) {
   console.log(message.toUpperCase())
}

但是我们能够确定传入的参数是有值的,这时我们就可以使用非空类型断言,来跳过 TypeScript 在编译阶段对它的检测。

非空断言使用的是 ! ,表示可以确定某个标识符是有值的

function showMessage(message?: string) {
   console.log(message!.toUpperCase())
}

可选链 ?.

它的作用是当对象的属性不存在时,会短路,直接返回 undefined,如果存在,那么才会继续执行。

!! 运算符

将一个其他类型转换成 boolean 类型

?? 运算符

当操作符的左侧是 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数

TypeScript 中类的使用

类的定义

我们使用 class 关键字来定义一个类,在类的内部,我们需要声明类的属性及类型,否则会报错。

这点与 JavaScript 不同,在 JavaScript 中我们可以直接构造而不需要声明属性。

// 定义类
class Person {
   name: string
   age: number

   constructor(name: string, age: number) {
      this.name = name
      this.age = age
   }

   play() {
      console.log('play')
   }
}

// 使用类创建对象
const p = new Person('kobe', 18)

p.play()

我们可以给类的属性设置默认值,这样可以省去构造函数。

但是这时使用类创建的对象,它的属性值是默认值,需要另外再去修改属性值。

class Person {
   name: string = ''
   age: number = 0

   play() {
      console.log('play')
   }
}

const p = new Person()
p.name = 'kobe'
p.age = 18

p.play()

类的继承

子类可以继承父类的属性跟方法。

如下,现在我们定义了两个类:

class Student {
   name: string = ''  // 公共的属性
   age: number = 0    // 公共的属性
   homework: string = ''
   grade: number = 0

   study() {
      console.log('study')
   }
   eating() {    // 公共的方法
      console.log('eating')
   }
}

class Teacher {
   name: string = ''
   age: number = 0
   title: string = ''

   teaching() {
      console.log('teaching')
   }
   eating() {
      console.log('eating')
   }
}

这两个类里面有一些属性跟方法是它们都有的,这时我们可以把它们抽离成一个新的类,让这两个类来继承:

class Person {
   name: string = ''
   age: number = 0

   eating() {
      console.log('eating')
   }
}

class Student extends Person {
   homework: string = ''
   grade: number = 0

   study() {
      console.log('study')
   }
}

// class Teacher extends Person {
//    title: string = ''

//    teaching() {
//       console.log('teaching')
//    }
// }

const s = new Student()
s.name = 'kobe'
s.age = 18
s.homework = 'math'
s.grade = 59

属性的继承

由于类的属性的定义方式有两种:有构造函数没有构造函数(默认值),所以在类的属性的继承问题上就会出现四种情况

  • 父类跟子类都没有构造函数,代码如上;

  • 父类有构造函数而子类没有

这时在 new 的时候执行的是父类的构造函数,所以父类的属性直接传参,子类的属性等对象创建之后再去修改;

class Person {
   name: string
   age: number

   constructor(name: string, age: number) {
      this.name = name
      this.age = age
   }
}

class Student extends Person {
   homework: string = ''
   grade: number = 0
}

const s = new Student('kobe', 18)
s.homework = 'math'
s.grade = 59
  • 父类跟子类都有构造函数

在子类的构造函数中我们使用 super 函数来调用父类中的构造函数,把父类中的属性值传递过去。也就说父类一定要有构造函数。

在子类的构造函数中调用 super 函数时要注意,必须写在最前面,这样在后面才能够访问到 this

class Person {
   name: string
   age: number

   constructor(name: string, age: number) {
      this.name = name
      this.age = age
   }
}

class Student extends Person {
   homework: string
   grade: number

   constructor(name: string, age: number, homework: string, grade: number) {
      // super 函数必须写在前面,否则访问不了 this
      super(name, age)
      this.homework = homework
      this.grade = grade
   }
}

const s = new Student('kobe', 18, 'math', 59)
  • 父类没有构造函数而子类有

这种情况是不允许的,因为在子类的构造函数执行时,必须要使用 super 函数来调用父类的构造函数,也就是说父类必须有构造函数。

方法的继承

父类中的方法我们可以直接调用,如果在子类中存在同名的方法,会优先执行子类中的;如果想要都执行的话,可以在子类的方法中通过 super 调用来执行。

class Person {
   name: string = 'kobe'
   age: number = 18

   eating() {
      console.log('父类 eating')
   }
}

class Student extends Person {
   homework: string = 'math'
   grade: number = 59

   eating() {
      super.eating()
      console.log('子类 eating')
   }
}

const s = new Student()

s.eating()

super 来调用父类的方法不存在代码的先后顺序的。

多态

类的继承是多态的前提

多态的目的是为了写出更加具有通用性的代码。

示例如下,我们定义了两个类,希望同时执行这两个类里面的一些方法

class Dog {
  running() {
    console.log('running')
  }
}

class Bird {
  flying() {
    console.log('flying')
  }
}

这时我们可以这样做

class Dog {
  action() {
    console.log('running')
  }
}

class Bird {
  action() {
    console.log('flying')
  }
}

function makeAction(animals: (Dog | Bird)[]) {
  animals.forEach((animal) => {
    animal.action()
  })
}

makeAction([new Dog(), new Bird()])

但是这样写代码的通用性没有那么好,因此我们可以使用多态的方式。

// 我们可以定义一个父类 Animal,然后让这两个类继承这个父类
// 父类中必须有同名的方法,否则会报错
class Animal {
  action() {}
}

class Dog extends Animal {
  action() {
    console.log('running')
  }
}

class Bird extends Animal {
  action() {
    console.log('flying')
  }
}

// 这里我们声明参数的类型时就可以使用父类的类型
// 这样就算后面有新增的类型,只需要通过继承的方式,在调用时直接传参就可以了
function makeAction(animals: Animal[]) {
  animals.forEach((animal) => {
    animal.action()
  })
}

makeAction([new Dog(), new Bird()])

抽象类和抽象方法

上面介绍多态的例子会存在一些问题 —— 因为我们在声明参数的类型时使用的是父类,所以如果在调用时我们传入的是父类的实例的话,它就会直接调用父类中的方法。

makeAction([new Animal()])

但是由于父类只是我们抽象出来的类,一般是没有具体实现的,这时我们就要用到抽象类跟抽象方法。

abstract class Animal {
  abstract action()
}

这样我们也就无法创建父类的实例了。

类的修饰符

TypeScript 中类的属性和方法支持几种修饰符:publicprivateprotectedreadonly

访问器 getter/setter

私有属性我们是不能直接访问的,如果我们想要获取或者设置它们的值就可以使用访问器。

class Person {
  private _name: string

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

  get name() {
    return this._name
  }

  set name(value) {
    this._name = value
  }
}

const p = new Person('kobe')

console.log(p.name)
p.name = 'why'

类的静态成员

前面我们在类中定义的成员和方法都属于对象级别的, 在开发中, 我们有时候也需要定义类级别的成员和方法。

class Person {
  static test: string = 'math'
  
  static learn() {
    console.log('learn')
  }
}

类的成员可以直接通过类来获取

Person.test = 'abc'
console.log(Person.test)

Person.learn()

接口的使用

使用接口定义对象类型

在前面我们使用了类型别名来定义对象的类型。我们也可以使用接口来定义。

接口的声明通过 interface 关键字

interface IInfoType {
  name: string
  age: number
}

const info: IInfoType = {
  name: 'abc',
  age: 123,
}

我们在接口中也可以定义可选属性跟只读属性

interface IInfoType {
  readonly name: string
  age?: number
}

接口的继承

接口也是可以继承的,接口的继承跟类的继承类似,只是有一点不同,就是方法在继承时,父类是直接定义一个具体的方法,而接口是声明方法的类型。接口本质上就是 属性/方法 类型的集合。

interface IInfoType {
  readonly name: string
  age?: number
  action: () => void
}

接口的多继承

一个接口可以继承多个其他的接口,而类只能继承一个父类。

interface Dog {
  type: string

  running: () => void
}

interface Bird {
  food: string

  flying: () => void
}

interface Person extends Dog, Bird {
  name: string
}

const p: Person = {
  name: 'kobe',
  type: 'blue',
  food: 'woof',
	
  running() {},
  flying() {},
}

接口合并的其他方式

接口的多继承相当于把多个接口合并。除了使用继承的方式,我们还可以通过交叉类型来实现。

interface Dog {
  type: string

  running: () => void
}

interface Bird {
  food: string

  flying: () => void
}

type NewType = Dog & Bird

const p: NewType = {
  type: 'blue',
  food: 'woof',
	
  running() {},
  flying() {},
}

接口的实现

接口跟类型别名的区别

接口是对象的模板,可以看作是一种类型约定,使用了某个模板的对象,就拥有了指定的类型结构。

类型别名可以对所有类型定义别名。