点击勘误issues (opens new window),哪吒感谢大家的阅读

# 进阶

# 数组和元组

# 数组

和普通的变量一样,数组中的类型定义也有一定的规则:类型+方括号表示

// 只允许存储number类型
let numArray: number[] = [1, 2, 3]
// 只允许存储string类型
let strArray: string[] = ['1', '2', '3']

值得一提的是,以上案例还有一种泛型方式的写法:

// 只允许存储number类型
let numArray: Array<number> = [1, 2, 3]
// 只允许存储string类型
let strArray: Array<string> = ['1', '2', '3']

在数组中也可以使用联合类型:

// 只允许存储number和string类型的值
let tsArray: (number | string) [] = [1, '2', '3']

我们知道,在数组中不仅可以存储基础数据类型,还可以存储对象类型,如果需要存储对象类型,可以用如下方式进行定义:

// 只允许存储对象仅有name和age,且name为string类型,age为number类型的对象
let objArray: ({ name: string, age: number })[] = [
  { name: 'AAA', age: 23 }
]

为了更加方便的撰写代码,我们可以使用类型别名的方式来管理以上类型:

// 类型别名
type person = {
  name: string;
  age: number;
}
let objArray: person[] = [
  { name: 'AAA', age: 23 }
]

# 元组

对元组的理解是:一个数组如果知道它确定的长度,且每个位置的值的类型也是确定的,那么就可以把这样的数组称为元组。

// tuple数组只有2个元素,并且第一个元素类型为string,第二个元素类型为number
let tuple: [string, number] = ['AAA', 123]

当访问元组中已知位置的索引时,将得到其对应正确的值;当访问元组中未知位置的索引时,会报错。

let tuple: [string, number] = ['AAA', 123]
console.log(tuple[1]) // 123
console.log(tuple[2]) // 报错

# 枚举

枚举Enum类型用来表示取值限定在指定的范围,例如一周只能有七天,颜色只能有红、绿、蓝等。

enum colors  {
  red,
  green,
  blue
}
console.log(colors.red)   // 0
console.log(colors.green) // 1
console.log(colors.blue)  // 2

代码分析:我们定义一个colors的枚举类型,其取值只能是red、green、blue。我们可以在打印的内容发现,其输出值从0开始,依次累加1。这是枚举类型的默认行为,我们可以手动设置一个起始值:

enum colors  {
  red = 10,
  green,
  blue
}
console.log(colors.red)   // 10
console.log(colors.green) // 11
console.log(colors.blue)  // 12

在枚举类型中,我们不仅可以正向的获取值,还可以通过值反向获取枚举:

enum colors  {
  red = 10,
  green,
  blue
}
console.log(colors[10]) // red
console.log(colors[11]) // green
console.log(colors[12]) // blue

#

# 类的继承

在JavaScript中,通过extends关键字来实现子类继承父类,子类也可以通过super关键字来访问父类的属性或者方法。

class Person {
  name: string
  constructor (name: string) {
    this.name = name
  }
  sayHello () {
    console.log(`hello, ${this.name}`)
  }
}
class Teacher extends Person {
  constructor (name: string) {
    // 调用父类的构造函数
    super(name)
  }
  sayTeacherHello () {
    // 调用父类的方法
    return super.sayHello()
  }
}
let teacher = new Teacher('why')
teacher.sayHello()        // hello, why
teacher.sayTeacherHello() // hello, why

有一种关于类属性的简写方式,就是在类的构造函数中指明访问修饰符。

// 简写形式
class Person {
  constructor (public name: string) {}
}
// 等价于
class Person {
  name: string
  constructor (name: string) {
    this.name = name
  }
}

# 存取器

在class中,可以通过getter和setter来改变属性的读取和赋值行为。

class Person {
  // 私有属性,只能在类中进行访问
  private _name: string
  constructor (_name: string) {
    this._name = _name
  }
  get name () {
    return this._name
  }
  set name(name) {
    this._name = name
  }
}
let person = new Person('why')
console.log(person.name)  // why
person.name = 'AAA'
console.log(person.name)  // AAA

# 静态属性和静态方法

所谓静态属性和静态方法,就是只能通过类来进行访问,不能通过类的实例来进行访问。在众多设计模式中,有一种设计模式叫做单例设计模式,可以使用static静态方法来辅助我们完成单例设计模式。

class Person {
  private static _instance: Person
  private constructor () {}
  public static getInstance () {
    if (!this._instance) {
      this._instance = new Person()
    }
  }
}
const person1 = Person.getInstance()
const person2 = Person.getInstance()
console.log(person1 === person2) // true

# TypeScript类的访问修饰符

在以上的实例中,我们使用到了TypeScript中关于类的几种访问修饰符,它有三种:

  • public:公有的,在任何地方都可以访问到。
  • protected:受保护的,只能在类的内部及其类的子类内部使用。
  • private:私有的,只能在类的内部进行使用。
