TypeScript 基础语法入门

Vue3 基本全部使用 TypeScript 来进行重写,尽管你可能觉得要学的东西越来越多了,但是作为程序员,如果没有保持对新鲜事物的好奇和接纳,很可能两年后你就必须要加入外卖大军了!话不多说,一起走进 TypeScript 的世界吧!

学习资料


TypeScript 中文文档地址 | TypeScript 线上学习视频

什么是 TypeScript


进入官网,映入眼帘的第一句就给出了解释:TypeScriptJavaScript 类型的超集,它可以编译成纯 JavaScript。所谓超集意思就是:js 有的东西,我 ts 有,js 没有的,我 ts 还有。这样说可能有点糊,我们先来看一个栗子。

熟悉 js 的小伙伴都知道当我们声明了一个变量后,比如赋值了一个字符串,但是后续仍然可以将其再赋值成其它类型的数据。例如:

let a = '123' // a 是 string 类型
a = 123 // 将 a 改变成 number 类型
console.log(typeof a) // number

我们尝试用 ts 写上面这段代码

let b = 123
b = '123' // 编辑器提示错误:不能将类型 string 分配给类型 number 

其实如果用 ts 的规范来写上面这段代码应该下面这样的

let b: number = 123 // 规定了 b 是 number 类型,如果赋值给 string 就会直接提示报错
b = 456

但是如果我们把这段代码直接拿到浏览器中,就会报错,因为浏览器不认识 TypeScript,我们写的所有 ts 代码都需要编译成纯 js 代码然后才能被浏览器识别,我们可以在 TypeScript 官网练习 将上面代码复制到左边,右边就会自动生成与其对应的 js 代码。

继续学习前,我们来看看 TypeScript 相对于 JavaScript 带来了什么优势,为什么未来我们一定要学 TypeScript 呢?我们先用 JavaScript 来写一段代码:

function demo (data) {
  return data.x * 2 + data.y * 2
}
demo()

上面这段 js 代码咋一看好像没错,但是如果我们放到浏览器中运行就会发现报错信息 Cannot read property 'x' of undefined(无法读取未定义的属性 x) 。我们在调用 demo() 的时候没有传参,导致代码真正运行过程中报错,但是它不会再编辑器中给我们报错提示, 只有当代码运行了才会报错。

我们使用 ts 改造一下上述代码:

function tsDemo(data: { x: number, y: number }) {
  return data.x * 2 + data.y * 2
}
tsDemo() // 编辑器直接报错提醒:应有一个参数,但获得 0 个

假如我们随便传一个不符合规定的参数,编辑器也会直接给我们错误提醒:

tsDemo(10) // 类型 number 的参数不能赋值给类型 {x: number, y: number} 的参数

只有我们正确传入了对应的类型参数,编辑器才会让我们通过,并且在函数中调用 data 编辑器就会直接将其下面的 x、y 给我们提示出来:

tsDemo({ x: 10, y: 10 }) // ok
TypeScript 相对于 JavaScript 优势总结:

1、ts 编写代码就会提示你的代码是否编写正确,而不需要像 js 等到代码运行时才发现错误。
2、ts 有更好的编辑器语法提示,写起代码来更舒服。
3、类型的声明可以让我们直观看到我们代码里潜在的语义,代码可读性更好。

TypeScript 基础环境搭建

一般使用 npm 来安装,当然前提肯定需要你装了 node ,然后使用如下命令:

npm install -g typescript

这样我们就全局安装了 typescript ,此时我们怎么应该运行 ts 文件的代码呢?我们知道在 node 环境下运行 js 文件只需要使用 node demo.js 就行了,但是 ts 文件不能直接在浏览器和 node 环境下运行,所以我们需要先将 ts 转成 js 文件:

tsc demo.ts

然后我们就会发现文件夹下多了一个 demo.js 文件,这个文件就是由 demo.ts 编译过来的。 接下来我们使用 node demo.js 就可以运行了。但是如果我们每次都需要先转一道 js 然后在运行会显的十分繁琐,所以我们可以安装 ts-node ,命令行输入如下指令:

npm install -g ts-node

此时我们就可以直接使用如下指令直接输出 ts 文件的结果:

ts-node demo.ts
静态类型的深度理解

TypeScript 中如果我们做了如下定义

const num: number = 123

我们不仅仅是将常量 num 永久定义成了 number 类型,而且我们在使用常量 num 的时候还可以直接使用 number 类型下面的所有方法,编辑器都会给我们最友好的提示。又比如我们定义了一个对象类型:

interface Point {
  x: number;
  y: number;
}
const point: Point = {
  x: 3,
  y: 4
}
point // 编辑器直接就会有 x 和 y 的提示
point.z // 类型 Point 上不存在属性 z

如果我们看到一个变量是静态类型,不仅仅意味着这个变量类型不能修改,还意味着这个变量的属性和方法基本也就确定了,编辑器在使用静态类型时就会给我们很好的语法提示。

TypeScript 的使用


为了让程序有价值,我们需要能够处理最简单的数据单元:数字,字符串,结构体,布尔值等。 TypeScript 支持与 JavaScript 几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。

基础类型

日常开发中常见的基础类型一般有:

  • boolean
  • number
  • string
  • void
  • undefined
  • symbol
  • null
// boolean
let isDone: boolean = false

// number 注意 es6 还支持2进制和8进制
let age: number = 10
let binaryNumber: number = 0b1111

// string,注意es6新增的模版字符串也是没有问题的
let firstName: string = 'viking'
let message: string = `hello, ${firstName}`

// 奇葩兄弟二人组,undefined 和 null
let u: undefined = undefined
let n: null = null
// 注意 undefined 和 null 是所有类型的子类型。
// 也就是说 undefined 类型的变量,可以赋值给 number 类型的变量:
let num: number = undefined

在实际使用中,其实单独的 tsnullundefined 不是很常用,因为声明一个 null 或者 undefined 的变量没有意义,这种数据类型的声明也只是在 js 中使用,比如程序初始时,先声明一个 null 的变量,后续的代码逻辑中再改成其它数据类型。

如果我们在编程阶段还不清楚类型的变量到底会指定为哪一个类型,那应该怎么办呢?在这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any 类型来标记这些变量:

let notSure: any = 4
notSure = 'maybe a string'
notSure = true
notSure.myName
notSure.getName()

当然,如果我们知道一个变量可能只是 number 和 string 类型的其中一种,我们也可以不使用 any 而是用下面这种语法:

// num 可以是 number 类型也可以是 string 类型
let num: number | string = 123
num = '456'
类型注解 和 类型推断

类型注解指的是我们在定义变量的时候就会去告诉 ts 这个变量是什么类型:

let count: number = 123 // 指定 count 是 number 类型

类型推断是指当我们未使用类型注解时,ts 会自动去尝试分析变量的类型:

const firstName = 1
const lastName = 2
// 自动推断出 total 为 number 类型,因为它是 2 个数字的和
const total = firstNumber + lastNumber

所以如果 ts 能够自动分析变量类型,我们就什么也不需要做,但是如果 ts 无法分析变量类型的话,我们就要使用类型注解了。栗子如下:

function getTotal(firstNumber, lastNumber) {
  return firstNumber + lastNumber
}
const result = getTotal(1, 2)

此时 ts 就无法推断出 result 是什么类型,只为给它分配一个 any 类型,所以像这种时候,我们就应该使用类型注解:

function getTotal(firstNumber: number, lastNumber: number) {
  return firstNumber + lastNumber
}
const result = getTotal(1, 2)

此时 result 就会被推断为 number 类型,符合我们预期的结果。

所以在未来使用 ts 的过程中我们就是希望变量和属性能够类型固定,能够推断的就让它推断,不能推断的我们告诉它就行了。

函数类型

