TypeScript入门教程

1. TypeScript简介和起步

学习源:up主'技术胖'

1.1 简介

TypeScript 是由微软公司在 2012 年正式发布,现在也有 8 年的不断更新和维护了,TypeScript 的成长速度是非常快的,现在已经变成了前端必会的一门技能。TypeScript 其实就是 JavaScript 的超集,也就是说 TypeScript 是建立在 JavaScript 之上的,最后都会转变成 JavaScript。

1.2 起步

  1. 全局安装ts
    你要使用 TypeScript 先要在你的系统中全局安装一下TypeScript,这里你可以直接在 VSCode 中进行安装,安装命令可以使用 npm 也可以使用 yarn
    npm i typescript -g

yarn global add typescript

  1. ts-node 的安装和使用
    为了方便直接运行ts文件,可以安装ts-node:

npm i ts-node -g

之后就可以使用ts-node demo.ts来运行ts文件

2. 静态类型定义

TypeScript 的一个最主要特点就是可以定义静态类型,英文是 Static Typing。那到底是什么意思呢?你可以简单的理解“静态类型”为,就是你一旦定义了该类型,就不可以再改变了。

2.1 基础静态类型

基础静态类型非常简单,只要在声明变量的后边加一个:号,然后加上对应的类型哦。比如下面的代码,就是声明了一个数字类型的变量,叫做count;
这样定义后count这个变量在程序中就永远都是数字类型,不可以改变了。比如我们这时候给count复制一个字符串,它就报错了。

let count: number = 123

let num:number
num = 45

类似这样常用的基础类型还有: null,undefined,symbol,boolean,void

2.2 对象静态类型

2.2.1 最简单的对象类型

const animal: {
  name: string
  age: number
} = {
  name: 'dog',
  age: 3
}

2.2.2 数组

movies必须是一个数组,数组里的内容必须为string类型

const movies: string[] = ['海上钢琴师', '集结号', '复联']

2.2.3 类

用类的形式,来定义变量

class Song {}
const ChineseSong: Song = new Song()

2.2.4 函数

返回值类型为string

const fn: () => string = () => {
  return '我是字符串'
  // return 10  // 报错
}

3. 类型注解和类型推断

3.1 类型注解

意思是显示的告诉代码,我们的count变量就是一个数字类型,这就叫做类型注解

let count: number;
count = 123;

3.2 类型推断

let countInference = 123;

这时候我并没有显示的告诉你变量countInference是一个数字类型,但是如果你把鼠标放到变量上时,你会发现 TypeScript 自动把变量注释为了number(数字)类型,也就是说它是有某种推断能力的,通过你的代码 TS 会自动的去尝试分析变量的类型。

工作使用问题(潜规则):

  • 如果ts能够自动分析变量类型,我们就什么也不需要做了;如:
const one = 1;
const two = 2;
const three = one + two;
  • 如果ts无法分析变量类型的话,我们就需要使用类型推断;如:
function getTotal(one, two) {
  return one + two;
}

const total = getTotal(1, 2);

这种形式,就需要用到类型注释了,因为这里的one和two会显示为any类型。这时候如果你传字符串,你的业务逻辑就是错误的,所以你必须加一个类型注解,把上面的代码写成下面的样子。

function getTotal(one: number, two: number) {
  return one + two;
}

const total = getTotal(1, 2)

这里有的一个问题是,为什么total这个变量不需要加类型注解,因为当one和two两个变量加上注解后,TypeScript 就可以自动通过类型推断,分析出变量的类型。

TypeScript 也可以推断出对象中属性的类型

const man = {
  name: "坤哥",
  age: 18,
}

写完后你把鼠标放在XiaoJieJie对象上面,就会提示出他里边的属性:

const man: {
    name: string;
    age: number;
}

这表明 TypeScript 也分析出了对象的属性的类型。

在写 TypeScript 代码的一个重要宗旨就是每个变量,每个对象的属性类型都应该是固定的,如果你推断就让它推断,推断不出来的时候你要进行注释。

4. 函数参数和返回类型定义

4.1 简单类型定义

上节写过这么一个函数:

function getTotal(one: number, two: number) {
  return one + two;
}

const total = getTotal(1, 2)

