TypeScript学习笔记

1 简介

TypeScript是由微软推出的编程语言,是javascript的超集,遵循ES5、ES6规范,拓展了js的语法

TypeScript更像是java等后端语言,可以开发大型企业项目,最新的前端框架基本都是使用了TS

2 安装

npm install -g typescript

cnpm以及yarn安装方式一样

3 编译及配置

3.1 编译

浏览器不识别ts代码,需要将ts代码编译为ES5的代码

新建index.ts脚本,编写代码

console.log("hello TS");

使用 tsc index.ts 命令进行编译

image-20200716112303468

编译完成之后会生成index.js脚本,内容是

image-20200716112435674

这一段代码看不出差别,来一段ts语法的代码

let str:String = 'feng';
console.log(str);

再来看看编译出来的js脚本

image-20200716112635829

3.2 在vscode中配置自动编译

每次写完代码手动保存很不方便,可以使用配置文件自动生成js

在根目录下运行命令 tsc --init生成配置文件tsconfig.json

image-20200716113444368

outDir一行的注释打开,目录修改为要输出到的目录,不修改也可

点击选项卡终端->运行任务->typescript->tsc监视,然后再编写ts代码保存时就可以自动生成js了

4 数据类型

ts为了使编写代码更规范增加了类型校验,ts主要提供了以下几种类型的数据

  • 布尔类型(boolean)
  • 数字类型(number)
  • 字符串类型(string)
  • 数组类型(array)
  • 元组类型(tuple)
  • 枚举类型(enum)
  • 任意类型(any)
  • null 和 undefined
  • void类型
  • never类型

4.1 布尔类型

在变量标识符之后声明变量类型为boolean,那么这个变量只能接受布尔类型的赋值,如果赋值为其他类型的数据,在编译时会报错,例如

let isReal: boolean = 123;

定义布尔类型的变量isReal,然后为其赋值数字123,在编译时会抛出下面错误

image-20200716211052101

意思是’123’不能赋值给boolean类型的变量

4.2 数值类型

规则同布尔类型,声明的标识符是number

整形和浮点型都可以用number声明

4.3 字符串类型

声明的标识符是string

4.4 数组类型

声明方式有两种:

  • 使用中括号

    let arr:number[] = [1,2,3]
    // 数字类型的数组不能出现其它数据类型
    
  • 使用泛型声明

    let arr: Array<number> = [1, 2, 3]
    

4.5 元组类型

let tuple:[string, number, boolean] = ['feng', 11, true]

元组也是一种数组,可以指定数组的每一项的数据类型

4.6 枚举类型

枚举类型用于声明一组命名的常数,当一个变量有几种可能的取值时,可以将它定义为枚举类型。

枚举类型可以手动设置值

enum Flag {
    success = 1,
    error = -1
}
let f: Flag = Flag.success
console.log(f);// 1

// 另外一种手动设置的方式,只定义第一项,后面的依次递增
enum Animal {
    cat = 2,
    dog,
    snake
}
var a: Animal = Animal.snake;
console.log(a); // 4
// 自定义值可以在任意位置,上面的代码汇中如果只定义dog=2,那么snake就是3

也可以使用默认值,默认值就是索引值

enum Color {
    blue,
    red,
    green
}
const c: Color = Color.blue;
console.log(c); // 0

4.7 任意类型

在上面的基本类型中,一旦定义了类型就不能再赋值其他类型的数据,否则就会报错

那如果想给一个变量赋值多种类型的数据就需要用到any来声明变量了

在ts文件中写入以下代码

var node = document.getElementById('box');
node.style.color = 'red';

保存时会抛出以下错误

image-20200724193926020

编译之后代码是可以运行的,但是存在问题

意思是node是一个Object类型,这时再给node声明Object类型

var node:Object = document.getElementById('box');
node.style.color = 'red';

image-20200724194334541

这时还会报错,因为进本数据类型中没有Object,这时就可以使用any类型了

var node: any = document.getElementById('box');
node.style.color = 'red';

4.8 null和undefined

null和undefined是其他数据类型的子类

undefined

声明变量是undefined类型之后,变量undefined不会报错,例如

image-20200724195304416

但如果定义了undefined类型就不会报错

image-20200724195337191

undefined一般用于联合类型

var num: number | undefined;
num = 123;
console.log(num);

num可以是number类型,也可以是undefined

null

变量声明null之后只能为空值,如果为变量赋值就会报错

image-20200724195605318

等于空值时不会报错

image-20200724195641544

4.9 void类型

用于定义没有返回值的方法

((): void => {
    console.log('void');
})()

