文章目录
一、TypeScript 简述
1. TypeScript 和 JavaScript
JavaScript 属于弱类型的解释型语言,弱类型虽然更灵活,但在越来越臃肿的前端代码中,它弱类型的特点会在开发和调试时造成很多困扰,比如:不清晰的数据类型、对于拼写错误的忽略、对于不存在的对象属性的访问等问题,都可能会导致开发人员花费大量时间去调查。
TypeScript 是由微软开发的,是基于 JavaScript 的扩展。换句话说就是 TypeScript 是 JavaScript 的超集,它涵盖了 JavaScript 的全部功能和语法,并新增了自己的特性。
2. TypeScript 新增的主要特性
- TypeScript 在运行前,需要编译器编译成 JavaScript。
- 静态时的类型检查功能(或者说编译时类型检查功能)。
- 新增更多的数据类型(any、unknown、never、void、tuple、enum)
- 新增 type 关键字、访问修饰符、泛型、接口、抽象类、装饰器等功能,更符合现代面向对象开发语言的特性。
二、使用 TypeScript 前的准备和编译
1 .编写 TypeScript 前的准备
1. TypeScript 的安装:
- 在使用 TypeScript 前, 需要先通过 npm 安装 TypeScript 模块,这个模块可以用来将
.ts文件编译成.js文件,
TypeScript 模块的安装命令:npm i typescript -g - 安装后可以通过
tsc --version来测试是否安装成功,tsc 应该是 TypeScriptCompile 的缩写。
2. 创建 TypeScript 的配置文件:
在待开发的目录内执行 tsc --init,之后会在目录内生成一个 tsconfig.json 配置文件,这个文件用来配置编译时的一些选项,文件中Json 的属性很多,暂时只需要知道其中 target 的属性即可,target 属性用来配置在编译时用哪个版本的 ECMAScript 语法进行编译。如:"target": "es6"。就是将 .ts 文件以 ES6 语法 编译成 的 .js 文件。
2. 创建和编译 TypeScript
1. 创建 TypeScript
在待开发的目录中,创建以 .ts 结尾的文件就是 TypeScript 文件。在文件内就可以编写 TypeScript 语法,甚至只使用 JavaScript 语法。
2. 将 TypeScript 文件编译成 JavaScript 文件
当 .ts 文件编写好后,运行 tsc xxx.ts 命令就可以将 xxx.ts 编译成 xxx.js 文件。或者直接使用 tsc 命令,后面不加 xxx.ts ,则会把当前目录下的所有 .ts 文件都分别进行编译。
这种方式,当 .ts 文件的内容修改后,需要每次都手动将 .ts 文件编译成 .js 文件,比较麻烦。
可以使用 tsc --watch 或 tsc --w 命令对目录进行监视,只要目录内的 .ts 文件的内容变化了,就是自动将其编译成 .js 文件。
三、TypeScript 中的类型
1. TypeScript 中声明类型的语法
在 TypeScript 中使用 冒号(:)对变量、函数形参、函数返回值的类型进行声明。
声明基本类型时,尽量使用小写,如 string、number、boolean
1. 声明变量的类型
let a: string
a = 'hello' // 正常赋值
a = 18 // 这行在静态检查时会发生错误提示,不能将 number 类型分配给 string
let b: string = 'hello' // 声明变量类型的同时赋初始值
console.log(b)
2. 声明函数形参的类型
function demo(a: string) {
console.log(a)
}
demo('hello') // 方法正常调用
demo(18) // 这行在静态检查时会发生错误提示,不能将 number 类型分配给 string
// 在声明形参类型的同时赋默认值
function demo(a: string = '111') {
console.log(a)
}
demo()
3. 声明函数返回值的类型
function demo(): string {
return 'hello' // 如果没有这行代码,在静态检查时会提示函数必须有一个 string 类型返回值
}
4. 类型推断
在 TypeScript 中,当不给变量指定数据类型时,TS 会自动进行类型推断。建议使用 TS 时最好显示指定变量类型。
- 当创建变量的同时给变量赋了初始值,TS 就会将初始值的类型定为变量类型。
let a = 'hello' // 创建变量的同时给变量赋了初始值,TS 就会将初始值的类型定为变量类型。
a = 18 // 这行在静态检查时会发生错误提示,不能将 number 类型分配给 string
- 当没有在创建变量的同时赋初始值时,TS 会将变量类型推断为 any(这是 TS 新增的数据类型,下面会提到)
let b // 没有在创建变量的同时赋初始值时,TS 会将变量类型推断为 any
// 以下赋值均可
a = 'hello'
a = true
a = 18
2. TypeScript 中新增的数据类型
在 JavaScript 中,定义了 7 种基本类型(string、number、boolean、null、undefined、bigint、symbol) 和 对象类型(Object)
在 TypeScript 中对数据类型进行了扩展,新增(any、unknown、never、void、tuple、enum):
- any:任意类型,变量指定为该类型就相当于放弃了对该变量的类型检查。相当于在使用 JS
let a: any
// 以下赋值均可,无警告
a = 'hello'
a = true
a = 18
// 对 a 的最后一次赋值是 number 类型,而 number 类型没有 substring 函数,但此处 TS 却无警告,导致会在运行时出错
a.substring(1, 2)
- unknown:未知类型,在对该类型变量赋值时没有类型限制,但是在使用该类型变量时会强制要求做类型检查。比 any 要安全一点
let a: unknown
// 以下赋值均可,无警告
a = 'hello'
a = true
a = 18
//a.substring(1, 2) // 这行在静态检查时会发生错误提示,提示 a 的类型为未知
/*
* unknown 类型的正确使用方式
*/
// 方式一:添加类型判断
if (typeof a === 'string') {
a.substring(1, 2)
}
// 方式二:用断言
(a as string).substring(1, 2);
// 方式三:用断言语法糖
(<string>a).substring(1, 2)
- never:变量不能接受任何类型,所以几乎不会用在变量上,没有意义。但是可以用在函数返回值类型上,但是这种函数也比较特殊,必须得是不能正常结束的函数,因为正常结束的函数默认会返回 undefined,undefined 也不能赋值给 never,所以必须得是不能正常执行完的函数才能使用 never 修饰函数返回值
function demo(): never {
throw new Error("程序异常退出!") // 异常会提前终止函数的执行
}
- void :一般用来修饰函数,规定函数不返回任何值(undefined 除外),调用者也不应该以该函数返回值进行任何操作。
function demo1(): void {
return '' // 这行在静态检查时会发生错误提示,提示不能将 string 赋值给 void
return 19 // 这行在静态检查时会发生错误提示,提示不能将 number 赋值给 void
return true; // 这行在静态检查时会发生错误提示,提示不能将 boolean 赋值给 void
}
// 函数默认返回 undefined,undefined 可以赋值给 void
function demo2(): void {
}
// 函数默认返回 undefined,undefined 可以赋值给 void
function demo3(): void {
return
}
// 函数返回 undefined,undefined 可以赋值给 void
function demo4(): void {
return undefined
}
- tuple:一种特殊的数组类型,指明数组中每个元素的类型
// 指定数组中的第一个元素必须是 string 类型,第二个元素必须是 number 类型
let arr1: [string, number]
arr1 = ['hello', 18]
// 指定数组中的第一个元素必须是 string 类型,第二个元素是可选的,如果存在,必须是 number 类型
let arr2: [string, number?]
arr2 = ['hello']
arr2 = ['hello', 18]
// 指定数组中的第一个元素必须是 string 类型,后面的元素可以是任意数量的 number 类型
let arr3: [string, ...number[]]
arr3 = ['hello']
arr3 = ['hello', 18, 19]
- enum:定义一组常量,增加代码可读性。如果只是简单的访问枚举中的常量,可以在定义 enum 时使用 const 修饰,在编译时会进行內联,减少生成的 JS 代码量。
// 枚举项的值默认按照声明顺序从 0 开始向后递增
const enum Sex{
Man, // 0
Women // 1
}
console.log(Sex.Man) // 输出 0
console.log(Sex.Women) // 输出 1
// 声明变量为枚举类型
let a : Sex
a = Sex.Man
console.log(a) // 输出 0
// 可以指定枚举项的值
const enum Sex2{
Man = '男',
Women = '女'
}
console.log(Sex2.Man) // 输出 男
console.log(Sex2.Women) // 输出 女
3. 声明 object 和 Object 类型
- 声明 object(小写) 类型表示可以存储所有非基本类型,如(对象,数组,函数)
let a: object
// 以下非基本类型均可
a = {}
a = { name: '张三' }
a = [1, 3, 5, 7, 9]
a = function () { }
a = new String('123')
class Person { }
a = new Person()
// 以下基本类型均不可
a = 1 // 警告:不能将类型“number”分配给类型“object”
a = true // 警告:不能将类型“boolean”分配给类型“object”
a = '你好' // 警告:不能将类型“string”分配给类型“object”
a = null // 警告:不能将类型“null”分配给类型“object”
a = undefined // 警告:不能将类型“undefined”分配给类型“object
- 声明 Object(大写) 类型表示除了 undefined 和 null,其他类型都可以存储
let b: Object
// 以下均可
b = {}
b = { name: '张三' }
b = [1, 3, 5, 7, 9]
b = function () { }
b = new String('123')
class Person { }
b = new Person()
b = 1
b = true
b = '你好'
// 以下均不可
b = null // 警告:不能将类型“null”分配给类型“Object”
b = undefined // 警告:不能将类型“undefined”分配给类型“Object”
四、关键字 type
通过 type 可以给已有的类型起别名,也可以给自定义的类型起名,也可以对多个类型进行联合和交叉使用。用法如下:
- 起别名
// 给 number 类型起别名为 num
type num = number
// 直接使用新的类型名称 num 进行变量类型的声明
let a: num
a = 10
// 直接创建一个自定义类型,并起名为 Persson
type Person = {
name: string
age: number
}
// 直接使用 Person 类型
let p: Person
p = {
name: '',
age: 18
}
- 类型联合:
// 给 number 类型 和 string 类型 联合起别名为 num
type num = number | string
// 将变量类型声明为 num 后,该变量既可以接收 number 类型也可以接收 string 类型
let a: num
a = 10
a = '10'
- 类型交叉:
// 先创建一个自定义类型,并起名为 Persson1
type Person1 = {
name: string
}
// 再创建一个自定义类型,并起名为 Persson2
type Person2 = {
age: number
}
// 将 Person1 和 Person2 交叉并起新的别名为 demo
type demo = Person1 & Person2
// 用 demo 声明的变量,同时拥有 Person1 和 Person2 的属性值
let a: demo
a = {
name: '',
age: 18
}
五、访问权限控制符 和 用 Class 关键字声明类
1. 访问权限控制符
TS 中新增了访问权限控制符,可以对类属性和函数进行访问权限控制。
| 修饰符 | 含义 | 规则 |
|---|---|---|
| public | 公开的 | 默认的访问权限。可以被:类内部、子类、类外部访问 |
| protected | 受保护的 | 可以被:类内部、子类访问 |
| private | 私有的 | 只可以被类内部访问 |
| readonly | 只读 | 规则 |
2. 用 Class 关键字声明类
在 TS 中通过 class 关键字声明类时, 和 JS 略有不同。
JS 中只需要在构造函数 constructor 的形参中声明类中的属性即可:
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
}
在 TS 中,不仅要在构造函数 constructor 的形参中声明类的属性,还要在类中主动声明:
class Person {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
在 TS 中如果也想像 JS 一样,不在类中声明属性, 可以在构造函数 constructor 中使用访问权限控制符进行简化
class Person {
constructor(public name: string, public age: number) {
this.name = name
this.age = age
}
}
六、抽象类
和 Java 中的抽象类一样, 通过 abstract 关键字修饰,抽象类不能直接被实例化,抽象类中可以包含由 abstract 修饰的未实现的方法,由子类去实现。
abstract class Person {
constructor(public name: string) { }
abstract doSomething(): void
}
class Man extends Person {
// 子类重写父类方法时,最好加上 override 关键字,可以对是否真的重写进行静态检查
override doSomething() {
console.log("子类重写")
}
}
七、接口
接口和 Java 8 之前的接口也大同小异,接口内只能定义属性和未实现的方法。不同的是,TS 中对于同名接口,会自动进行合并。
// 定义一个接口
interface IPerson {
name: string
doSomething(): void
}
// 定义重名接口会进行合并
interface IPerson {
age: number
}
// 创建一个类实现接口
class Person implements IPerson {
constructor(public name: string, public age: number) {
}
doSomething(): void {
console.log('hello')
}
}
let p: Person
p = new Person('hello', 18)
p.doSomething()
// 也可以不创建类,直接实现接口,有点类似匿名类的感觉
let p2: IPerson
p2 = {
name: '',
age: 1,
doSomething() {
console.log('hello')
},
}
八、泛型
和 Java 中的泛型道理也差不多,在定义函数、类、接口时,为了增加通用型,可以使用泛型定义参数或属性,在使用的时候,在具体指定其真实类型。
// 定义泛型函数
function say<T>(data: T): void {
console.log(data)
}
// 调用泛型函数并传入真实类型
say<string>('hello')
// 定义泛型接口
interface IPerson<T> {
name: T
}
// 匿名实现接口,并传入真实类型
let p: IPerson<string> = {
name: 'hello'
}
// 定义泛型类
class Person<T> {
constructor(public name: T) { }
}
// 使用时传入真实类型
let p2: Person<string>
p2 = new Person('hello')
九、类型声明文件
因为 JS 不具备 TS 的类型声明功能,所以当我们在 TS 中引入 JS 模块时,TS 无法知道 JS 中声明的 变量、函数参数、返回值等的具体类型,只能全部当作 any 类型处理,虽然程序可以正常执行,但是却不能享受 TS 的编译时类型检查。
为了让 TS 引入 JS 时,能够享受 TS 的编译时类型检查,就需要指明 JS 中变量、函数参数、返回值等的具体类型,而 “类型声明文件” 就是做这个事情的。“类型声明文件” 的后缀名是 .d.ts
假设有 JS 文件,demo.js:
export function add(a, b) {
return a + b;
}
现在要在 TS 文件 test.ts 中引入 demo.js 导出的模块:
import { add } from './demo.js';
console.log(add(1, 2));
这样程序虽然可以正常运行, 但是 TS 不会对引入的 JS 模块进行静态类型检查(编译时检查)。这时候就可以创建一个 “类型声明文件”,创建规则就是在 JS 文件同目录中 创建 JS文件同名.d.ts。本例中就是创建 demo.d.ts
declare function add(num1: number, num2: number): number
export { add }
有了这个 “类型声明文件后”,TS 就会对导入的 JS 模块进行类型检查,增加代码可靠性。
九、装饰器
装饰器有点切面编程的感觉,用类似于 Java 中注解的方式, 对 类、函数、属性、参数等进行增强。
使用装饰器功能需要在 tsconfig.json 中设置 "experimentalDecorators": true,tsconfig.json 是前面提到过的通过 tsc --init 生成的。
简单列举下各种装饰器大概的样子,大同小异,具体得根据实际需求才能发挥作用。
1. 类装饰器
/**
* 定义一个类装饰器
* 参数 target 是被装饰的类的类信息
* 如果在装饰器中 return 一个类,那么这个类会替换被装饰的类
*
*/
function Demo(target: Function) {
console.dir(target)
}
@Demo // 使用类装饰器
class Test {
}
// 当装饰器修饰的类的对象被创建时会触发类装饰器
new Test()
2. 属性装饰器
/*
定义一个属性装饰器
target: 对于静态属性来说值是类,对于实例属性来说值是类的原型对象。
propertyKey: 属性名。
*/
function Demo(target: object, propertyKey: string) {
console.log(target,propertyKey)
}
class Person {
@Demo age: number
@Demo static school:string
constructor(name: string, age: number) {
this.age = age
}
}
const p1 = new Person('张三', 18)
3. 方法装饰器
/*
参数说明:
○ target: 对于静态方法来说值是类,对于实例方法来说值是原型对象。
○ propertyKey:方法的名称。
○ descriptor: 方法的描述对象,其中value属性是被装饰的方法。
*/
function Demo(target: object, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(target)
console.log(propertyKey)
console.log(descriptor)
const original = descriptor.value;
// 替换原始方法
descriptor.value = function (...args: any[]) {
console.log(`${propertyKey}开始执行......`)
const result = original.call(this, ...args)
console.log(`${propertyKey}执行完毕......`)
return result;
}
}
class Person {
constructor(
public name: string,
public age: number,
) { }
// Demo装饰实例方法
@Demo speak() {
console.log(`你好,我的名字:${this.name},我的年龄:${this.age}`)
}
// Demo装饰静态方法
@Demo static isAdult(age: number) {
return age >= 18;
}
}
const p1 = new Person('张三', 18)
// 方法调用
p1.speak()
4. 访问器装饰器
function RangeValidate(min: number, max: number) {
return function (target: object, propertyKey: string, descriptor: PropertyDescriptor) {
// 保存原始的 setter 方法,以便在后续调用中使用
const originalSetter = descriptor.set;
// 重写 setter 方法,加入范围验证逻辑
descriptor.set = function (value: number) {
// 检查设置的值是否在指定的最小值和最大值之间
if (value < min || value > max) {
// 如果值不在范围内,抛出错误
throw new Error(`${propertyKey}的值应该在 ${min} 到 ${max}之间!`);
}
// 如果值在范围内,且原始 setter 方法存在,则调用原始 setter 方法
if (originalSetter) {
originalSetter.call(this, value);
}
};
};
}
class Weather {
private _temp: number;
constructor(_temp: number) {
this._temp = _temp;
}
// 设置温度范围在 -50 到 50 之间
@RangeValidate(-50, 50)
set temp(value) {
this._temp = value;
}
get temp() {
return this._temp;
}
}
const w1 = new Weather(25);
console.log(w1)
w1.temp = 67
console.log(w1)
5. 装饰器工厂
如果想给装饰器传参,就需要使用装饰器工厂。
// 定义一个装饰器工厂 LogInfo,它接受一个参数 n,返回一个类装饰器
function LogInfo(n:number) {
// 装饰器函数,target 是被装饰的类
return function(target: Function){
target.prototype.speak = function () {
for (let i = 0; i < n; i++) {
console.log(`我的名字:${this.name},我的年龄:${this.age}`)
}
}
}
}
@LogInfo(5)
class Person {
constructor(
public name: string,
public age: number
) { }
speak() {
console.log('你好呀!')
}
}
let p1 = new Person('张三', 18)
p1.speak()
6. 装饰器组合使用时的执行顺序问题
如果有装饰器工厂的情况,会先从上到下执行所有的装饰器工厂,生成出装饰器。然后在从下到上依次执行装饰器。
//装饰器
function test1(target: Function) {
console.log('test1')
}
//装饰器工厂
function test2() {
console.log('test2工厂')
return function (target: Function) {
console.log('test2')
}
}
//装饰器工厂
function test3() {
console.log('test3工厂')
return function (target: Function) {
console.log('test3')
}
}
//装饰器
function test4(target: Function) {
console.log('test4')
}
@test1
@test2()
@test3()
@test4
class Person { }
/*
控制台打印:
test2工厂
test3工厂
test4
test3
test2
test1
*/

558

被折叠的 条评论
为什么被折叠?