这个代码其实是有一个小坑的,就是我们并没有定义getTotal的返回值类型,虽然TypeScript可以自己推断出返回值是number类型。 但是如果这时候我们的代码写错了,比如写程了下面这个样子:

function getTotal(one: number, two: number) {
  return one + two + "";
}

const total = getTotal(1, 2)

这时候total的值就不是number类型了,但是不会报错

可以直接给total一个类型注解:

const total: number = getTotal(1, 2)

这样写虽然可以让编辑器报错,但是这还不是很高明的算法,因为你没有找到错误的根本,这时错误的根本是getTotal()函数的错误,所以合适的做法是给函数的返回值加上类型注解,代码如下:

function getTotal(one: number, two: number): number {
  return one + two;
}

const total = getTotal(1, 2)

4.2 函数无返回值

有时候函数是没有返回值的,比如现在定义一个sayHello的函数,这个函数只是简单的terminal打印,并没有返回值。没有返回值的函数,我们就可以给他一个类型注解void,代表没有任何返回值。

function sayHello(): void {
  console.log("hello world")
}

如果这样定义后,你再加入任何返回值,程序都会报错。

4.3 never返回类型

如果一个函数是永远也执行不完的,就可以定义返回值为never,那什么样的函数是永远也执行不完的那?我们先来写一个这样的函数(比如执行的时候,抛出了异常,这时候就无法执行完了)

function errorFunction(): never {
  throw new Error()
  console.log("Hello World")
}

还有一种是死循环,这样也运行不完,比如下面的代码:

function forNever(): never {
  while (true) {}
  console.log("Hello mom")
}

4.4 函数参数为对象(解构)时

当一个函数的参数是对象时,我们如何定义参数对象的属性类型?先写个一般javaScript的写法:

function add({ one, two }) {
  return one + two
}

const total = add({ one: 1, two: 2 })

在浏览器中你会看到直接报错了,意思是total有可能会是任何类型,那我们要如何给这样的参数加类型注解呢?正确的写法:

function add({ one, two }: { one: number; two: number }) {
  return one + two
}

const total = add({ one: 1, two: 2 })

5. 数组类型的定义

5.1 一般数组类型

现在我们可以定义一个最简单的数组类型,比如就是数字类型,那么就可以这么写:

const numberArr = [1, 2, 3]

这时候你把鼠标放在numberArr上面可以看出,这个数组的类型就是 number 类型。这是 TypeScript 通过类型推断自己推断出来的。如果你要写类型注解,可以写成下面的形式,之前有写过:

const numberArr: number[] = [1, 2, 3]

string/boolean/undefined等一般数组类型同理

数组中含有多种类型,只要加个(),然后在里边加上|就可以了:

const arr: (number | string)[] = [1, 'string', 2]

5.2 数组中对象类型

数组包含对象:

const objArr: { name: string; age: number }[] = [
  { name: '张三', age: 45 },
  { name: '李四', age: 19 },
  { name: '坤坤', age: 24 }
]

这种形式看起来比较麻烦,而且如果有同样类型的数组,写代码也比较麻烦,TypeScript 为我们准备了一个概念,叫做类型别名(type alias)。比如刚才的代码,就可以定义一个类型别名,定义别名的时候要以type关键字开始。现在定义一个Lady的别名:

type Person = { name: string; age: number }
const objArr: Person[] = [
  { name: '张三', age: 45 },
  { name: '李四', age: 19 },
  { name: '坤坤', age: 24 }
]

5.3 元组

TypeScript 中提供了元组的概念,这个概念是JavaScript中没有的。其实元组在开发中并不常用,一般只在数据源是CVS这种文件的时候,会使用元组。其实你可以把元组看成数组的一个加强,它可以更好的控制或者说规范里边的类型。

我们先来看一个数组和这个数组注解的缺点,比如我们有一个person数组,数组中有姓名、职业和年龄,代码如下:

const person = ['李华', '学生', 20]

这时候把鼠标放到person变量上面,可以看出推断出来的类型。我们就用类型注解的形式给他作一个注解,代码如下:

const person: (string | number)[] = ['李华', '学生', 20]

但是这并不能很好的限制,比如我们把代码改成下面的样子,TypeScript依然不会报错:

const person: (string | number)[] = ['李华', 20, '学生']

