常用运算符
非空类型断言 !.
用来跳过 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 中类的属性和方法支持几种修饰符:public、private、protected、readonly
访问器 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() {},
}
接口的实现
接口跟类型别名的区别
接口是对象的模板,可以看作是一种类型约定,使用了某个模板的对象,就拥有了指定的类型结构。
类型别名可以对所有类型定义别名。