lambda表达式的写法

4.10 never类型

其他类型,表示从不会出现(包括null和undefined的子类型),被never声明的变量只能被never类型的数据赋值

5 函数

5.1 声明函数

5.1.1 命名函数

在TypeScript中声明函数时,要声明参数类型以及函数返回值类型

没有返回值使用void进行声明

function run(): string {
    return 'string'
}

在声明函数返回值类型之后,返回值的类型必须是声明的类型,否则报错

image-20200803114930092

5.1.2 匿名函数

在TypeScript中定义匿名函数也需要声明返回值类型

var func2 = function (): number {
    return 123;
}

同样,返回值需要符合声明的返回值类型,不一致则会报错

5.2 传参

定义一个方法,接收一个firstname和一个lastname,返回一个fullname

function getName(firstname: string, lastname: string): string {
    return `${firstname}--${lastname}`
}

console.log(getName('Chan', 'Jack'));
// Chan--Jack

匿名函数同样的在参数后声明类型

5.2.1 可选参数

在func3中,有两个参数,现在调用func3,只传一个参数

function func3(name: string, age: number): void {
    console.log(name + "--" + age);

}
func3('zhangsan');

编译时会报错,原因是少了一个参数

image-20200803232523051

在可选参数后添加?,表示参数可以有也可以没有

可选参数必须放到参数的最后面

5.2.2 默认参数

在ES5中不可以设置默认参数,在ES6和TS中可以设置默认参数

function func3(name: string, age: number=20): void {
    console.log(name + "--" + age);

}
func3('zhangsan');
// zahngsan--20

当默认参数在最后时,设置默认参数之后可以不传参数,使用默认的值,当默认参数在前面时必须传参数

5.2.3 剩余参数