我们只是简单的把数组中的位置调换了一下,但是TypeScript并不能发现问题,这时候我们需要一个更强大的类型,来解决这个问题,这就是元组。

元组和数组类似,但是类型注解时会不一样:

const person: [string, string, number] = ['李华', '学生', 20]

元组的使用
工作中不经常使用元组,因为如果要使用元组,完全可以使用对象的形式来代替,但是如果你维护老系统,你会发现有一种数据源时CSV,这种文件提供的就是用逗号隔开的,如果要严谨的编程就需要用到元组了。例如我们有这样一组由CSV提供的(注意这里只是模拟数据):

'李华', '学生', 20
'陈晨', '老师', 27
'张瓜呱', '砖家', 50

如果数据源得到的数据时这样的,你就可以使用元组了:

const persons: [string, string, number][] = [
  ['李华', '学生', 20],
  ['陈晨', '老师', 27],
  ['张瓜呱', '砖家', 50]
]

6. interface 接口

6.1 起步

基本使用如下:

interface Person {
  name: string
  age: number
  gender: string
  height?: number // ?表示该属性可有可无
}

const jack: Person = {
  name: 'jack',
  age: 10,
  gender: 'man'
}

const lucy: Person = {
  name: 'jack',
  age: 10,
  gender: 'woman',
  height: 166
}

**接口和类型别名的区别:**这两个语法和用处好像一样,确实用起来基本一样,但是也有少许的不同。类型别名可以直接给类型,比如string,而接口必须代表对象。

比如我们的类型别名可以写出下面的代码:

type user = string

但是接口就不能这样写,它必须代表的是一个对象,也就是说,你初始化user1的时候,必须写出下面的形式.

interface user2 {
  name: string
  age: number
}

6.2 细节及技巧

6.2.1 使用[propName: string]: any表示还可以增添任意类型属性:

interface Person {
  name: string
  age: number
  gender: string
  height?: number
  [propName: string]: any // 表示还可以增添任意类型属性
}

const jack: Person = {
  name: 'jack',
  age: 10,
  gender: 'man',
  weight:70// 新增添的其他属性
}

6.2.2 sayHello: string表示必须要有一个返回值为string的sayHello方法:

interface Person {
  name: string
  age: number
  gender: string
  height?: number 
  [propName: string]: any 
  sayHello(): string // 方法返回值为string
}

const jack: Person = {
  name: 'jack',
  age: 10,
  gender: 'man',
  weight: 70,
  sayHello() {
    return '你好'// 返回string
  }
}

假如方法没有返回值,则为void

6.2.3 接口和类的约束,可使用 implements

class Man implements Person {
  name = '废物'
  age = 48
  gender = '女'
  sayHello() {
    return '你好啊'
  }
}

const tom = new Man()

6.2.4 接口间的继承,可使用 extends

interface Person {
  name: string
  age: number
  gender: string
  height?: number
  [propName: string]: any
  sayHello(): string // 函数返回值为string
}

interface Teacher extends Person {
  subject: string// teacher特有的属性
}

const cuiHua: Teacher = {
  name: '翠花',
  age: 41,
  gender: '女',
  subject: '语文',// teacher特有的属性
  sayHello() {
    return '你好啊'
  }
}

7. TS中类的概念和使用

7.1 类的访问类型

  • public:公共的属性或方法,允许在类的内部和外部被调用
  • private: 只允许再类的内部被调用,外部不允许调用
  • protected: 允许在类内及继承的子类中使用

例子:

class Student {
  private name: string = '坤坤'// 私有
  protected age: number = 11// 保护
  public sayHi() {// 公共
    console.log(`hello,我是${this.name},今年${this.age}岁了`)
  }
}

class GradeOne extends Student {
  public practiceTime: number = this.age - 8.5// 通过继承可以访问保护的属性
  public fn() {
    console.log(this.name)// 报错,不可以访问Student中的私有属性
  }
}

const ikun = new Student()
ikun.sayHi() // 可以访问
console.log(ikun.name) // 报错
console.log(ikun.age) // 报错

const ikun2 = new GradeOne()
ikun.sayHi() // 可以访问
console.log(ikun2.name) // 报错
console.log(ikun2.age) // 报错
console.log(ikun2.practiceTime) // 可以访问