JavaScript 一样,TypeScript 函数可以创建有名字的函数和匿名函数。 你可以随意选择适合应用程序的方式,不论是定义一系列 API 函数还是只使用一次的函数。

通过下面的例子可以迅速回想起这两种 JavaScript 中的函数:

// 命名函数
function add(x, y) {
    return x + y;
}

// 匿名函数
let myAdd = function(x, y) { return x + y; };

我们首先来尝试为一个命名函数添加类型:

// 约定函数传入两个值都是 number 类型,且函数的返回值也为 number 类型
function add(x: number, y: number): number {
  return x + y
}
add(1, 2) // OK
add(1, 2, 3) // Error 应有 2 个参数,但获得 3 个
add(1, '2') // Error 类型 'string' 的参数不能赋值给类型 'number' 的参数

我们约定了 add() 的形参只能传入两个数字类型,并且它的返回值也是 nunmber 类型。你也许会奇怪,根据类型推断来说,我们不需要给函数的返回值加上类型注解,可以通过类型推断默认推断出两个数字的和肯定也是 number 类型。但是如果我们不小心在上述函数中写入了如下代码:

function add(x: number, y: number) {
  return x + y + ''
}
const result = add(1, 2) // result 变成了 string 类型,编辑器未报错提醒

此时我们不小心在函数结尾加了一个空字符串,结果导致不能推断出正确的类型,所以为了保证代码的严谨性,对于函数类型来说,我们一般在结尾处给其进行类型注解。

在函数的形参中,如果我们希望传递的第三个参数为可选参数,如果有就用,没有就不用,那么我们可以这样来写:

// ?: number 代表这是一个可选参数,有就用,没有就不用
function add(x: number, y: number, z?: number): number {
    if (typeof z === 'number') {
        return x + y + z
    } else {
        return x + y
    }
}
add(1, 2) // ok
add(1, 2, 3) // ok
add(1, 2, 3, 4) // error 应该有 2-3 个参数,但获得 4 个

如果我们希望这个命名函数没有返回值,那么我们就可以为这个函数给定 void 类型:

function add(x: number, y: number): void {
  return x + y // error 不能将类型 number 分配给类型 void
}
const result = add(1, 2)

日常开发中,我们经常会使用对象解构的方式进行传参,如果我们使用解构去传参,那么我们应该怎么样来定义对应的类型呢?你可能会这样写:

function add({ x: number, y: number }) { // error
  return x + y
}
const result = add({ x: 1, y: 2 })

编辑器直接给我们报错了,正确的写法应该如下:

function add({ x, y }: { x: number, y: number }): number {
  return x + y
}
const result = add({ x: 1, y: 2 })

命名函数其实在 ES6 之后我们基本很少写了,现在的开发中,你可能进场这样写一个函数:

const add = (x: number, y: number): number => {
    return x + y
}

此时聪明的你会发现,add 好像就直接是函数类型,我们试着在编辑器中把鼠标一如到 add 上面会发现它的类型为:const add:(x: number, y: number) => number,所以函数不仅输入输出有类型,它自己本身也是有类型的。

此时如果我们把 add 赋值给一个 string 类型就会报错

let add2: string = add // error 不能将函数类型分配给 string 类型

此时我们应该将 add2 定义为函数类型才能接收 add,那么问题来了,怎么给常量定义一个函数类型呢,我们先跟着编辑器的提示写:

let add2: (x: number, y: number) => number = add // ok 

感觉写着写着你会不会蒙圈,感觉定义类型就像在写一个箭头函数,这个时候我们要始终记得两点:

1、在 ts 中凡是在 : 后面都是在声明类型,和实际的代码逻辑没有任何关系,而 = 后面跟的是函数的具体实现,也就是函数体。
2、定义函数类型时,后面的 => 不是箭头函数,而是 ts 中声明函数类型返回值的方法,在上述栗子中它只是代表这个函数类型的返回值是一个 number 类型。

数组

TypeScriptJavaScript 一样可以操作数组元素。 有两种方式可以定义数组。 第一种,可以在元素类型后面接上 [],表示由此类型元素组成的一个数组:

//最简单的方法是使用「类型 + 方括号」来表示数组
let numberArr: number[] = [1, 2, 4]
let stringArr: string[] = ['1', '2', '3']

如果数组中是一个对象类型,那么我们应该如下定义:

let objArr: { name: string, age: number }[] = [{
  name: 'cc',
  age: 18
}]

当然,因为我们约束了类型中只能有 nameage 两个字段,所以此时 objArr 就被限定了只能有 nameage 两个属性,如果我们随意添加一个新的属性,编辑器就会给出报错提醒。如果我们想在类型约束中给出更多的字段,我们肯定不能用上面那种写法,此时我们可以使用类型别名 type,具体怎么用呢?

// 使用 type 定义一个 User 类型
type User = {
  name: string;
  age: number;
  sex: string
}
// 使用 User 类型
let objArr: User[] = [{
  name: 'cc',
  age: 18,
  sex: '男'
}]

当然前面说了定义数组有两种方式,其实第二种方式就是是使用数组泛型,Array<元素类型>。当然泛型算是 ts 中比较难懂的一个概念,我们这里先罗列出来用法,后面说到泛型在来具体讲解:

// 使用数组泛型定义变量的类型
let list: Array = [1, 2, 3]
元组

元组和数组很像,但是元组更具象,它表示一个已知元素数量和类型的数组,各元素的类型不必相同。我们先写个栗子:

// 这表示一个 [] 里面只能是 string 类型或者 number 类型
const arr: (string | number)[] = ['aa', 'bb', 18]

但是上述栗子中如果我们希望这个数组只有 3 个值,并且这 3 个值的类型就是 string string number,并且这三个值的类型顺序也不能颠倒哦。那么我们就可以使用元组:

const arr: [string, string, number] = ['aa', 'bb', 18] // ok
const arr: [string, string, number] = ['aa', 18, 'bb'] // error

元素的应用场景,一般向 csv 格式中的数据都是知道表头是姓名、性别、年龄等信息就可以使用元组:

const teacherList: [string, string, number][] = [
  ['cc', 'male', 18],
  ['wc', 'female', 28]
]
interface 接口

这应该是最重要的一个基础语法了,我们项目中用的最多的就是 interface,我们先通过一个小栗子来看看它到底怎么用:

const getPersonName = (person: { name: string }): void => {
  console.log(person.name)
}
const setPersonName = (person: { name: string }, name: string): void => {
  person.name = name
}
const person: { name: string } = {
  name: 'cc'
}
getPersonName(person)
setPersonName(person, 'wc')

上述代码中我们定义了两个函数和一个常量,我们发现有一个对象类型 {name: string} 我们写了 3 遍,那么我们能不能把这个对象类型抽离出来公用呢,聪明的你肯定想到了使用 类型别名 type,那么我们使用 type 改造一下上面的代码:

// 使用类型别名 type
type Person = {
  name: string
}
const getPersonName = (person: Person): void => {
  console.log(person.name)
}
const setPersonName = (person: Person, name: string): void => {
  person.name = name

const person: Person = {
  name: 'cc'
}

除了 type 我们是否还有其它方法改造这段代码呢?接下来就要介绍 interface 了,那我们就使用 interface 继续改造:

interface Person {
  name: string
}
const getPersonName = (person: Person): void => {
  console.log(person.name)
}
const setPersonName = (person: Person, name: string): void => {
  person.name = name
}
const person: Person = {
  name: 'cc'
}

聪明的你应该发现貌似 interfacetype 好像用法都差不多,但是在 ts 中通用性的规范是如果能用接口去表述一些类型的话就要优先去使用接口,实在不行我们才使用类型别名 type

接下来我们在 interface 中使用 ? 可选参数和 readonly 只读参数:

interface Person {
  readonly name: string, // name 只能读不能修改
  age?: number // age 可有可无
}

const setPersonName = (person: Person, name: string): void => {
  person.name = name // error 无法分配到 name,因为它是只读属性
}
const person: Person = {
  name: 'cc'
}

但是我们可能会遇到这种情况,就是我们定义的常量 person 虽然规定了 Person 类型,但是如果这个常量还有性别、身高等属性,而我们的接口类型定义中又不知道未来还会有哪些属性,怎么样防止 ts 效验错误呢?还是看栗子:

interface Person {
  name: string,
  age?: number
}
const person: Person = { // error Person 类型中未指定 sex 和 height
  name: 'cc',
  sex: 'male',
  height: 180
}

此时我们可以在 Person 类型中使用如下定义:

interface Person {
  name: string,
  age?: number,
  [propName: string]: any
}

此时错误提示就被关闭了,它代表 Person 这个类型除了 nameage 以外还可以有其它的属性,属性的名字是一个字符串类型就行,属性的值可以是任意类型。当然一个接口里面除了有属性还可以有方法:

interface Person {
  name: string,
  age?: number,
  [propName: string]: any,
  say(): string // 必须有 say 方法,返回一个 string 类型
}
const person: Person = {
  name: 'cc',
  sex: 'male',
  height: 180,
  say() {
    return '111'
  }
}
implements

在面向对象中,一个类只能继承另外一个类,有时候不同类之间有一些共同的特性,而一个类想使用一个接口去做类的一些属性约束时就要使用 implements 关键字,这时候我们就可以把这些特性提取成接口然后用 implements 关键词来实现,这样就可以提高灵活性。举个栗子:

// car 和 cellphone 两个类有相同的特性,但是没有公共父类,这时候可以把相同特性抽离成接口
interface Radio {
  switchRadio(trigger: boolean): void
}
class Car implements Radio {
  switchRadio(trigger: boolean) {}
}
class Cellphone implements Radio {
  switchRadio(trigger: boolean) {}  
}

interface Battery {
  checkBatteryStatus(): void
}
// 要实现多个接口,我们只需要中间用逗号隔开即可
class Cellphone implements Radio, Battery {
  switchRadio() {
  }
  checkBatteryStatus() {}
}
接口继承

接口之间也可以实现相互继承,这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。

interface Shape {
  color: string;
}
interface PenStroke {
  penWidth: number;
}
// Square 继承了 Shape 和 PenStroke
interface Square extends Shape, PenStroke {
  sideLength: number;
}
// 既然 demo 使用了 Square 那么就必须有 color、penWidth、sideLength 属性
const demo: Square = {
  color: '#ccc',
  penWidth: 20,
  sideLength: 10
}

当然接口不仅可以定义成一个对象,还可以定义成一个函数:

interface Test {
  // 定义一个函数类型接收两个 number 类型的形参同时返回 number 类型
  (firstNumber: number, lastNumber: number): number;
}
const total: Test = (firstNumber: number, lastNumber: number): number => {
  return firstNumber + lastNumber
}
const result1 = total(1, 2)
class 类 -- TypeScript

首先我们来定义一个类:

class Person {
  name: string;
  constructor(message: string) {
    this.name = message
  }
  getName() {
    return this.name;
  }
}
const person = new Person('cc')
console.log(person.getName()) // cc

我们再次定义一个 Teacher 类来继承父类 Person

// 使用 extends 构建继承关系
class Teacher extends Person {
  getTeacherName() {
    return 'Teacher'
  }
}
const teacher = new Teacher('dd')
console.log(teacher.getName()) // dd
console.log(teacher.getTeacherName()) // Teacher

使用继承之后,子类的实例可以直接访问父类上的方法。如果子类上面也有 getName 方法,那么就会优先执行子类上的 getName(),如下栗子:

class Teacher extends Person {
  getTeacherName() {
    return 'Teacher'
  }
  getName() {
    return 'cc'
  }
}
const teacher = new Teacher('dd')
console.log(teacher.getName()) // cc

假如我们希望在子类重写的方法中再次调用父类的这个方法,就可以使用 super 关键字,如下栗子:

class Teacher extends Person {
  getTeacherName() {
    return 'Teacher'
  }
  getName() {
    return super.getName() + 'ee'
  }
}

const teacher = new Teacher('dd')
console.log(teacher.getName()) // ddee

当子类的方法将父类的方法覆盖之后,如果我们仍需调用父类的这个方法,就可以通过 super 进行调用。

公共,私有与受保护的修饰符

了解了类的基本用法之后,我们来认识类中的访问类型,类中的访问类型基本分为以下三种:

  • Public:修饰的属性或方法是共有的,不仅可以自己访问,且实例对象和子类都能访问,如不定义访问类型默认类型就是 Publice
  • Private:修饰的属性或方法是私有的,只有自己可以访问,实例对象和子类都不能访问
  • Protected:修饰的属性或方法是受保护的,只有自己和自己的子类中可以访问,实例对象无法访问

通过栗子我们来瞅瞅 Private 的用法:

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name
  }
  private run() {
    return `${this.name} is running`
  }
  eat() {
    return this.run()
  }
}
const snake = new Animal('snake')
console.log(snake.run()) // error run 为私有属性,只能在 Animal 类中使用
console.log(snake.eat()) // 所以我们又在其中定义 eat 方法,通过 eat 调用 run