当不确定参数的数量时,可以使用ES6的扩展运算符(...

function getSum(...args: number[]): void {
    var result: number = 0;
    args.forEach(element => {
        result += element
    });
    console.log(result);

}
getSum(1, 2, 3, 4)		// 10
getSum(1, 2, 3, 4, 5)	// 15

也可以固定前几个参数,然后后面的用扩展运算符接收

function getSum2(a: number, ...args: number[]): void {
    var result: number = a;
    args.forEach(element => {
        result += element
    });
    console.log(result);

}
getSum2(1, 2, 3, 4)	// 10

5.3 函数重载

java中的重载:两个或两个以上同名函数,但是他们接收的参数不一样,这时会出现函数重载

TypeScript中的重载:通过为同一个函数提供多个函数类型定义来实现多种功能的目的

function getInfo(name: string): string;
function getInfo(age: number): string;
function getInfo(str: any): any {
    if (typeof str === 'string') {
        return '我叫' + str
    } else {
        return '我的年龄是' + str
    }
}
console.log(getInfo('zahngsan'));
console.log(getInfo(12));

来看下结果

image-20200804000503044

显然,ts在通过这种方式为一个函数实现了两种功能,但是,传参的类型必须在重载的函数参数之内,如果匹配不到就会报错

6 类

6.1 对比ES5

6.1.1 定义类

在ES5中使用构造函数创建对象,直接在function内部添加属性和方法

function Person() {
    this.name = 'zhangsan'
    this.getInfo = function () {
        console.log(this.name);
    }
}
var p = new Person();
p.getInfo();	// zhangsan

还可以通过原型链的方式给对象添加属性和方法

Person.prototype.age = 20;
Person.prototype.getAge = function () {
    console.log(this.age);
}
var p = new Person();
p.getAge();	// 20

但是,使用原型链添加的引用类型属性会被所有实例共享,意思就是当某一实例的该属性变化时,其他实例的属性都会改变

Person.prototype.friends = ['f1', 'f2'];
Person.prototype.getFriends = function () {
    console.log(this.friends);
}
var p = new Person();
p.getFriends();
var p2 = new Person();
p2.friends.push('f3');
p.getFriends();
p2.getFriends();

image-20200804094212109

可以看到,改变一个实例的引用类型的属性,另一个实例也变了;基本数据类型不存在这种问题

6.1.2 静态方法

ES5中给对象添加静态方法非常简单

Person.static = function () {
    console.log('静态方法');
}
Person.static();    // 静态方法

静态方法不需要实例化就可以调用,并且,对象实例不可以调用静态方法

6.1.2 继承

ES5中通过对象冒充来实现继承

function People() {
    Person.call(this);
}

var peo = new People();
peo.getInfo();	// zhangsan

此外也可以通过原型的方法来实现继承

function People() {

}
People.prototype = new Person();

var peo = new People();
peo.getInfo();

但是在实例化子类时无法给父类传参

function Person(name) {
    this.name = name;
    this.getInfo = function () {
        console.log(this.name);
    }
}
function People() {

}
People.prototype = new Person();

var peo = new People('zhangsan');
peo.getInfo();	// undefined

然后就有了原型链+构造函数的组合继承模式——寄生组合继承

function People(name) {
    Person.call(this, name)
}
People.prototype = Person.prototype;

var peo = new People('zhangsan');
peo.getInfo();	// zhangsan

6.2 TypeScript中的类

6.2.1 定义类

在TS中,使用class关键字来定义类,类需要实例化时需要添加构造器

class Person {
    name: string;	// 属性
    constructor(n: string) {         // 构造函数,实例化时触发
        this.name = n;
    }
    getName(): string {
        return this.name;
    }
    setName(name: string): void {
        this.name = name
    }
}

var p: Person = new Person('李四');
console.log(p.getName());	// 李四
p.setName('张三');
console.log(p.getName());	// 张三
拓展——params方式实例化
class Article {
    title: string | undefined;
    desc: string | undefined;
    status: number | undefined
    constructor(params: {
        title: string | undefined,
        desc: string | undefined,
        status?: number | undefined
    }) {
        this.title = params.title;
        this.desc = params.desc;
        this.status = params.status;
    }
}

var newArticle: Article = new Article({
    title: '大事件',
    desc: '都是小事'
})
console.log(newArticle);

通过向构造器传入json对象,然后使用索引值来实例化对象

6.2.2 继承

TS中继承有专门的关键字extends,需要配合super初始化父类构造函数来实现继承

class People extends Person {
    constructor(name: string) {
        super(name);
    }
}
var peo = new People('王五');
console.log(peo.getName());	// 王五

6.2.3 修饰符

  • public:公有,在类里面、子类、外部都可以访问
  • protected:保护,在类里面,子类中可以访问
  • private:私有,只有类里面可以访问

属性不加修饰符默认是public

6.2.4 静态属性&静态方法

使用static修饰符声明静态方法,但是静态方法无法直接获取类中的属性,只能获取静态属性(同样用static修饰)

class Person {
    name: string;
    static age: number = 20;
    constructor(n: string) {         // 构造函数,实例化时触发
        this.name = n;
    }
    getName(): string {
        return this.name;
    }
    setName(name: string): void {
        this.name = name
    }
    static log(): void {
        console.log(`年龄是${this.age}`);
    }
}
Person.log();	// 年龄是20

6.2.5 多态

父类定义一个方法不去实现,让继承他的子类去实现,每一个子类有不同的表现

class Animals {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    getInfo(): void {

    }
}
class Dog extends Animals {
    constructor(name: string) {
        super(name);
    }
    getInfo(): void {
        console.log(this.name + '是一只狗');

    }
}
class Cat extends Animals {
    constructor(name: string) {
        super(name);
    }
    getInfo(): void {
        console.log(this.name + '是一只猫');

    }
}
var cat: Cat = new Cat('kitty');
cat.getInfo();	// kitty是一只猫
var dog: Dog = new Dog('Tony');
dog.getInfo();	// Tony是一只狗

这段代码中有一个父类Animals,两个子类Dog和Cat分别继承Animals,Animals定义了一个getInfo方法,两个子类中分别重写了这个方法,实现了不同的功能

6.2.6 抽象类

提供其他类继承的基类,不能被实例化;用absort关键字定义抽象类和抽象方法,抽象类中的抽象方法不包含具体实现并且必须在派生类中实现;抽象方法只能放在抽象类中

abstract class AbstractAnimals{
    abstract func1(): void;
    abstract func2(): string;
}

class Mouse extends AbstractAnimals{
    func1(): void {
        console.log('function 1');
        
    }
    func2(): string {
        return 'function 2'
    }
    
}

7 接口

在面向对象程序设计中,接口是一种规范的定义,它规定了行为和动作的规范,起到了限制和规范的作用。接口定义了某一批类需要遵守的规范,不关心这些类内部的状态数据,也不关心方法实现细节,只规定类需要提供哪些方法。类似于java中的类,更加灵活。

定义接口关键字:interface

7.1 属性接口

属性接口就是对json格式的限制

interface FullName {
    firstName: string;
    lastName: string;
}
function getFullName(name: FullName) {
    return `${name.firstName}----${name.lastName}`
}
console.log(getFullName({ firstName: 'zhang', lastName: 'san' }));
// zhang----san

在这段代码中,定义了一个FullName接口,方法getFullName参数name要满足这个接口,如果不满足接口定义的规则就会报错

image-20200804173022419

并且,参数必须且只能有接口定义的数据项,如参数使用对象name,该对象必须包含接口定义的项并且符合数据类型限制

可以使用 ? 定义可选属性,用法同可选参数

7.2 函数类型接口

对方法传入的参数以及返回值进行约束

interface FuncRule {
    (key: string, value: string): string;
}
var funcIn: FuncRule = function (key: string, value: string): string {
    return `${key}---${value}`
}
console.log(funcIn('name', '张三'));

接口中规定了两个参数和他们的类型以及返回值的类型,实现这个接口的函数就必须有这两个参数和返回值。参数类型不符合规定就会报错

image-20200804202314697

7.3 可索引接口

对数组、对象的约束

7.3.1 对数组的约束

interface UserArr{
    [index:number]:string;
}
var arr:UserArr=['aa', 'bb']
console.log(arr[0])

接口规定了索引值是number类型,value是string类型(开始这里没看懂,数组的索引还能不是number???继续往下看)

7.3.2 对对象的约束

interface UserObj{
    [index:string]:string
}
var arr:UserObj = {
    name: 'alex'
}

这里明白了,对数组的约束和对对象的约束不是具体的分类,索引值是number就限定了数组,索引值是string就限定了对象

7.4 类类型接口

类类型约束和抽象有些类似,这里最接近于java中的接口类

// 接口
interface AnimalsInterface {
    name: string;
    eat(food: string): void;
}
// 实现类
class Tigger implements AnimalsInterface {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    eat(food: string): void {
        console.log(`${this.name}吃的是${food}。`);
    }
}
// 实例化
var t: Tigger = new Tigger('泰哥');
t.eat('猪肉');	// 泰哥吃的是猪肉。

在实现接口方法的时候,参数可以没有

7.5 接口扩展(接口继承)

interface Animals {
    eat(): void;
}
interface People extends Animals {
    name: string;
    work(): void;
}
class Chinese implements People {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    work(): void {
        console.log(this.name + '可以工作');
    }
    eat(): void {
        console.log(this.name + '吃大餐');
    }

}
var ming: Chinese = new Chinese('小明');
ming.work();

接口People继承了接口Animals,在实现People接口的时候,需要同时实现Animals中定义的内容,如果去掉了Animals中的方法就会报错

image-20200806110203632

8 泛型

泛型是程序设计语言的一种特性。允许程序员在强类型程序设计语言中编写代码时定义一些可变部分,那些部分在使用前必须作出指明。各种程序设计语言和其编译器、运行环境对泛型的支持均不一样。将类型参数化以达到代码复用提高软件开发工作效率的一种数据类型。泛型类]是引用类型,是堆对象,主要是引入了类型参数这个概念。