class Person {
  private age: number
  protected address: string
  public name: string
  constructor (age: number, address: string, name: string) {
    this.age = age
    this.address = address
    this.name = name
  }
}
class Teacher extends Person {
  sayHello () {
    console.log(`my addresss is ${this.address}`) // my address is 广东广州
    console.log(`my name is ${this.name}`)        // my name is why
    console.log(`my age is ${this.age}`)          // 编译报错
  }
}
const person = new Person(21, '广东广州', 'why')
console.log(person.name)    // why
console.log(person.age)     // 编译报错
console.log(person.address) // 编译报错

# 只读属性

可以使用readonly关键字来表示属性是只读的。

class Person {
  constructor (public readonly name: string) {}
}
let person = new Person('AAA')
console.log(person.name)  // AAA
person.name = 'BBB'       // 编译报错 

# 抽象类

在TypeScript中,可以使用abstract关键字来定义抽象类以及抽象类中的抽象方法,在使用抽象类的过程中,有几点需要注意:

  • 抽象类不能被实例化,只能被继承。
  • 抽象类中的抽象方法必须被子类实现。
  • 抽象类不能被实例化:
abstract class Animal {
  name: string
  constructor (name: string) {
    this.name = name
  }
}
class Person extends Animal{}
const person = new Person('why')
console.log(person.name)    // why
const animal = new Animal() // 编译报错

抽象类中的抽象方法必须被子类实现:

abstract class Animal {
  name: string
  constructor (name: string) {
    this.name = name
  }
  abstract eat (): void
}
class Person extends Animal{
  // 子类必须实现抽象类中的抽象方法
  eat () {
    console.log('person is eating')
  }
}
const person = new Person('why')
console.log(person.name)    // why
person.eat()                // person is eating

# 类和接口

# 类实现接口

一个类可以实现一个或者多个接口,用逗号分隔。

如果我们定义了一个接口,然后类去实现它,那么这个接口中的属性和方法,在类中必须全部都要存在,否则会编译报错。

interface Animal {
  age: number
  sayHello (): void
}

class Person implements Animal {
  age: number
  sayHello () {
    console.log(this.age)
  }
}

# 接口继承接口

在上面的案例中,我们使用到了类实现接口,其实一个接口还可以继承自另外一个接口。

interface Animal {
  age: number
  sayHello (): void
}
interface Person extends Animal {
  // Person接口继承了Animal,就拥有了Animal种所有的属性和方法
  name: string
}

class Person implements Person {
  age: number
  sayHello () {
    console.log(this.age)
  }
}

# 接口继承类

在有些语言中,接口一般而言是不能继承类的,但在TypeScript中是可以继承的,接口继承类以后,就拥有类中所有的属性和方法。

class Point {
  x: number
  y: number
  constructor (x: number, y: number) {
    this.x = x
    this.y = y
  }
}
interface Point3d extends Point {
  z: number
}
let point3d: Point3d = {
  x: 10,
  y: 10,
  z: 10
}
console.log(point3d)  // { x: 10, y: 10, z: 10 }

# 泛型

泛型generics是指在定义函数、接口和类的时候,不预先指定其具体类型,而在使用的时候再去指定的一种特性。

# 函数中的泛型

假设我们有如下一个函数,其中参数a和b接受的类型必须为相同的类型。

function join (a, b) {
  return `${a}${b}`
}

我们在没有了解到泛型之前,我们可以用联合类型来定义:

function join (a: number | string, b: number | string) {
  return `${a}${b}`
}

代码分析:在以上的例子中,我们仅仅只是规定了a和b参数必须是number类型或者string类型,但并没有办法来限制a和b必须是同一个类型。这个时候我们可以使用泛型来表示:

function join<T> (a: T, b: T): string {
  return `${a}${b}`
}
console.log(join(1, 2))     // 12    
console.log(join('1', '2')) // 12
console.log(join(1, '2'))   // 编译报错

注意:我们在调用join()函数并进行传参的时候,TypeScript会自动帮我们推断参数的类型,以上三行代码也可以像如下方式进行撰写:

console.log(join<number>(1, 2))     // 12    
console.log(join<string>('1', '2')) // 12
console.log(join<number>(1, '2'))   // 编译报错

泛型可以是多个的。

function join<T, P> (a: T, b: P): string {
  return `${a}${b}`
}
console.log(join(1, 2))     // 12    
console.log(join('1', '2')) // 12
console.log(join(1, '2'))   // 12

代码分析:在以上的案例中,join方法接受2个泛型类型,其中参数a:T,参数b:p,因此console.log(join(1, '2'))会正确被编译并输出12。

# 类中的泛型

泛型同样可以使用在类中。