7.2 类的构造函数

构造函数就是在类被初始化的时候,自动执行的一个方法。

最常规和容易理解的写法:

class Animal {
  public name: string
  constructor(name: string) {
    this.name = name
  }
}

const dog = new Animal('狗')

下面写法就相当于你定义了一个name,然后在构造函数里进行了赋值,这是一种简化的语法,在工作中我们使用这种语法的时候会更多一些:

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

const dog = new Animal('狗')

类继承中的构造器写法:

在子类中使用构造函数需要用super()调用父类的构造函数:

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

class Bird extends Animal {
  constructor(public name: string, public age: number) {
    super(name)
  }
}

const parrot = new Bird('鹦鹉', 2)// { name: '鹦鹉', age: 2 }

在子类里写构造函数时,必须用super()调用父类的构造函数,如果需要传值,也必须进行传值操作。就是是父类没有构造函数,子类也要使用super()进行调用,否则就会报错:

class Animal {}

class Bird extends Animal {
  constructor(public age: number) {
    super()
  }
}

const parrot = new Bird(2)// { age: 2 }

7.3 只读属性 readonly

只读属性的属性值不可修改,否则报错

class Animal {
  constructor(public readonly name: string) {}
}
const dog = new Animal('狗')
console.log(dog.name)
dog.name = '猫'// 报错,无法分配到 "name" ,因为它是只读属性

7.4 抽象类

  • 关键字abstract
  • 抽象类不允许被实例化,抽象类的存在只为了向子类服务
  • 抽象类中包含抽象属性/方法,和普通属性/方法
  • 被抽象的属性/方法不允许拥有具体的内容
abstract class Animal {
  abstract name: string // 抽象一个name属性,但是name属性不允许有值,也不允许被 constructor 赋值
  abstract eat(): void // 抽象一个方法,方法不允许有内容,只允许标注返回值类型
}

class Dog extends Animal {
  // 方法属性具体化,只能这样赋值,不允许用constructor
  name: string = '狗'
  eat(): void {
    console.log('狗吃骨头')
  }

  constructor(public gender: string) {
    super() // 此处super中也不允许有父类的抽象属性
  }
}

const dog = new Dog('公')
dog.eat()// 狗吃骨头
console.log(dog)// { gender: '公', name: '狗' }

8. TS配置文件

8.1 起步

在需要编译的ts文件夹打开终端,输入tsc -init生成tsconfig.json文件

在tsconfig.json的compilerOptions上方加上如下配置项,表示要编译包含的文件:

"include": ["02.ts"]

或者使用exclude表示不包含的文件:

"exclude": ["02.ts"]

完成后在终端输入tsc就可以根据配置项编译生成js文件

tsc fileName 是没办法遵循tsconfig.js文件的,但是ts-node遵循

8.2 常用编译配置项compilerOptions

  1. removeComments 为true则ts编译成js时会将所有注释删除

  2. strict 为true则启用所有严格的类型检查选项,它下方的所有配置项都将开启为true;想要更改下方的配置项需要置为false

    • noImplicitAny 为true则为隐含的’any’类型的表达式和声明启用错误报告
    • strictNullChecks 为true则不允许null值
    • noUnusedLocals 为true则局部变量定义未使用时报错
  3. rootDir 指定源文件中的根文件夹

  4. outDir 为所有编译的文件指定一个输出文件夹

  5. sourceMap 编译后的文件和源文件的映射关系,编译后的代码报错后可以通过错误信息定位到源文件对应的行数

更多编译配置项详解:https://www.tslang.cn/docs/handbook/compiler-options.html

9. 联合类型和类型保护

9.1 联合类型

所谓联合类型,可以认为一个变量可能有两种或两种以上的类型。

比如下面这个参数person的类型可能为Man或Woman

interface Man {
  isMan: boolean
  smoke: () => {}
}

interface Woman {
  isMan: boolean
  makeup: () => {}
}

const judge = (person: Man | Woman) => {}

但这时候问题来了,如果我直接写一个这样的方法,就会报错,因为judge不能准确的判断联合类型具体的实例是什么:

const judge = (person: Man | Woman) => {
    person.smoke()
}

这时候就需要再引出一个概念叫做类型保护,类型保护有很多种方法,这里讲几个最常使用的。