——百度百科《泛型》

8.1 泛型变量

先来看个例子

function getValue<T>(value: T): string {
    return '获得的值是' + value;
}
console.log(getValue<number>(12345));

定义方法时规定了泛型T,传参时使用泛型规定类型

image-20200806120229155

8.2 泛型类

java中的ArrayList就是这种东西

class ArrayList<T> {
    private list: T[] = [];
    add(item: T): void {
        this.list.push(item);
    }
    getList(): T[] {
        return this.list;
    }
}
var arr = new ArrayList<number>();
arr.add(1);
arr.add(4);
console.log(arr.getList());

image-20200806121802362

这里用number类型做了个例子,泛型可以是任意类型,类也可以

使用上文7.5中的Chinese类来示范

// 实例化对象
var ming: Chinese = new Chinese('小明');
var hong: Chinese = new Chinese('小红');

// 将对象添加进list
var arr = new ArrayList<Chinese>();
arr.add(ming);
arr.add(hong);
console.log(arr.getList());

image-20200806122342717

8.3 泛型接口

在定义接口时使用泛型,实现接口时相应的需要用泛型实现,调用方法时可以根据需要规定数据的类型

interface ConfigFn {
    <T>(value: T): T;   // 最后一个T是返回值类型,可以是其他的
}
var getData: ConfigFn = function <T>(name: T): T {
    return name;
}
console.log(getData<string>('张三'));

9 装饰器

装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,属性或参数上,可以修改类的行为。
通俗的讲装饰器就是一个方法,可以注入到类、方法、属性参数上来扩展类、属性、方法、参数的功能。
常见的装饰器有:类装饰器、属性装饰器、方法装饰器、参数装饰器
装饰器的写法:普通装饰器(无法传参) 、 装饰器工厂(可传参)