class CreateClass<T> {
  zeroValue: T
  add: (x: T, y: T) => T
}
let createNumber = new CreateClass<number>()
createNumber.add = function (x, y) {
  return x + y
}
let createString = new CreateClass<string>()
createString.add = function (x, y) {
  return `${x}${y}`
}
console.log(createNumber.add(1, 2))     // 3
console.log(createString.add('1', '2')) // 12

在TypeScript@2.3+以后的版本,我们可以为泛型提供一个默认值。

class CreateClass<T = number> {
  zeroValue: T
  add: (x: T, y: T) => T
}
let createNumber = new CreateClass()
createNumber.add = function (x, y) {
  return x + y
}
let createString = new CreateClass<string>()
createString.add = function (x, y) {
  return `${x}${y}`
}
console.log(createNumber.add(1, 2))     // 3
console.log(createString.add('1', '2')) // 12

代码分析:在CreateClass类的定义部分,我们为泛型提供了一个默认值number,因此我们在实例createNumber初始化的时候就可以不用传递number了。

# 接口中的泛型

像在类中一样,泛型可以存在于接口中。

interface CreateArray {
  <T>(length: number, value: T): T[]
}
let createArrayFunc: CreateArray = function (length, value) {
  let result = []
  for (let index = 0; index < length; index++) {
    result[index] = value
  }
  return result
}
console.log(createArrayFunc(3, 'AAA'))  // ['AAA', 'AAA', 'AAA']
console.log(createArrayFunc(2, true))   // [true, true]

# 声明合并

如果定义了两个相同名字的函数、接口或类,那么它们会合并成一个类型。 声明合并,我们在上面已经有实例的案例了,那就是函数的重载。

function add (a: number, b: number): number;
function add (a: string, b: string): string;
function add (a: number | string, b: number | string): number | string {
  if (typeof a === 'number' && typeof b === 'number') {
    return a + b
  } else {
    return a + '' + b
  }
}
console.log(add(1, 2))      // 3
console.log(add('1', '2'))  // 12

当重复定义同一个接口时,会进行接口合并:

interface Person {
  name: string,
  address: string
}
interface Person {
  name: string,
  age: 23
}
// 相当于
interface Person {
  name: string,
  address: string,
  age: 23
}

当合并的属性类型不一致时,会报错。

interface Person {
  name: string,
  address: string
}
interface Person {
  // 报错,name类型冲突
  name: number,
  age: 23
}

# 命名空间

在我们以上所有案例中,我们编写的代码大多数是运行在Node环境下的,接下来我们来编写一些代码,让其在浏览器环境中运行。

首先我们需要创建如下的项目以及目录结构:

|-- TypeScript
|   |-- dist
|   |   |-- index.html
|   |-- src
|   |   |-- page.ts
|   |-- tsconfig.json

其中,tsconfig.json的配置如下:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",                
    "outDir": "./dist",
    "rootDir": "./src", 
    "strict": true,
    "esModuleInterop": true
  }
}

在配置完tsconfig.json以后,我们来撰写page.ts中的代码:

class Header {
  constructor () {
    let dom = document.createElement('div')
    dom.innerHTML = 'Header'
    document.body.append(dom)
  }
}
class Content {
  constructor () {
    let dom = document.createElement('div')
    dom.innerHTML = 'Content'
    document.body.append(dom)
  }
}
class Footer {
  constructor () {
    let dom = document.createElement('div')
    dom.innerHTML = 'Footer'
    document.body.append(dom)
  }
}
class Page {
  constructor () {
    new Header()
    new Content()
    new Footer()
  }
}

编写完以上代码后,我们运行如下命令:

# 编译src下的*.ts文件到dist目录下

$ tsc

随后我们在dist/index.html中引用我们刚刚编译的代码:

<script src="./page.js"></script>
<script>
  new Page()
</script>

当我们在浏览器中运行index.html文件后,我们可以在浏览器下正确的看到我们想要的输出内容。

当我们在打开page.js文件时,我们可以发现:

在全局作用域环境下,我们一次性引入了四个全局变量:Header、Content、Footer和Page。要解决这个问题,我们可以使用namespace命令空间:

// 使用命名空间包裹我们的代码并把Page类导出出去
namespace Home {
  class Header {
    constructor () {
      let dom = document.createElement('div')
      dom.innerHTML = 'Header'
      document.body.append(dom)
    }
  }
  class Content {
    constructor () {
      let dom = document.createElement('div')
      dom.innerHTML = 'Content'
      document.body.append(dom)
    }
  }
  class Footer {
    constructor () {
      let dom = document.createElement('div')
      dom.innerHTML = 'Footer'
      document.body.append(dom)
    }
  }
  export class Page {
    constructor () {
      new Header()
      new Content()
      new Footer()
    }
  }
}

随后,再次使用tsc命令重新编译代码,编译后的page.js如下:

再次修改index.html中的代码,我们依然能够得到跟前面示例代码一样的输出结果:

<script src="./page.js"></script>
<script>
  new Home.Page()
</script>
上次更新: 2022/1/13 下午1:16:36