9.2 类型保护

类型断言就是通过断言的方式确定传递过来的准确值

9.2.1 类型断言

interface Man {
  isMan: boolean
  smoke: () => {}
}

interface Woman {
  isMan: boolean
  makeup: () => {}
}

const judge = (person: Man | Woman) => {
  if(person.isMan){
    (person as Man).smoke()// 断言
  }else{
    (person as Woman).makeup()// 断言
  }
}

9.2.2 in语法

我们还经常使用in语法来作类型保护

interface Man {
  isMan: boolean
  smoke: () => {}
}

interface Woman {
  isMan: boolean
  makeup: () => {}
}
  
const judge = (person: Man | Woman) => {
  if ('smoke' in person) {
    person.smoke()
  } else {
    person.makeup()
  }
}

9.2.3 typeof 语法

先来写一个新的add方法,方法接收两个参数,这两个参数可以是数字number也可以是字符串string,如果我们不做任何的类型保护,只是相加,这时候就会报错。代码如下:

function add(first: string | number, second: string | number) {
  return first + second
}

可以直接使用typeof来进行解决:

function add(first: string | number, second: string | number) {
  if (typeof first === "string" || typeof second === "string") {
    return `${first}${second}`
  }
  return first + second  
}

9.2.4 instanceof 语法

class NumberObj {
  count: number
}

然后我们再写一个addObj的方法,这时候传递过来的参数,可以是任意的object,也可以是NumberObj的实例,然后我们返回相加值,不进行类型保护,这段代码一定是错误的:

function addObj(first: object | NumberObj, second: object | NumberObj) {
  return first.count + second.count
}

直接使用instanceof语法进行判断一下,就可以解决问题:

function addObj(first: object | NumberObj, second: object | NumberObj) {
  if (first instanceof NumberObj && second instanceof NumberObj) {
    return first.count + second.count
  }
  return 0
}

10. enum枚举类型

枚举的作用是列举类型中包含的各个值,一般用它来管理多个相同系列的常量(即不能被修改的变量),用于状态的判断。

// 枚举类型中的每项属性依次对应着数字0、1、2……
enum Status {
  dog,
  cat,
  lion
}

const getAnimal = (status: number) => {
  if (status === Status.dog) return 'dog'
  if (status === Status.cat) return 'cat'
  if (status === Status.lion) return 'lion'
}

console.log(getAnimal(0)) // dog
console.log(getAnimal(1)) // cat
console.log(getAnimal(2)) // lion

上述代码中,第一项属性对应的下标从0开始,假如想要从1开始,只需要做如下修改:

// 枚举类型中的每项属性依次对应着数字1、2、3……
enum Status {
  dog = 1,
  cat,
  lion
}

const getAnimal = (status: number) => {
  if (status === Status.dog) return 'dog'
  if (status === Status.cat) return 'cat'
  if (status === Status.lion) return 'lion'
}

console.log(getAnimal(0)) // undefined
console.log(getAnimal(1)) // dog
console.log(getAnimal(2)) // cat
console.log(getAnimal(3)) // lion

我们这里能打印出枚举的值(也有叫下标的),那如果我们知道下标后,也可以通过反查的方法,得到枚举的值:

// 枚举下标
console.log(Status.dog) // 1
// 枚举反查
console.log(Status[1]) // dog

11. 泛型(难点)

11.1 基本概念与使用

下面有一个简单的join方法,方法接受两个参数first和second,参数有可能是字符串类型,也有可能是数字类型。方法里为了保证都可以使用,所以我们只作了字符串的基本拼接。

const join = (first: string | number, second: string | number) => {
  return `${first}${second}`
}

console.log(join('baidu', '.com'))

现在有这样一个需求,就是first参数如果传的是字符串类型,要求second也传字符串类型.同理,如果是number类型,就都是number类型。
那现在所学的知识就完成不了啦,所以需要学习泛型来解决这个问题。

  • 泛型:[generic - 通用、泛指的意思],那最简单的理解,泛型就是泛指的类型。

泛型的定义使用<>(尖角号)进行定义的,比如现在给join方法一个泛型,名字就叫做T(起这个名字的意思,就是你可以随便起一个名字,但工作中要进行语义化),后边的参数,这时候他也使用刚定义的泛型名称。然后在正式调用这个方法时,就需要具体指明泛型的类型.

