1. 1.1. 简介
TypeScript是一种由微软开发的自由和开源的编程语言,它是JavaScript的一个超集,扩展了JavaScript的语法TypeScript如它的名字一样,最广为人知的特点是增加了类型系统,是一门强类型语言
相比弱类型语言,丢失了灵活性,但在编写大型项目时会降低程序复杂度,在编译时发现问题,减少bug增强程序健壮性
1.1.1. 特性
兼容ES6
强类型
枚举
泛型
类
接口
装饰器
1.1.1.1. 资料
官网
中文网
1.1.1.2. 使用说明
typeScript无法直接在浏览器当中运行,所以使用ts需要先安装对应的编译器
这个编译器的作用类似与我们之前使用less或sass编译器,它可以把ts转换为浏览器支持的标准语法
1.1.1.3. 编译器安装
这里安装需要确保本地已经拥有npm包管理工具
# 安装命令
npm install -g typescript
# 检测命令
tsc -v
1.1.1.4. 编译器使用
创建holle.ts文件,typescript编写的脚本使用ts作为后缀名,这个与less/sass也一样
运行编译命令: tsc hello.ts, 就会生成对应的es5js文件
class Person {
public name: string;
public age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
speak(): void {
console.log(`${this.name}今年${this.age}岁了`);
}
}
let xiaoming = new Person('小明', 16);
xiaoming.speak();
1.1.1.5. 配置文件使用
在使用编译ts脚本时, 有时候需要我们明确指定一些编译规则
比如要指定编译后的js为es3还是es5还是es6, 或者启用处于实现阶段的新特性, 比如装饰器
都需要我们通过tsconfig.json配置文件来指定
2. 类型
2.1. 为什么要类型
使用typeScript的类型定义可以让工具更早的帮我们分析出潜在bug与隐患
下面通过一个小例子给大家演示类型的好处与弊端
2.1.1. javascript
假如我们做一个加法运算,求两数之和
如果没有类型检测,我们很有可能会不小心和一个字符串相加,从而产生bug
let a = 10;
let b = '20';
let c = 30;
let sum = a + b + c; // 102030
2.1.1.1. typescript
如果使用typescript编写上面的代码,然后进行编译
你会发现详细的错误提示,这样显著提升了程序的健壮性
当然缺点也是有的,即失去了js原本的简洁性,增加编码量
let a: number = 10;
let b: string = '20';
let c: number = 30;
let sum: number = a + b + c;
2.2. 基本类型
2.2.1. number类型
数字类型的变量的只能赋值为数字,不区分整数与小数
let num: number = 10;
num = 20; // 正常
num = 'abc'; // 报错
2.2.1.1. string类型
字符串类型的变量的只能赋值为字符或字符串
let str: string = 'abc';
str = 'cba'; // 正常
str = 123; // 报错
2.2.1.2. boolean类型
字符串类型的变量的只能赋值为true或false
let bol: boolean = true;
bol = false; // 正常
bol = 1; // 报错
2.2.1.3. undefined类型
undefined类型的变量只能赋值为undefined
let undef: undefined;
undef = undefined; // 正常
undef = ''; // 报错
2.2.1.4. null类型
null类型的变量只能赋值为null
let empty: null;
empty = null; // 正常
empty = 0; // 报错
2.2.1.5. 组合类型(也叫联合类型)
ts为了适当保留js开发的灵活性, 提供了组合类型, 让变量可以赋值为多种类型
let num: (number | string);
num = 123; // 正常
num = '456'; // 正常
num = true; // 报错
2.2.1.6. any类型
如果组合类型不能满足你, 那么可以选择any任意类型, 不过使用any后你就失去了ts的类型检查
let data: any;
num = 123; // 正常
num = '456'; // 正常
num = true; // 正常
2.2.1.7. 补充
默认情况下undefined与null可以赋值给其他类型, null又可以赋值给undefined类型
如果想严谨一些, 可以配置编译选项strictNullChecks为true
let num: number;
let str: string;
let bol: boolean;
num = undefine; // 正常
str = null; // 正常
bol = null; // 正常
2.3. 数组类型
2.3.1. 基本类型数组
在基本数据类型后面添加[]即可表示对应类型的数组
let numArr: number[] = [1, 2, 3];
let strArr: string[] = ['a', 'b', 'c'];
let bolArr: boolean[] = [true, false];
let undifArr: undefined[] = [undefined];
let nulArr: null[] = [null];
2.3.1.1. 组合类型数组
let arr1: (number|string)[] = [1, 2, 'abc', 'cba', 4, 5];
let arr2: (number|boolean)[] = [1, 2, true, false];
2.3.1.2. 任意类型数组
let anyArr: any[] = [1, 'abc', true, null, undefined];
2.4. 函数
2.4.1. 参数与返回值类型
声明形参类型的语法与变量一样,都是 变量名:类型
声明返回值类型的语法需要写在小括号的后面 ():类型
function sum(a: number, b: number): number {
return a + b;
}
sum(10, 20); // 正常
sum('a', 30); // 报错
2.4.1.1. 参数个数
在typescript中会严格检查函数调用时传参的个数是否符合要求
function sum(a: number, b: number): number {
return a + b;
}
sum(10, 20); // 正常
sum(5); // 报错
sum(5, 8, 10); // 报错
2.4.1.2. 可选参数
如果某些参数是可选的, 可以使用?号进行标识, 也可采用es6的默认值语法
注意: 可选参数务必定义在形参的最后
function sum(a: number, b?: number, c: number = 50): number {
return b? a + b: a;
}
sum(5); // 正常
sum(5, 8, 10); // 正常
sum(5, 8, 10, 20); // 报错
2.4.1.3. 任意参数与类型
可以通过es6语法接收任意多参数, 并通过数组方式定义参数的类型
function sum(...arg: number[]): number {
return arg.reduce((sum, v) => {
return sum + v;
}, 0);
}
sum(1, 2, 3); // 正常
sum(1, 2, 3, 4, 5); // 正常
sum(1, 2, 3, 4, 'abc'); // 报错
2.4.1.4. 无返回值
如果函数没有返回值, 那么请用void来标识
function log(msg): void {
console.log(msg);
return 10; // 报错
}
2.4.1.5. 异常
如果某些函数内部一定会抛出错误, 或者永远不会执行完毕, 请用never来表示
function error(msg: string): never {
msg = `您的程序运行出现异常, 错误信息为: ${msg}`;
throw new Error(msg);
}
2.5. 枚举类型
枚举是typeScript扩展的一种数据类型, 这种类型可以用来存储有限且不可变的数据
2.5.1. 语法与特性
枚举类型需要使用新的关键字enum来定义, 语法有点类似与类或接口, 但是作用完全不同
枚举类型也算一种对象类型, 定义时首字母最好大写
枚举中元素的key必须是字符串, value可以是字符串或数字
enum Gender { man = '男', woman = '女' };
enum Gender2 { man, woman }; // 如果不赋值, 那么会从0开始给他们赋默认值
// 通过key取值
console.log(Gender.man); // 男
console.log(Gender2.man); // 0
// 报错, 值不可变
Gender.man = '公';
Gender2.man = 10;
2.5.1.1. 特殊特性
如果元素的value为数字, 那么可以反向获取key
// 默认的数字值从0开始, 这里我们也自己指定
enum Directions { 左 = 37, 上 = 38, 右 = 39, 下 = 40 };
console.log(Directions[37]); // 左
console.log(Directions[38]); // 上
// 指定value为字符串
enum Directions { 左 = 'left', 上 = 'top', 右 = 'right', 下 = 'bottom' };
console.log(Directions.left); // 报错, 不能通过字符串反向获取key
console.log(Directions.top); // 报错, 不能通过字符串反向获取key
2.6. 作用
2.6.1. 作用1 - 限制变量的赋值范围
假如我有一个color变量, 要求这个变量的值只能为'红', '黄',' 蓝', '绿', '紫', 不能为其他
像这样的需求我们可以使用枚举来解决
// 先把有限的值定义成枚举类型, 并给每个值取个名字
enum Color {red = '红', yellow = '黄', blue = '蓝', green = '绿', purple = '紫'};
// 然后定义一个变量, 指明这个变量只能存储Color里面的东西
let color: Color = Color.red;
color = Color.yellow; // 正常
color = '白'; // 报错
color = '黑'; // 报错
color = '紫'; // 报错, 必须用Color给其赋值
2.6.1.1. 作用2 - 定义固定数据, 拟补const不足
我们知道enum定义的数据内容是不可变的, 它的表现与const不同
如果使用const变量保存一个对象, 那么就不能给变量赋新值, 但是却可以给变量存储的对象的属性赋新值
所以要是有一些固定不变的数据, 比如: 周一到周日, 中国所有省, 四季, 人种等等, 我们都可以使用enum来定义
// 他们的默认值为0, 1, 2, 3
enum Season { '春', '夏', '秋', '冬' };
// 枚举有个特性, 如果值为数字, 可以反向取值, 利用这个特性我们可以遍历取出所有值
for(let i = 0; i < 2; i++) {
console.log(Gender[i]);
}
2.7. 面向对象
2.7.1. 字面量对象
在typeScript中,无法给对象动态添加属性,必须预先定义
let obj = { a: 1 };
obj.a = 10; // 正常
obj.b = 20; // 报错
2.7.1.1. class中的实例属性
ts不允许在构造器中动态给实例添加属性, 必须要先进行声明
class Person {
name: string; // 定义实例属性name及类型
age: number = 0; // 定义实例属性时可以赋一个默认值
constructor(name: string, age: number, gender: string) {
this.name = name; // 正常
this.age = age; // 正常
this.gender = gender; // 报错,没有预先定义,无法动态添加属性
}
}
2.7.1.2. class中的实例属性 - 简写版
在进行面向对象开发时, 会经常在new的时候传值, 上面那样预先定义再赋值的写法虽严禁但实在过于重复
所以ts提供了一种简写形式, 即在构造器形参上面添加权限修饰符, 加上后ts就知道这个形参是要被赋给实例的
class Person {
// 形参加上权限修饰符之后, 不用提前声明, 也不用写this.xxx = xxx这样的代码了, ts会自动添加
constructor(public name: string, public age: number, public gender: string) {}
}
let xiaohong = new Person('小红', 12, '女');
console.log(xiaohong.age); // 12
2.7.1.3. 静态成员
在es6中,只允许使用static关键字定义静态方法,不能定义静态属性
但是在ts中没有这个限制, 只要前面加上static关键字就是静态成员, 否则就是实例成员
class Person {
static maxAge: number = 250;
static getTotal() {
return '70亿';
}
}
// 静态成员通过类名调用
Person.maxAge; // 250
Person.getTotal(); // '70亿'
2.8. 访问权限修饰符
访问权限修饰符的作用是限定属性的可访问范围
访问修饰符与主流面向对象语言一样, 有三个: public private protected
2.8.1. public
只有public权限的属性或方法才可以在类外部使用
如果不明确指明权限的话, 那么默认就是public
class Person {
public name: string; // 手动赋予public权限
age: number; // 默认赋予public权限
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
public say(): void {
console.log(`我是${this.name}`);
}
}
let p = new Person('小虎', 20);
p.say(); // 我是小虎
p.name // 小虎
p.age // 20
2.8.1.1. private
只供类内部使用的属性或方法,可以通过private关键字来修饰
class Person {
public name: string;
private age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
public say(): void {
console.log(`我是${this.name},我${this.age}岁了`);
}
}
let p = new Person('小虎', 20);
p.say(); // 我是小虎,我20岁了
p.name // 小虎
p.age // 报错,age是私有属性,只能在类的内部使用
2.8.1.2. protected
只供类或子类内部使用的属性或方法,可以通过protected关键字来修饰
class Animal {
protected name: string;
private age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
class Person extends Animal {
public sayName(): void {
console.log(`我是${this.name}`); // 正常, 父中protected属性在父子类中均可使用
}
public sayAge(): void {
console.log(`我${this.age}岁了`); // 报错, 父中private属性只能在父中使用
}
}
let p = new Person('小芳', 18);
p.name // 报错, protected属性不能在外面使用
p.age // 报错, private属性不能在外面使用
2.9. 读写修饰符
2.9.1. readonly
ts提供了只读属性修饰符readonly, 属性一旦赋值便不可修改
注意: 只读修饰符不能作用与方法
class Person {
readonly name: string;
readonly age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
// 报错, 不能使用readonly修饰方法
readonly say() {
console.log('说一嘴');
}
}
let p = new Person('小美', 20);
p.name; // 小美
p.name = '大美'; // 报错, name是只读属性
2.9.1.1. 联合使用
读写修饰符允许配合一个权限修饰符共同使用, 但权限修饰符必须写在前面
class Person {
public readonly name: string;
private readonly age: number;
private protected grender: string;
}
2.10. 接口
接口就是契约, 可用来限定对象应该由哪些属性或方法构成
定义接口需要使用interface关键字
2.10.1. 作用
在编写大型项目时, 我们可能需要先设计一下这个项目的构成, 比如需要那些模块, 实现那些类, 每个类有那些功能
那么在设计的过程中我们可以使用代码进行记录与描述, 这些简单的代码就是接口
接口不会实现任何具体的业务逻辑, 它的作用就是用来规范或提醒将来如何实现
使用接口还有很多好处, 比如下面这个人使用接口后的评价 接口好处
2.10.1.1. 语法与特性
interface Person {
name: string;
age: number;
gender: string = '男'; // 报错, 接口里的属性不能赋值
say(): void;
study(): void;
run(): void{ console.log('跑') } // 报错, 接口里的方法不能实现
}
2.11. 使用范例
2.11.1. 用接口约束类
我们先使用interface设计一个接口, 里面描述某种类应该有的属性与方法, 然后我们再根据接口去实现对应的类
同时为了让工具能够自动帮我们分析实现的对不对,有没有缺失, 可以在类上使用implements关键接口
// 使用接口定义Person类应该有的属性与方法
interface PersonInterface {
name: string;
age: number;
eat(food: string): void;
}
// 当真正编写Person类时, 使用implements关联对应的接口, 以约束我们的实现
class CNPerson implements PersonInterface {
// 如果不添加name或age就会报错, 多添加不会报错
constructor(public name: string, public age: number, public gender: string) {}
// 不实现这个方法就会报错
eat(food: string): void {
console.log('我要吃${food}');
}
// 多实现不会报错
say(): void {
console.log(`我是${this.name}`);
}
}
2.11.1.1. 用接口约束字面量对象
我们在定义变量时, 如果想限制这个变量可存储的对象具体结构, 那么变量的类型可以为某个接口的名称
注意: 接口类型的变量, 必须严格按照接口的定义数据结构进行赋值,多了少了都不行
interface data {
a: number,
b: string
};
let obj: data = { a: 10, b: 'abc' }; // 正常, 这个对象符合接口data定义的数据结构
let obj2: data = { a: 10 }; // 报错, 缺失b属性
let obj3: data = { a: 10, b: true }; // 报错, b属性类型不对
let obj3: data = { a: 10, b: true, c:1 }; // 报错, 多了c属性
2.12. 泛型
2.12.1. 为什么要泛型
假设有一个方法,可以传入任意多个数字,组成数组并返回
function createArray(...arg: number[]): number[] {
return arg;
}
假设我们现在要改造这个函数,让它可以实现更多需求,变得更通用
改造需求是:传入的参数可以是数字,也可以是字符串等其他任意类型
但是必须保证所有的参数类型一致,同时返回该类型构成的数组
function createArray(...arg: any[]): any[] {
return arg;
}
你可能会像上面那样写,但显然,这不能保证参数的类型是一致的
2.12.1.1. 使用泛型
要解决上面的问题,泛型就派上用场了
泛型的使用方式是在函数的后面使用<>定义一个变量
这个变量的值由调用者传入一个描述数据类型的字符串, 比如string/number/boolean等
然后我们就可以使用这个变量来约束参数与返回值的类型
function createArray<Type>(...arg: Type[]): Type[] {
return arg;
}
let arr1: number = createArray<number>(10, 20, 30); // 正确
let arr2: string = createArray<string>('a', 'b', 'c') // 正确
let arr3: any = createArray<string>('a', 'b', 10) // 报错
2.12.1.2. 泛型在类中的应用
泛型除了可以应用在函数外, 还可以应用在类上
看下面的代码实现了一个缓存数据的类, 目前这个类对于缓存的数据没有要求
class Cache {
private data = {};
set(key: string, val: any) {
this.data[key] = val;
}
get(key: string): any {
return this.data[key];
}
}
let c: Cache = new Cache();
c.set('小红', 10);
c.get('小红');
假设现在需求变了, 要求每个缓存只能存储同一种类型的数据,
那可能你需要定义多个缓存类才能满足需求, 看下面的实现
class CacheNumber {
private data = {};
set(key: string, val: number) {
this.data[key] = val;
}
get(key: string): number {
return this.data[key];
}
}
class CacheString {
private data = {};
set(key: string, val: string) {
this.data[key] = val;
}
get(key: string): string {
return this.data[key];
}
}
有了泛型后, 就不用向上面那样大费周折了, 而且支持的类型更多, 理论上无限
class Cache<Type> {
private data = {};
set(key: string, val: Type) {
this.data[key] = val;
}
get(key: string): Type {
return this.data[key];
}
}
let c = new Cache<string>();
c.set('小红', 10); // 报错, val必须为字符串
c.set('小红', '10'); // 正常
// 如果变量在定义便赋了值, 那么ts会自动推导变量的类型
// 如果要手动写类型的话, 必须声明一致的泛型类型, 看下面例子
let c2: Cache = new Cache<number>(); // 报错
let c3: Cache<string> = new Cache<number>(); // 报错
let c3: Cache<number> = new Cache<number>(); // 正常
2.12.1.3. 使用泛型定义数组
之前我们在定义数组类型时是这样写的
let arr: number[] = [];
arr.push(10); // 正常
arr.push('abx'); // 报错
学习了泛型之后还可以这样写
let arr: Array<number> = [];
arr.push(10); // 正常
arr.push('abx'); // 报错
3. 装饰器
3.1. 简介
装饰器的作用主要是用来修饰类行为的, 这就像它的名字一样, 给别人来个锦上添花
这个特性同样来自其他语言, 目前ECMA也有一个提案要把这项功能融入js当中, 未来有很大可能被正式采纳
3.1.1. 编译说明
由于装饰器还没有正式成为ECMA标准, 未来可能还会有些许变化
所以要在ts中使用它需要添加experimentalDecorators编译选项
同时还要指明编译后的ES版本大于等于5, 因为装饰器会用到ES5中的特性, 否则转换后的代码会有问题
使用编译命令: tsc 文件名.ts --target ES5 --experimentalDecorators
或者先写好配置文件, 然后直接运行tsc命令, 根据配置文件进行编译
3.1.1.1. 使用说明
目前装饰器只能应用与类/实例与静态成员身上
应用在类上, 装饰器函数可以自动接收一个参数: 类
应用在静态方法上, 装饰器函数可以自动接收三个参数: 类 属性名 描述对象
应用在静态属性上, 装饰器函数可以自动接收两个参数: 类 属性名
应用在实例方法上, 装饰器函数可以自动接收三个参数: 原型 属性名 描述对象
应用在实例属性上, 装饰器函数可以自动接收两个参数: 原型 属性名
3.2. 初探装饰器
装饰器本质就是一个函数,内部封装了一些用以修饰别人的公共处理逻辑,
装饰器的语法就是在要修饰的东西前面使用@符号调用函数
3.2.1. 装饰器语法
function decoratorFn(...arg: any[]) {
console.log('我执行了, 并且我自动收到了下面的东西!');
console.log(arg);
}
// 写法1: 在类的前一行使用装饰器
@decoratorFn
class Test {}
// 写法2: 在类前面的使用装饰器
@decoratorFn class Test2 {}
3.2.1.1. 装饰属性与方法
function decoratorFn(...arg: any[]) {
console.log('我执行了, 并且我自动收到了下面的东西!');
console.log(arg);
}
class Test {
@decoratorFn
static staticProp: string = '静态属性';
@decoratorFn
static staticFn(): void {
console.log('静态方法');
}
@decoratorFn
instanceProp: string = '实例属性';
@decoratorFn
instanceFn(): void {
console.log('实例方法');
}
}
3.3. 装饰器作用
3.3.1. 装饰类
在类上使用装饰器, 装饰函数会自动接收到一个值: 被装饰的类
拿到类后我们可以修改类的一些东西, 比如静态属性
// 增强类的静态成员
function addFeature(target: any): void {
let newFeature = ['拉', '撒', '睡'];
if(target.feature) {
target.feature.push(...newFeature);
}
}
@addFeature
class Person {
static feature: string[] = ['吃', '喝'];
}
@addFeature
class Cat {
static feature: string[] = ['吃', '喝'];
}
console.log(Person.feature); // ['吃', '喝', '拉', '撒', '睡']
console.log(Cat.feature); // ['吃', '喝', '拉', '撒', '睡']
3.3.1.1. 装饰器优点
装饰器的好处是在无需做任何变动的情况下实现了功能增强
装饰器也是复用的另外一种形式, 装饰器的特点是目的性很强, 在多人项目中很容易让别人看懂你的意图
下面是不用装饰器的代码, 仍然可以实现相同的功能, 但无论是代码还是语义, 都不如装饰器优雅
function feature(target: any): void {
target.feature = ['吃', '喝', '拉', '撒', '睡'];
}
class Person {}
class Dog {}
// 实现了和装饰器同样的效果, 但是写起来明显不如装饰器语法来的简单
// 同时语义上无法直观的看出调用feature方法是为了增强类, 谁知道feature内部干了什么
feature(Person);
feature(Dog);
3.3.1.2. 装饰方法
在方法上使用装饰器, 装饰函数会自动接收到三个值: (类|原型) 方法名 描述对象
静态方法第一个参数为类, 实例方法第一个参数为原型
拿到这些值后, 我们可以修改或者替换方法定义
function executeTime(target: any, name: string, msg: any): void {
// 缓存旧方法
let oldFn = msg.value;
// 赋值新方法, 新方法对旧方法进行了功能扩展
msg.value = function(...arg: any[]) {
let date = Date.now();
oldFn.call(this, ...arg);
console.log(`执行了: ${Date.now() - date} 毫秒`);
}
}
class Person {
@executeTime
static getTotal(): void {
console.log('70亿');
}
@executeTime
study(time: number): void {
for(let i =0; i < time; i++) {
console.log('学习');
}
}
}
new Person().study(1000);
Person.getTotal()
3.4. 装饰器工厂 - 可传参的装饰器
装饰器工厂的写法就是函数返回另一个函数, 这种写法好处是可以接收用户传递的参数
这样我们就可以根据用户的参数进行个性化定制
3.4.1. 语法
// 外层的函数可收到用户传递的参数
function test(param: string): any {
console.log(param);
// 内层函数可收到装饰器自动传递的参数, 说白了就是以前的装饰器函数
return function(target: any): void {
console.log(target);
}
}
// 这里的装饰器语法相比以前多了小括号调用与传参
@test('可以传参?')
class Person {}
3.4.1.1. 范例
function feature(level: string): any {
return function(target: any): void {
// 高等生物
if(level === '高等') {
target.feature.push('玩', '乐');
}
// 低等生物
else if(level === '低等') {
target.feature.push('拉', '撒');
}
}
}
@feature('高等')
class Person {
static feature: string[] = ['吃', '喝'];
}
@feature('低等')
class Dog {
static feature: string[] = ['吃', '喝'];
}