我们再来瞅瞅 Protected 的用法,其实也很简单,跟着栗子看看就行:

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name
  }
  protected eat() {
    return this.name + ' eat'
  }
}
const snake = new Animal('snake')
console.log(snake.eat()) // error 只能在类和子类中访问,实例对象无法访问

class Dog extends Animal {
  myEat() {
    return super.eat() // 子类内部可以调用 eat 方法
  }
}
const dog = new Dog('dog')
console.log(dog.myEat())
构造器 constructor

接下来认识一下构造器 constructor ,其实 ts 中的类基本和 js es6 中的类用法相同,constructor 方法是类的默认方法,通过 new 命令生成对象实例时,自动调用该方法。一个类必须有 constructor 方法,如果没有显式定义,一个空的 constructor 方法会被默认添加。

class Person {
  name: string;
  constructor(name: string) {
    this.name = name
  }
}
const person = new Person('cc')
console.log(person.name)

constructor 接收的形参就是实例化传过来的值,然后我们通过 this.name 将其赋值给类中的 name 属性,那么我们能不能将 constructor 接收的形参直接初始化为类中的全局变量呢,我们只需要这样写就行了:

class Person {
  constructor(public name: string) {
  }
}
const person = new Person('cc')
console.log(person.name)

如果父类有构造器,而子类也有自己的构造器时编辑器就会报错,代码如下:

class Teacher extends Person {
  constructor() {} // 报错
  sayHi() {
    console.log(this.name)
  }
}

子类中声明构造器的时候,必须要调用 super() 去把父类的构造器也调用一下,当然这里我们只通过 super() 调用父类的构造器也不行,因为父类的构造器中要求我们传入一个 name,所以我们子类的 super() 中也要加入传参才行。栗子如下:

class Person {
  constructor(public name: string) {
  }
}

class Teacher extends Person {
  constructor(public age: number) {
    super('zz');
  }
  sayHi() {
    console.log(this.name)
  }
}
const person = new Person('cc')
const teacher = new Teacher(18)
console.log(teacher.name) // zz
console.log(teacher.age) // 18

当然,如果我们父类的 constructor 没有接收任何参数,子类的构造器也必须调用 super() 只是此时可以不用传递对应的参数:

class Person {
  constructor() {
  }
}
class Teacher extends Person {
  constructor(public age: number) {
    super()
  }
}
存储器 (getters / setters)

此时你有没有一个疑问,privateprotected 这些访问类型存在的意义是什么呢?或者说我们在什么地方会用到呢?其实 privateprotected 一般结合着存储器来用,好吧?前面那个我都没弄明白,怎么又突然来一个存储器的概念?TypeScript 支持通过 getters/setters 来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。我们还是用栗子来说话吧:

class Person {
  constructor(private _name: string) { }
  get name() {
    return this._name + ' wc'
  }
}
const person = new Person('cc')
console.log(person.name) // 'cc wc'

Person 类中我们有一个私有属性 _name (一般私有属性的定义我们都会在属性前面加上 _ 表示这是一个私有属性),我们不希望外界能直接访问到这个私有属性,所以此时我们可以通过 getters 像外曝光这个数据,然后实例对象可以直接访问到,使用 getters 之后此时实例就不是函数调用的方式访问,而是直接通过属性调用方式访问。在 get 的过程中,我们可以对私密数据进行加密处理,不让外界猜到基础数据的值。

我们想重新给这个私有属性赋值,那么我们就可以使用 setters ,在 setters 中我们可以解析新设置的值,让私有属性的值等于解析过后的值:

class Person {
  constructor(private _name: string) { }
  get name() {
    return this._name
  }
  set name(name: string) {
    const realName = name.split(' ')[0]
    this._name = realName
  }
}
const person = new Person('cc')
person.name = 'cc wc'
console.log(person.name) // cc
静态属性

到目前为止,我们只讨论了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。 我们也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。

我们可以给类的 constructor 上设置一个 private,因为每个类实例之后都会自动执行 constructor() ,而现在它上面设置了私有属性,那么这个类就不能进行实例化了,但是我们却可以通过 static 关键字直接访问这个类上的方法,而不是实例上的方法,举个栗子:

class Person {
  private constructor() { }
  static eat() { // 使用 static 关键字将方法直接定义在类上
    console.log('eat...')
  }
}
const person = new Person('cc') // 无法实例化
console.log(Person.eat()) // 直接访问类上的方法

延伸思考:是否可以在类上实现单例模式呢?

class Person {
  private constructor(public name: string) { }
  // 定义一个私有属性 instance 将其类型设置为 Person
  private static instance: Person
  static getInstance() {
    if (!this.instance) {
      this.instance = new Person('cc')
    }
    return this.instance
  }
}
const person1 = Person.getInstance()
const person2 = Person.getInstance()
console.log(person1.name) // cc
console.log(person2.name) // cc
抽象类

抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。 abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法。这是官网给出的解释,看这句话我们大概能知道抽象类前面都要加上 abstract 关键字,然后就是它不能直接实例化。

这里我们用栗子来说明,假如有多个图像类,它们都有计算面积的方法:

// 3 个图形类应该都有一个公有的方法,比如说计算面积的方法
class Circle {
  getArea() { }
}
class Square {
  getArea() { }
}
class Triangle {
  getArea() { }
}

但是这三个图形计算面积的方法确实不一样的,他们并不能完全抽离成一个方法,此时我们就可以使用抽象类。抽象类中建立一个公用的抽象方法,抽象方法不负责具体的实现,只是做一个定义。如下栗子:

// 建一个抽象图形类
abstract class Geom {
  width: number;
  // 建一个抽象的图形面积实现方法,但是具体实现不能在抽象方法里写
  abstract getArea(): number
}
new Geom() // 报错,不能直接实例化

然后我们在图形类出继承这个抽象类:

class Circle extends Geom {
  // 此时类中必须要有 getArea() 并且给其一个返回值,不然就会报错
  getArea() {
    return 123
  }
}
const circle = new Circle()
circle.width = 100
circle.getArea()

如果我们实例化这个 Circle 类,就会发现这个实例上有 width 属性和 getArea() 可以直接调用,有没有感觉它和 extends 还有接口中的 implements 傻傻分不清,感觉功能都好相似,当然具体的功能差异我们在后面继续来讲。

突然发现这篇笔记居然已经写了这么长了,泛型、枚举等一些进阶语法还没开始写。不过基础语法就先写到这里吧,后面在写进阶篇,文章整理仅供学习参考,如有错误,欢迎指正!

你可能感兴趣的:(TypeScript 基础语法入门)