const join = <T>(first: T, second: T) => {
  return `${first}${second}`
}

console.log(join<string>('baidu', '.com')) // 两个参数类型必须同为string
console.log(join<number>(1, 2)) // 两个参数类型必须同为number

11.2 数组使用方法

// 数组,写法一
const arrFn = <T>(arr: T[]) => {
  return arr
}
console.log(arrFn<number>([0, 1, 2, 3, 4]))// 数组中每一项必须为number

// 数组,写法二
const arrFn1 = <T>(arr: Array<T>) => {
  return arr
}
console.log(arrFn1<number>([0, 1, 2, 3, 4]))// 数组中每一项必须为number

11.3 多个泛型定义

// 多个泛型定义
const join1 = <T, P>(first: T, second: P) => {
  return `${first}${second}`
}

console.log(join1<string, number>('你真', 6)) // 第一个参数为string,第二个为number

11.4 泛型的类型推断

泛型也是支持类型推断的,比如下面的代码并没有报错,这就是类型推断的功劳。

const join1 = <T, P>(first: T, second: P) => {
  return `${first}${second}`
}

console.log(join1<string, number>('你真', 6)) // 第一个参数为string,第二个为number

console.log(join1('祝', 999)) // 类型推断,第一个参数为string,第二个为number

但个人不建议大量使用类型推断,这会让你的代码易读和健壮性都会下降,所以这个知识点,大家做一个了解就可以了。

11.5 在类中使用泛型

一个类的基本例子:

class People {
  constructor(private lady: string[]) {}
  getName(index: number): string {
    return this.lady[index]
  }
}

const ladies = new People(['Linda', 'Anna', 'luna'])

console.log(ladies.getName(1))

使用泛型改造上述例子:

class People<T> {
  constructor(private lady: T[]) {}
  getName(index: number): T {
    return this.lady[index]
  }
}

const ladies = new People<string>(['Linda', 'Anna', 'luna'])

console.log(ladies.getName(1))

11.6 类中泛型的继承

定义一个interface接口Lady,并让泛型T继承Lady:

// 泛型中的继承
interface Lady {
  name: string
}

class People<T extends Lady> {
  constructor(private lady: T[]) {}
  getName(index: number): string {
    return this.lady[index].name
  }
}

const ladies = new People([{ name: 'Linda' }, { name: 'Anna' }, { name: 'luna' }])// 参数必须是数组包含对象形式

console.log(ladies.getName(1))

11.7 泛型的约束

// 泛型的约束,泛型只能为string或number
class People<T extends string | number> {
  constructor(private lady: T[]) {}
  getName(index: number): T {
    return this.lady[index]
  }
}

const ladies = new People<string>(['Linda', 'Anna', 'luna'])// 正常

const ladies1 = new People<boolean>([true, true, false])// 报错:类型“boolean”不满足约束“string | number”

12. 命名空间namespace

12.1 起步

新建一个项目TSWeb,终端输入:

  • npm init -y 初始化package,-y表示使用默认配置
  • tsc -init 初始化ts配置文件

创建基本目录:

  • 新建index.html,作为入口文件
  • 新建build文件夹作为编译生成文件夹
  • 新建src文件夹作为源文件夹
  • src下新建pages.ts,用来存放ts模块

修改tsconfig.json:

 "rootDir": "./src",
  "outDir": "./build",

index.html中代码如下:

DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <script src="./build/pages.js">script>
  <title>Documenttitle>
head>

<body>

body>

html>

在pages.ts中书写测试用例如下:

class Header {
  constructor() {
    const elem = document.createElement('div')
    elem.innerText = 'This is Header'
    document.body.appendChild(elem)
  }
}

class Content {
  constructor() {
    const elem = document.createElement('div')
    elem.innerText = 'This is Content'
    document.body.appendChild(elem)
  }
}

class Footer {
  constructor() {
    const elem = document.createElement('div')
    elem.innerText = 'This is Footer'
    document.body.appendChild(elem)
  }
}

class Page {
  constructor() {
    new Header()
    new Content()
    new Footer()
  }
}

在终端输入tsc,随后在build文件夹下便生成了pages.js