装饰器是过去几年中js最大的成就之一,已是ES7的标准特性之一

9.1 类装饰器

装饰器本身是一个函数,装饰器通过@符号来使用

function logClass(params: any) {
    params.prototype.name = '装饰器';
    params.prototype.run = function () {
        console.log('这是一个装饰器');
    }
}

@logClass
class Log {
    constructor() {

    }
    getData() {

    }
}
var log: any = new Log();	// 这里变量类型声明any
log.run();		// 这是一个装饰器

9.1.1 装饰器工厂

装饰器工厂可以传参,通过闭包的方式进行操作

function logClass(params?: any) {
    return function (target: any) {
        console.log(params);
    }
}

@logClass('get')
class Log {
    constructor() {
    }
}
var log: any = new Log();		// get

这里调用装饰器的时候方法立即执行

function logClass(params?: any) {
    return function (target: any) {
        target.prototype.pro = params;
    }
}

@logClass('post')
class Log {
    constructor() {
    }
}
var log: any = new Log();
console.log(log.pro);

上面这一段代码通过装饰器动态给类添加属性,在调用构造器的时候一定要加括号,参数可选时没有参数也要带括号

9.1.2 重载构造函数

  • 类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数;

  • 如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明;

function logClass(target: any) {
    return class extends target {
        url = 'newValue'
        getData() {
            console.log('getData:', this.url);
        }
    }
}
@logClass
class HttpClient {
    public url: string | undefined;
    constructor() {
        this.url = 'value'
    }
    getData() {
        console.log(this.url);
    }
}
var http = new HttpClient();
http.getData(); //getData: newValue

装饰器返回的就是HttpClient的子类,因此TS可以自动推导 http 的类型;

重载时必须将类里面的所有属性几方法重载

9.2 属性装饰器

属性装饰器表达式会在运行时当作函数被调用,传入两个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象;
  2. 成员的名字;
function LogProperty(params: any) {
    return function (target: any, attr: any) {
        console.log(target);
        console.log(attr);
    }
}
class User {
    @LogProperty('zhangsan')
    name: string;
    constructor() {
    }
}

image-20200806225107450

从图中可以看到属性装饰器传入的两个参数

function LogProperty(params: any) {
    return function (target: any, attr: any) {
        target[attr] = params;
    }
}
class User {
    @LogProperty('zhangsan')
    name: string | undefined;
    constructor() {

    }
    getName(): void {
        console.log(this.name);

    }
}
var u = new User();
u.getName();	// zhangsan

9.3 方法装饰器

方法装饰器被应用到方法的属性描述符上,可以用来监视、修改、替换方法的定义。方法装饰器会在运行时传入3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象;
  2. 成员的名字;
  3. 成员的属性描述符;
function get(params: any) {
    return function (target: any, methodName: any, desc: any) {
        console.log(target);
        console.log(methodName);
        console.log(desc);
    }
}
class HttpClient {
    url: string | undefined;
    constructor() { }
    @get('/user')
    getData() {
        console.log('getData: ', this.url);
    }
}

image-20200806231007904

可以从打印结果看到方法装饰器的三个参数

function get(params:any) {
    return function(target:any, methodName:any, desc:any) {
        var oldMethod = desc.value;
        desc.value = function(...args:any[]) {
            let newArgs = args.map((item)=>{
                return String(item);
            });
            oldMethod.apply(this, newArgs);
        }
    }
}
class HttpClient {
    constructor() { }
    @get('http://baidu.com')
    getData(...args:any[]) {
        console.log('getData: ', args);
    }
}
var http = new HttpClient();
http.getData(1, 2, true);  // getData: ["1", "2", "true"]

9.4 参数装饰器

参数装饰器表达式会在运行时被调用,可以为类的原型增加一些元素数据,传入3个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员来说是类的原型对象;
  2. 方法名称,如果装饰的是构造函数的参数,则值为undefined
  3. 参数在函数参数列表中的索引;
function logParams(params:any) {
    return function(target:any, methodName:any, paramIndex:any) {
        console.log(target)  // { constructor:f, getData:f } 
        console.log(methodName)  // getData
        console.log(paramIndex)  // 0
    }
}
class HttpClient {
    constructor() { }
    getData(@logParams('uuid') uuid:any) {
        console.log(uuid);
    }
}

参数装饰器只能用来监视一个方法的参数是否被传入

9.5 装饰器执行顺序

多个装饰器同时装饰到一个声明上,语法支持从左到右,或从上到下书写;

不同装饰器的执行顺序:属性装饰器 > 方法装饰器 > 参数装饰器 > 类装饰器;


前端小白