深入浅出 Typescript
TypeScript 是 JavaScript 的一个超集,支持 ECMAScript 6 标准。
TypeScript 由微软开发的自由和开源的编程语言。
TypeScript 设计目标是开发大型应用,它可以编译成纯 JavaScript,编译出来的 JavaScript 可以运行在任何浏览器上。
以上为网上对 Typescript 的一些解释,那我们为什么要学 Typescript?
提到前端我们首先会想到 HTML,CSS,JavaScript 三大家族,我们掌握这三个就可以在前端界获得一席之地,怎么突然又冒出个 Typescript,真心是学不动了,但是众所周知,Vue 创始人尤雨溪尤大大已经宣布 Vue3.x 代码库将使用 Typescript 编写,并且在知乎上对于"Typescript 不适合在 Vue 开发业务中使用吗?" 的提问中做出回答,传送门:https://www.zhihu.com/question/310485097/answer/591869966,Vue 又是现在国内主流的前端框架之一,只能说,如果不学 Typescript,尤大大都救不了你了。
众所周知,从本质上来说,JavaScript是一种自由松散语言,它的语法规则并不是那么严格。正因为如此,我们就更容易犯错,而且,即使是在运行的时候,我们也不能找到所有的错误。鉴于此,TypeScript作为JavaScript的增强版,它的语法更严格,我们在编写代码的时候就能够发现大部分错误。不仅如此,按照TypeScript官方的说法,TypeScript使得我们能够以JavaScript的方式实现自己的构思。TypeScript对面向对象的支持也非常完善,它拥有面向对象编程语言的所有特性。
TypeScript最大的目的是让程序员更具创造性,提高生产力,它将极大增强JavaScript编写应用的开发和调试环节,让JavaScript能够方便用于编写大型应用和进行多人协作。
不过目前最后运行时还需要将TypeScript编译为JavaScript。
那接下来我们就来看看如何安装使用 Typescript。
在安装 Typescript 之前我们要先安装 NodeJs,然后运行
npm install -g typescript
以上命令会在全局环境下安装 tsc
命令,安装完成之后,我们就可以在任何地方执行 tsc
命令了。
编译一个 TypeScript 文件很简单:
tsc demo.ts
运行上面的代码,我们就可以将 demo.ts 生成一个可以让浏览器解析的 demo.js 的文件。
我们约定使用 TypeScript 编写的文件以 .ts
为后缀,用 TypeScript 编写 React 时,以 .tsx
为后缀。
那么我们在编写代码的时候不能每次都手动编译 .ts 文件,我们想要的是实时编译 .ts 文件,接下来我们就以 webstorm 编辑器来使 .ts 文件进行实时编译。其他编辑器可自行百度如何实时编译。
webstorm 版本:
创建一个 demo 项目,然后在项目中创建一个 tsconfig.json 的文件,内容如下:
这里只是简单的配置,详细参数配置:https://www.tslang.cn/docs/handbook/tsconfig-json.html
打开Webstorm,为TypeScript文件更改编译设置,File->Settings->Tool->File Watchers->TypeScript,这里我们需要选择TypeScript,但是File Watchers下默认是不存在的。需要点击右侧“+”号,选择,弹出 New Watcher,设置好圈红线的部分,点击ok。勾选“TypeScript”,点击ok。
File->Settings->Languages & Frameworks->TypeScript
根据上面的操作我们就可以实时编译 .ts 文件了。
目录结构如下,在 index.html 中银润 test.js
test.ts 会实时编译为 test.js 文件。
test.ts
index.html 开发者工具中的打印日志
根据上面的步骤我们就可以对 ts 文件进行实时编译了,接下来我们就来看一下 Typescript 的一些基本用法。
从上图我们可以看出 Typescript 包含了 ES6 和 ES5,那我们就可以在 typescript 中使用 es5 和 es6,同时进行了扩展,语法上跟我们之前讲的 Java 语法有很多相似。
Typescript 基础类型
Typescript 基础类型有:布尔类型(boolean)、数字类型(number)、字符串类型(string)、数组类型(array)、元组类型(tuple)、枚举类型(enum)、任意类型(any)、null 和 undefined 、void、never 类型等,接下来我们就一一来看一下这些类型的应用。
1 /** 2 * 在定义完参数进行赋值时, 3 * 必须按照给定的参数类型进行赋值 4 * 否则会出现编译问题 5 * 但在页面当中还是会进行编译 6 * 但是不提倡这么做 7 */ 8 // boolean 9 let flag: boolean = true; 10 console.log(flag); // true 11 // let flag1:boolean = 123; // Type '123' is not assignable to type 'boolean'. 12 13 // number 14 let num: number = 123; 15 console.log(num); // 123 16 17 // string 18 let str: string = "abc"; 19 console.log(str); // abc 20 21 // array 两种定义方式 22 let arrS: string[] = ["123", "abc"]; // 数组内元素必须为 string 23 let arrA: Array<number> = [123, 456]; // 数组内元素必须为 number 24 console.log(arrS); // ["123", "abc"] 25 console.log(arrA); // [123, 456] 26 27 // tuple 元祖类型,属于数组的一种,可以为每个元素指定类型 28 let tup: [number, string] = [123, "abc"]; 29 console.log(tup); // [123, "abc"] 30 // let tup1:[number,string] = ["abc",123]; // 报错 31 32 // enum 枚举类型 33 /** 34 * 在日常生活或者开发中 35 * 很多都不能或者不容易使用数据表达 36 * 如:颜色,日期,角色,性别等 37 * 例如在开发中我们常用 -1 表示 error,用 0 表示 success 38 * 这个就可以成为枚举 39 */ 40 enum Flag { 41 error = -1, 42 success = 0 43 } 44 45 let e: Flag = Flag.error; 46 console.log(e); // -1 47 48 /** 49 * 如果枚举元素不赋值,则默认取值为下标 50 * 如果某个元素取值为数字 n 51 * 后面的元素如果不取值则默认为 n+1,以此类推 52 * 如果某个元素取值为 string 类型 53 * 后面的元素则必须取值,取值类型无要求 54 */ 55 enum Color { 56 red, blue, black = 4, yellow, green = "green", white = 123 57 } 58 59 let red: Color = Color.red; 60 let blue: Color = Color.blue; 61 let black: Color = Color.black; 62 let yellow: Color = Color.yellow; 63 let green: Color = Color.green; 64 let white: Color = Color.white; 65 console.log(red, blue, black, yellow, green, white); // 0 1 4 5 "green" 123 66 67 // undefined 68 let un: undefined; 69 console.log(un); // undefined 70 71 // 我们也可以通过 | 来赋值多种类元素 72 let uns: number | undefined; 73 74 // any 类型,可以为任意类型 75 let an: any = 123; 76 console.log(an); // 13 77 an = "abc"; 78 console.log(an) // abc
在上面的代码中,我们演示了一下 Typescript 中的一些基本类型的使用,接下来我们再来看一下在函数中数据类型的应用。
Typescript 函数方法
1 // 如果没有返回值,则在方法名后面加 :void 2 function test(): void { 3 console.log("test") 4 } 5 6 test(); // test 7 8 // 如果有返回值,则在方法名后面加 :返回值的类型 9 function num(): number { 10 return 123; 11 } 12 13 console.log(num()); // 123 14 15 function str(): string { 16 return "abc"; 17 } 18 19 console.log(str()); // "abc" 20 21 // 定义传参 22 function getData(name: string, age: number): void { 23 console.log(`${name}--${age}`) 24 } 25 26 getData("张三", 18); // 张三--18 27 // getData("张三"); // 报错 Expected 2 arguments, but got 1. 28 // getData("张三","18"); // 报错 Argument of type '"18"' is not assignable to parameter of type 'number'. 29 30 /** 31 * 方法可选参数 32 * 在参数后面添加 ? 33 * 表示该参数为可选参数 34 */ 35 function getInfo(name: string, age?: number): string { 36 if (age) { 37 return `${name}--${age}` 38 } else { 39 return `${name}` 40 } 41 } 42 43 console.log(getInfo("张三", 18)); // 张三--18 44 console.log(getInfo("张三")); // 张三 45 46 /** 47 * 方法默认参数 48 * 在参数后面直接赋值 49 * 表示该参数直接当做了被传入参数 50 */ 51 function getUser(name: string, age: number = 18): string { 52 if (age) { 53 return `${name}--${age}` 54 } else { 55 return `${name}` 56 } 57 } 58 59 console.log(getUser("张三", 18)); // 张三--18 60 console.log(getUser("张三")); // 张三--18 61 62 /** 63 * 剩余参数 64 * 如果在传参过程中 65 * 前面的参数已经给定 66 * 在调用函数传参时会先将 67 * 传入的参数作为指定参数 68 * 剩余参数必须为最后一个参数传入 69 */ 70 // 正常的传参 71 function sum1(...arr: number[]): number { 72 let sum: number = 0; 73 for (let i = 0; i < arr.length; i++) { 74 sum += arr[i]; 75 } 76 return sum; 77 } 78 console.log(sum1(1, 2, 3, 4)); // 10 79 80 // a 作为第一个参数,其余的为剩余参数,即 (a,剩余参数) 81 function sum2(a: number, ...arr: number[]): number { 82 let sum: number = 0; 83 for (let i = 0; i < arr.length; i++) { 84 sum += arr[i]; 85 } 86 return sum; 87 } 88 console.log(sum2(1, 2, 3, 4)); // 10
在上面的代码中,我们实现了在 ts 中如何定义方法和如何进行方法传参,跟定义基本类型一样需要对方法进行有效的规定。
接下来我们再来看一下在 ts 中如何实现类和类的继承。
Typescript 类
在 ES5 中,我们是通过构造方法和原型链的方式进行继承的,在之前的文章中我们也讲过如何实现继承,传送门:https://www.cnblogs.com/weijiutao/p/12090916.html,Typescript 包含 ES6,所以本章着重讲解一下 ES6 中 class 关键字的类和继承。
1 // 在 ts 中定义类 2 class Person { 3 name: string; 4 5 constructor(name: string) { 6 this.name = name; 7 } 8 9 getName(): void { 10 console.log(this.name); 11 } 12 13 setName(name: string): string { 14 return this.name = name; 15 } 16 17 work(): void { 18 console.log("父类在工作") 19 } 20 } 21 22 let p = new Person("张三"); 23 p.getName(); // 张三 24 p.setName("李四"); 25 p.getName(); // 李四 26 p.work(); // 父类在工作 27 28 // 在 ts 中实现继承 29 /** 30 * 通过 extends 继承了 Person 的属性和方法 31 */ 32 class Student extends Person { 33 constructor(name: string) { 34 super(name); 35 } 36 37 // 子类自己的方法 38 run(): void { 39 console.log(this.name + "在运动") 40 } 41 42 // 子类重写父类的方法 43 work(): void { 44 console.log("子类在工作") 45 } 46 47 } 48 49 let s = new Student("王五"); 50 s.getName(); // 王五 51 s.run(); // 王五在运动 52 s.work(); // 子类在工作
* 类里面的修饰符
* Typescript 里面定义属性的时候
* 给我们提供了三种修饰符
* public:公有类型,在类里面、子类、类外面都可以访问
* protected:保护类型,在类里面,子类里面可以访问,类外面无法访问
* private:私有类型,在类里面可以访问,子类,类外面无法访问
* 属性不加修饰符,默认为公有属性
1 /** 2 * 类里面的修饰符 3 * Typescript 里面定义属性的时候 4 * 给我们提供了三种修饰符 5 * public:公有类型,在类里面、子类、类外面都可以访问 6 * protected:保护类型,在类里面,子类里面可以访问,类外面无法访问 7 * private:私有类型,在类里面可以访问,子类,类外面无法访问 8 * 属性不加修饰符,默认为公有属性 9 */ 10 11 class Person { 12 name: string; 13 public age: number = 18; 14 protected sex: string = "男"; 15 private city: string = "北京"; 16 17 constructor(name: string) { 18 this.name = name; 19 } 20 21 // 在本类中访问 public 类型 22 getName(): void { 23 console.log(this.name); 24 } 25 26 // 在本类中访问 public 类型 27 getAge(): void { 28 console.log(this.age); 29 } 30 31 // 在本类中访问 protected 类型 32 getSex(): void { 33 console.log(this.sex); 34 } 35 36 // 在本类中访问 private 类型 37 getCity(): void { 38 console.log(this.city); 39 } 40 41 } 42 43 let p = new Person("张三"); 44 // 外部访问 public 类型 45 console.log(p.name); // 张三 46 // 外部访问 public 类型 47 console.log(p.age); // 18 48 // 外部访问 protected 类型 49 // console.log(p.sex); // 报错 Property 'sex' is protected and only accessible within class 'Person' and its subclasses. 50 // 外部访问 private 类型 51 // console.log(p.city); // 报错 Property 'city' is private and only accessible within class 'Person'. 52 53 class Student extends Person { 54 constructor(name: string) { 55 super(name); 56 } 57 58 getInfo(): void { 59 // 在子类中访问 public 属性 60 console.log(this.name); 61 // 在子类中访问 private 属性 62 console.log(this.age); 63 // 在子类中访问 protected 属性 64 console.log(this.sex); 65 // 在子类中访问 private 属性 66 // console.log(this.city) // 报错 Property 'city' is private and only accessible within class 'Person'. 67 } 68 } 69 70 let s = new Student("王五"); 71 s.getInfo(); // 王五 18 男
在上面的代码中我们实现了一下 class 中的修饰符,接下来我们再来看一下 class 的静态属性、静态方法
1 /** 2 * 通过 static 关键字可以定义静态属性和静态方法 3 * 静态属性和静态方法直接通过 类. 来实现 4 */ 5 6 class Person { 7 name: string; 8 static age: number = 18; 9 10 constructor(name: string) { 11 this.name = name; 12 } 13 14 run(): void { 15 console.log(`${this.name}在运动`) 16 } 17 18 /** 19 * 静态方法无法通过 this 调用类里面的属性 20 * 通过 类. 调用 21 */ 22 static work(): void { 23 console.log("父类静态方法" + Person.age) 24 } 25 } 26 27 let p = new Person("张三"); 28 p.run(); // 张三在运动 29 console.log(p.name); // 张三 30 31 console.log(Person.age); // 18 32 Person.work(); // 父类静态方法18
在上面的代码中我们实现了一下 class 中的静态属性、静态方法,接下来我们再来看一下 class 的多态
1 /** 2 * 我们在定义类方法后 3 * 子类继承该类,并根据实际情况 4 * 来实现自己所需要的类方法 5 */ 6 class Person { 7 name: string; 8 age: number; 9 10 constructor(name: string) { 11 this.name = name; 12 } 13 14 work(): void { 15 console.log(`${this.name}在工作`) 16 } 17 18 19 } 20 21 class Student extends Person { 22 constructor(name: string) { 23 super(name); 24 } 25 26 run(): void { 27 console.log(`${this.name}在学习`) 28 } 29 } 30 let s = new Student("张三"); 31 s.run(); // 张三在学习 32 33 class Teacher extends Person { 34 constructor(name: string) { 35 super(name); 36 } 37 38 run(): void { 39 console.log(`${this.name}在讲课`) 40 } 41 } 42 let t = new Teacher("李四"); 43 t.run(); // 李四在讲课
在上面的代码中,我们定义了 Person 类并定义了一个 work 方法,然后 Student 和 Teacher 类分别继承了 Person,但是根据自己的角色重写了 work 方法,这就是一种多态。
接下来我们再来看一下 class 中的抽象类
1 /** 2 * 抽象类是提供其他类继承的基类,不能被实例化 3 * abstract 关键字定义抽象类和抽象方法 4 * 子抽象类中的抽象方法不能包含具体实现,必须在实现类中实现 5 * 抽象类和抽象方法只是用来定义标准 6 */ 7 abstract class Person { 8 name: string; 9 10 constructor(name: string) { 11 this.name = name; 12 } 13 14 abstract work(): any; 15 } 16 17 // 无法被实例化 18 // let p = new Person(); // 报错 error TS2511: Cannot create an instance of an abstract class. 19 20 class Student extends Person { 21 constructor(name: string) { 22 super(name); 23 } 24 25 work(): any { 26 console.log(`${this.name}在学习`) 27 } 28 } 29 30 let s = new Student("张三"); 31 s.work(); // 张三在学习
Typescript 接口
在面向对象的编程中,接口是一种规范的定义,它定义了行为和动作的规范,在程序设计里面,几口是一种限制和规范的作用。接口定义了某一批类所需要遵循的规范,接口不关心这些类的内部状态数据,也不关心这些类里面方法的实现细节,它只规定这批类里面必须提供某些方法,提供这些方法的类就可以满足实际需要,Typescript 中的接口类似于 Java,同时还增加了更灵活的接口类型,包括属性、函数、数组类等。
在日常生活中,我们会接触到很多类似接口的问题,比如 USB 接口,我们在电脑上插鼠标,键盘,U盘的时候不用去考虑它到底能不能插进去,只要型号对了就肯定能插进去,接口就相当于一个标准,你要想把鼠标插到我的电脑上,在出厂时就必须遵守该电脑定义的接口标准。
接下来我们就来看一下接口:
1、属性和类接口
1 interface FullName { 2 firstName:string, 3 lastName:string, 4 sayHi: ()=>string 5 } 6 7 let person:FullName = { 8 firstName:"张", 9 lastName:"三", 10 sayHi: ():string =>{return "hell world"} 11 }; 12 13 console.log(person.firstName); // 张 14 console.log(person.lastName); // 三 15 console.log(person.sayHi()); // hello world
在上面的代码中,我们通过 interface 编写了一个 FullName 的接口,然后定义了一个 person 的变量来实现这个接口,那么我们就可以使用该接口里面的属性了。
2、函数类型接口
1 // 对方法传入的参数以及返回值进行约束 2 interface encrypted { 3 (key: string, value: string): string 4 } 5 6 // key 和 value 必须符合 encrypted 接口的 string 类型约束 7 let getData: encrypted = (key: string, value: string) => { 8 return `${key}--${value}`; 9 }; 10 // 错误写法 11 // let getData:encrypted = (key:number,value:string)=>{ 12 // return `${key}--${value}`; 13 // }; 14 console.log(getData("name", "张三")); // name--张三
3、数组类型接口
1 interface nameArray { 2 [index:number]:string 3 } 4 5 let list:nameArray = ["张三","李四"]; 6 // let list:namelist = ["张三","李四",123]; // 错误元素 123 不是 string 类型
4、接口的继承
1 /** 2 * 通过 implements 来实现接口的继承 3 * 同时接口内的属性和方法在子类中必须重新定义 4 * 可以实现多继承,class 子类 implements 接口1, 接口2{ } 5 */ 6 7 interface Person { 8 name: string, 9 work: () => void 10 } 11 12 class Student implements Person { 13 name: string; 14 15 constructor(name: string) { 16 this.name = name; 17 } 18 19 work(): void { 20 console.log(`${this.name}在学习`) 21 } 22 } 23 24 let s = new Student("张三"); 25 s.work(); // 张三在学习
5、接口的扩展
1 /** 2 * 通过 extends 来实现接口的扩展 3 * 在实现接口的时候,接口的扩展接口也必须重新定义 4 */ 5 6 interface Animals { 7 name: string, 8 } 9 10 interface Person extends Animals { 11 age: number, 12 } 13 14 let person:Person = { 15 name:"张三", 16 age: 18 17 }; 18 console.log(person); // {name: "张三", age: 18}
Typescript 泛型
在软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性,组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。
在 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据,这样用户就可以有自己的数据类型来使用组件。
通俗理解,泛型就是解决函数、类、接口 的复用性,以及对不特定数据类型的支持。
1、泛型函数
现在需要写一个方法实现,我们传入的参数和返回的参数保持一致,并且参数必须为 string 或者 number,按照正常的思维我们代码应该是这样:
1 function getData1(value: string): string { 2 return value; 3 } 4 5 function getData2(value: number): number { 6 return value; 7 } 8 9 // 返回 string 类型 10 getData1("abc"); 11 // 返回 number 类型 12 getData2(123);
在上面的代码中,我们需要定义两个不同的方法来分别实现 string 和 number 类型数据的返回,这样会造成大量重复代码,当然我们可以将返回类型变为 any 类型来解决,如下:
1 function getData(value: any): any { 2 return value; 3 } 4 5 // 返回 string 类型 6 getData("abc"); 7 // 返回 number 类型 8 getData(123);
使用 any 类型可以解决我们的问题,但是其实是放弃了类型检验,和普通 js 就没有什么区别了。
接下来我们来看一下泛型是如何解决该问题的:
1 /** 2 * 定义泛型类型 T 3 * 我们需要 T 是什么类型 4 * 直接定义 T 的类型即可 5 * @param value 6 */ 7 function getData<T>(value: T): T { 8 return value; 9 } 10 11 // 返回 string 类型 12 getData<string>("abc"); 13 // 返回 number 类型 14 getData<number>(123); 15 16 // 错误写法 17 // getData<string>(456); // 报错 rgument of type '456' is not assignable to parameter of type 'string'.
在上面的代码中,我们通过 T 来定义方法的泛型,这样方法在传入时需要先定义参数的类型,然后定义返回数据类型,这样就实现了上面的问题。
2、泛型类
现在有这样一个问题,我们要定义一个类,该类中 toString 方法返回值为一个二维坐标系上的某个点,按照之前我们讲的类的内容,可以写出如下代码:
1 class Point { 2 x: number; 3 y: number; 4 5 constructor(x: number, y: number) { 6 this.x = x; 7 this.y = y; 8 } 9 10 toString() { 11 return "(" + this.x + "," + this.y + ")"; 12 } 13 } 14 15 let p = new Point(1, 2); 16 // 坐标系点 (1,2) 17 console.log(p.toString()); // (1,2)
上面的代码可以实现我们想要的功能,但是现在又有新需求,坐标系上的点可以为 string 类型的数据,入(一,二),而我们在定义传入 x,y 时候已经定义了它传入的类型必须是 number 类型,如果向函数泛型那样定义 any 类型的话其实就失去了类型校验,跟普通的 js 没有什么区别了,这时候就需要泛型来解决了,如下:
1 /** 2 * Point 类的泛型 T 3 * Point 需要传入什么参数类型 4 * 就让 T 是什么类型即可 5 */ 6 class Point<T> { 7 x: T; 8 y: T; 9 10 constructor(x: T, y: T) { 11 this.x = x; 12 this.y = y; 13 } 14 15 toString() { 16 return "(" + this.x + "," + this.y + ")"; 17 } 18 } 19 20 let p1 = new Point<number>(1, 2); 21 // 坐标系点 (1,2) 22 console.log(p1.toString()); // (1,2) 23 24 let p2 = new Point<string>("一", "二"); 25 // 坐标系点 (一,二) 26 console.log(p1.toString()); // (一,二)
在上面的代码中,我们通过 T 实现了 Point 类型的泛型,Point 需要传入什么类型的参数,我们就将 T 定义成什么类型的数据即可。
3、泛型接口
还是上面的问题,不过这次我们是将 Point 写成接口的形式,如下:
1 interface Point { 2 (x: number, y: number): string 3 } 4 5 let getPoint: Point = (x, y): string => "(" + x + "," + y + ")"; 6 7 console.log(getPoint(1, 2)); // (1,2)
在上面的代码中,我们将 x 和 y 定义为 number 类型,这样在传入参数时就只能是 number 类型了,按照上面泛型函数和泛型类的方法,我们将 number 变为 T,如下:
1 interface Point<T> { 2 (x: T, y: T): string 3 } 4 5 let getPoint1: Point<number> = (x, y): string => "(" + x + "," + y + ")"; 6 // 坐标系点 (1,2) 7 console.log(getPoint1(1, 2)); // (1,2) 8 9 let getPoint2: Point<string> = (x, y): string => "(" + x + "," + y + ")"; 10 // 坐标系点 (一,二) 11 console.log(getPoint2("一", "二")); // (一,二)
通过 T 我们将 Point 接口传入的参数 x,y 进行泛型,这样我们就可以根据自己的需求传入我们想要的参数类型了。
TypeScript 模块
TypeScript 模块的设计理念是可以更换的组织代码。
模块是在其自身的作用域里执行,并不是在全局作用域,这意味着定义在模块里面的变量、函数和类等在模块外部是不可见的,除非明确地使用 export 导出它们。类似地,我们必须通过 import 导入其他模块导出的变量、函数、类等。
两个模块之间的关系是通过在文件级别上使用 import 和 export 建立的。
模块使用模块加载器去导入其它的模块。 在运行时,模块加载器的作用是在执行此模块代码前去查找并执行这个模块的所有依赖。 大家最熟知的JavaScript模块加载器是服务于 Node.js 的 CommonJS 和服务于 Web 应用的 Require.js。
此外还有有 SystemJs 和 Webpack。
模块导出使用关键字 export 关键字,要在另外一个文件使用该模块就需要使用 import 关键字来导入。
接下来我们就来实现一下:
在上图中我们通过 module.ts 中 export 关键字导出我们封装好的属性和方法,在 test.ts 中我们就可以使用 import 关键字来导入我们想要的属性和放大了。
我们也可以通过 export default 来导出模块,如下
但是 export default 在模块导出只能引用一次,导入时也不能使用 { },而是直接导入模块即可。
Typescript 命名空间
在代码量较大的情况下,为了避免各种命名冲突,可将相似功能的函数,类,接口等放置到命名空间内。同 Java 的包,.net 的命名空间一样,Typescript 的命名空间可以将代码包括起来,只对外暴露需要在外部访问的对象,命名空间内的对象通过 export 导出。1 /** 2 * 通过 namespace 来定义命名空间 3 *分别定义了 A 和 B 两个命名空间 4 * 我们可以根据需求来调用 A 或 B 里面类和方法 5 */ 6 namespace A { 7 interface Person { 8 name: string, 9 10 work(): void, 11 } 12 13 export class Student implements Person { 14 name: string; 15 16 constructor(name: string) { 17 this.name = name; 18 } 19 20 work() { 21 console.log(`${this.name}在学习`) 22 } 23 } 24 } 25 26 namespace B { 27 interface Person { 28 name: string, 29 30 work(): void, 31 } 32 33 export class Student implements Person { 34 name: string; 35 36 constructor(name: string) { 37 this.name = name; 38 } 39 40 work() { 41 console.log(`${this.name}在写作业`) 42 } 43 } 44 } 45 46 let a = new A.Student("张三"); 47 a.work(); // 张三在学习 48 let b = new B.Student("李四"); 49 b.work(); // 李四在写作业
空间命名用起来很简单,只需要通过 namespace 关键字来命名空间即可。
1 /** 2 * 类小黄使其在声明之前被声明(紧靠着类声明) 3 * 类装饰器应用于类构造函数 4 * 可以用来监视,修改或替换类定义 5 */ 6 function log(params: any) { 7 console.log(params); // ƒ Person() {} 8 params.prototype.name = "动态属性"; 9 params.prototype.func = () => { 10 console.log("动态方法"); 11 } 12 } 13 14 @log 15 class Person { 16 17 } 18 19 let p: any = new Person(); 20 console.log(p.name); // 动态属性 21 p.func(); // 动态方法
在上面的代码中,我们通过 @log 的形式来定义装饰器,在 log 方法中传入一个 params 的参数,通过打印我们发现它就是 Person 方法了,那么我们就可以通过原型链 prototype 的形式添加属性和方法了。
上面@log 并没有传参,接下来我们看一下在调用装饰器的时候传入我们想要的参数:
1 /** 2 * 在 log 方法中,我们通过 return 方式 3 * 将该方法返回,那就跟无参装饰器一样了 4 * 这样我们就可以在 log 方法中传参了,如 params 5 */ 6 function log(params: string) { 7 return function (target: any) { 8 console.log(params); // 张三 9 console.log(target); // f Person() {} 10 target.prototype.name = params; 11 target.prototype.work = function (): void { 12 console.log(`${params}在工作`) 13 } 14 } 15 } 16 17 @log("张三") 18 class Person { 19 20 } 21 22 let p: any = new Person(); 23 console.log(p.name); // 张三 24 p.work(); // 张三在工作
1 /** 2 * 属性装饰器表达式会在运行时当作函数被调用 3 * 传入下列 2 个参数 4 * 1、对于静态成员来说是类的构造函数,对于实例成员是类的原型对象 5 * 2、成员的名字 6 */ 7 function log(param: any) { 8 return function (target: any, key: string) { // 分别传入 2 个参数 9 console.log(param); // 张三 10 console.log(target); // {constructor: ƒ} 11 console.log(key); // name 12 target[key] = param; 13 } 14 } 15 16 17 class Person { 18 @log("张三") 19 name: string; 20 21 constructor() { 22 } 23 24 getName(): void { 25 console.log(this.name); 26 } 27 } 28 29 let p: any = new Person(); 30 console.log(p.name); // 张三 31 p.getName(); // 张三
在上面的代码中,我们在 name:string 上面添加了 @log 的装饰器,这就是属性装饰器,同时我们对装饰器进行了传参“张三”,我们将 log 方法返回,通过打印发现返回的方法的第一个参数 target 为 constructor 构造器,第二个参数为 name,那我们就可以根据 target[key] 的形式对 name 属性进行赋值了。
1 /** 2 * 方法装饰器会被应用到方法的属性描述符上 3 * 用来修改,监视和替换方法定义 4 * 方法装饰器在运行时传入下列三个参数 5 * 1、对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象 6 * 2、成员的名字 7 * 3、成员的属性描述符 8 */ 9 function log(param: any) { 10 return function (target: any, key: any, description: any) { // 分别传入三个参数 11 console.log(param); // 张三 12 console.log(target); // {getName: ƒ, constructor: ƒ} 13 console.log(key); // getName 14 console.log(description); // {writable: true, enumerable: true, configurable: true, value: ƒ} 15 console.log(description.value); // ƒ () {console.log(this.name) } 16 target.age = 20; // 添加属性 17 target.work = () => console.log(`${param}在工作`); // 添加方法 18 // 修改 getName 方法 19 description.value = ():void => console.log(`${param}---我是被修改的 getName 方法`); 20 } 21 } 22 23 24 class Person { 25 name: string; 26 27 constructor() { 28 } 29 30 @log("张三") 31 getName(): void { 32 console.log(this.name); 33 } 34 } 35 36 let p: any = new Person(); 37 console.log(p.age); // 20 38 p.work(); // 张三在工作 39 p.getName(); // 张三---我是被修改的 getName 方法
4、方法参数装饰器
1 /** 2 * 方法参数装饰器会在运行时当中函数被调用 3 * 可以使用参数装饰器为类的原型增加一些元素数据 4 * 传入下列 3 个参数 5 * 1、对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象 6 * 2、方法的名字 7 * 3、参数在函数参数列表中的索引 8 */ 9 function log(param: any) { 10 return function (target: any, methodName: any, index: any) { // 分别传入三个参数 11 console.log(param); // name 12 console.log(target); // {getData: ƒ, constructor: ƒ} 13 console.log(methodName); // getData 14 console.log(index); // 0 15 target.age = 18; 16 target.work = (): void => console.log(`我在工作`) 17 } 18 } 19 20 21 class Person { 22 constructor() { 23 } 24 25 getData(@log("name") name: string): void { 26 console.log(name); 27 } 28 } 29 30 let p: any = new Person(); 31 p.getData("张三"); // 张三 32 console.log(p.age); // 18 33 p.work(); // 我在工作
方法参数装饰器跟其他装饰器不同的是需要将 @log 写在方法的参数内,比较怪,一般情况下方法装饰器就能解决方法参数装饰器所实现的功能,所以一般我们使用方法参数器即可。
装饰器的执行顺序:属性装饰器>方法装饰器>方法参数装饰器>类装饰器,如果有多个相同装饰器,则先执行后面的装饰器。