可以终端输入tsc -w来监视ts文件的变化,有变化则重新编译

在index.html的body标签中:

<body>
  <script> new Page()script>
body>

这时候再到浏览器进行预览,就可以看到对应的页面被展现出来了。看起来没有什么问题,但是有经验的程序员就会发现,这样写全部都是全局变量(通过查看./build/page.js文件可以看出全部都是var声明的变量)。过多的全局变量会让我们代码变的不可维护。

其实理想的是,只要有Page这个全局变量就足够了,剩下的可以模块化封装起来,不暴露到全局。

12.2 命名空间的使用

命名空间这个语法,很类似编程中常说的模块化思想,比如webpack打包时,每个模块有自己的环境,不会污染其他模块,不会有全局变量产生。命名空间就跟这个很类似,注意这里是类似,而不是相同。

命名空间声明的关键词是namespace 比如声明一个namespace Home,需要暴露出去的类,可以使用export关键词,这样只有暴漏出去的类是全局的,其他的不会再生成全局污染了。修改后的代码如下:

namespace Home {
  class Header {
    constructor() {
      const elem = document.createElement("div");
      elem.innerText = "This is Header";
      document.body.appendChild(elem);
    }
  }

  class Content {
    constructor() {
      const elem = document.createElement("div");
      elem.innerText = "This is Content";
      document.body.appendChild(elem);
    }
  }

  class Footer {
    constructor() {
      const elem = document.createElement("div");
      elem.innerText = "This is Footer";
      document.body.appendChild(elem);
    }
  }

  export class Page {
    constructor() {
      new Header();
      new Content();
      new Footer();
    }
  }
}

TS 代码写完后,再到index.html文件中进行修改,用命名空间的形式进行调用,就可以正常了。

现在再到浏览器中进行查看,可以看到现在就只有Home.Page是在控制台可以得到的,其他的Home.Header…这些都是得不到的,说明只有Home.Page是全局的,其他的都是模块化私有的。

这就是 TypeScript 给我们提供的类似模块化开发的语法,它的好处就是让全局变量减少了很多,实现了基本的封装,减少了全局变量的污染。

在src目录下新建一个文件components.ts,编写代码如下:

namespace Components {
  export class Header {
    constructor() {
      const elem = document.createElement("div");
      elem.innerText = "This is Header";
      document.body.appendChild(elem);
    }
  }

  export class Content {
    constructor() {
      const elem = document.createElement("div");
      elem.innerText = "This is Content";
      document.body.appendChild(elem);
    }
  }

  export class Footer {
    constructor() {
      const elem = document.createElement("div");
      elem.innerText = "This is Footer";
      document.body.appendChild(elem);
    }
  }
}

这里需要注意的是,我每个类(class)都使用了export导出,导出后就可以在page.ts中使用这些组件了。比如这样使用-代码如下。

namespace Home {
  export class Page {
    constructor() {
      new Components.Header();
      new Components.Content();
      new Components.Footer();
    }
  }
}

这时候你可以使用tsc进行重新编译,但在预览时,你会发现还是会报错,找不到Components,想解决这个问题,我们必须要在index.html里进行引入components.js文件。

<script src="./build/page.js">script>
<script src="./build/components.js">script>

这样才可以正常的出现效果。但这样引入太麻烦了,可不可以像webpack一样,只生成一个文件那?那答案是肯定的。

直接打开tsconfig.json文件,然后找到outFile配置项,这个就是用来生成一个文件的设置,但是如果设置了它,就不再支持"module":“commonjs"设置了,我们需要把它改成"module”:“amd”,然后在去掉对应的outFile注释,设置成下面的样子。

{
  "outFile": "./build/page.js"
}

配置好后,删除掉build下的js文件,然后用tsc进行再次编译。

然后删掉index.html文件中的component.js,在浏览器里还是可以正常运行的。

12.3 子命名空间

也就是说在命名空间里,再写一个命名空间,比如在Components.ts文件下修改代码如下。

namespace Components {
  export namespace SubComponents {
    export class Test {}
  }

  //something ...
}

写完后在控制台再次编辑tsc,然后你在浏览器中也是可以查到这个命名空间的Components.SubComponents.Test(需要刷新页面后才会显示)。

你可能感兴趣的:(前端,javascript,typescript,前端)