TypeScript学习笔记

一、TypeScript 是什么

TypeScript 以 JavaScript 为基础构建的语言,是一个 JavaScript 的超集

TypeScript 完全支持 JavaScript,并进行扩展,添加了类型

TypeScript 可在任何支持 JavaScript 的平台中执行

但 TypeScript 不能被 JavaScript 解析器直接执行,需要将 TypeScript 编译为 JavaScript

一般大型项目使用 TypeScript,相比 JavaScript,更易于维护(因为变量有类型了)

二、TypeScript 增加了什么

(1)类型

(2)支持 ES 的新特性

(3)添加 ES 不具备的新特性,如抽象类、接口等

(4)丰富的配置选项(可被编译成任意版本的 JS)

(5)强大的开发工具

三、TypeScript 开发环境搭建

(1)下载并安装 Node.js

(2)使用 npm 全局安装 TypeScript 解析器

npm i -g typescript

(3)编写 ts 文件

(4)进入 ts 文件所在目录,使用 tsc 对 ts 文件进行编译

tsc xxx.ts

四、TypeScript 类型

类型声明

类型声明是 TS 非常重要的一个特点

通过类型声明可指定 TS 中变量(参数/形参)的类型

指定类型后,当为变量复制时,TS 编译器会自动检查值是否符合类型声明,符合则赋值,否则报错

语法:

let 变量: 类型;
let 变量: 类型 = 值;
(形参: 类型, 形参: 类型, ...) => 返回值类型
function fn(参数: 类型, 参数: 类型): 返回值类型{...}
fn(参数: 赋值, 参数: 赋值)

自动类型判断

TS 拥有自动类型判断机制

当对变量的声明和赋值是同时进行的,TS 编译器会自动判断变量的类型,所以若变量的声明和赋值同时进行的,可省略类型声明

类型

number:任意数字

string:任意字符串

boolean:布尔值

字面量:限制变量的值就是该字面量的值,如 let a: 10;let b: "mail" | "female"let c: boolean | string

any:任意类型,一个变量设置类型为 any 后相当于对改变了关闭了 TS 的类型检测,

若声明变量不指定类型,则 TS 解析器会自动判断变量类型为 any(隐式的 any)
let d: any; //或 let d;
d = 10;
d = 'a';
d = true

any 类型可赋值给任意变量
let s: string;
s = d; //不报错

unknown:类型安全的 any,unknown 类型不能直接赋值给其他类型变量

let e: unknown;
e = 10;
e = 'a';
e = true

unknown 类型不能赋值给其他类型变量
let s: string;
s = e; //报错
if(typeof e === "string"){
    s = e;  //不报错
}
s = e as string;  //不报错,通过类型断言(用来告诉解析器变量的实际类型)后可赋值
//或
s = <string>e;  //类型断言的另一种写法

void:没有值(或 undefined、null),以函数为例,表示没有返回值的函数

never:不能是任何值,表示永远不会返回结果(连空都不是),可用于函数报错

function fn():never{
    throw new Error('报错');
}

object:任意的 JS 对象,{} 用来指定对象中可以包含哪些属性,在属性后加上?,属性?表示属性是可选的;

let a: object;
let b: {name: string};  //只能有个 name 属性
let c: {name: string, age?: number}; //必须有 name 属性,age 属性可有可无
let d: {name: string, [propName: string]: any};  //对象中需要有 name 属性,其他属性不一定,[propName: string]: any 表示任意类型的属性
let k: {name: string} & {age: number}; //对象需要同时有 name 和 age 属性

array:任意 JS 数组,方式一 类型[],方式二 Array<类型>

let e: string[]; //字符串数组
let g: Array<number>; //数字数组,相当于 let g: number[];

tuple:元组,即固定长度数组,TS 新增类型

let h: [string, string]; //长度为2
h = ['a','b'];
let i: [string, number]; //长度为2,第一个元素为字符串,第二个元素为数字

enum:枚举,TS 中新增类型,如 enum(A,B)

enum Gender{
    Male = 0,
    Female = 1
}
let j:{name: string, gender: Gender}
j = {
    name:'xx',
    gender: Gender.Male
}
console.log(j.gender === Gender.Male)

联合类型:可使用 | 连接多个类型

函数结构的类型声明:(形参: 类型, 形参: 类型…) => 返回值类型

let d: (a:number, b:number) => number;
d = function(n1:string, n2:string):number{
    return 10;
}

类型别名:type 别名 = 类型

let myType = string;
let myType = 1 | 2 | 3 | 4 | 5;
let m: myType;  //相当于 let m: 1 | 2 | 3 | 4 | 5;

html 标签:HTMLElement、HTMLCollection

五、TypeScript 编译选项

自动编译文件

编译文件时,使用 -w 指令后,TS 编译器会自动监视文件的变化,并在文件发生变化时对文件进行重新编译

tsc xxx.ts -w

自动编译整个项目

直接使用 tsctsc -w 指令,可自动将当前项目下的所有 ts 文件编译为 js 文件

但能直接使用 tsc 命令的前提是在项目根目录下创建 ts 编译器的配置文件 tsconfig.json

tsconfig.json 是一个 JSON 文件,添加配置文件后,只需 tsc 命令即可对整个项目编译

在 tsconfig.json 中配置选项如下:

 //定义希望被编译文件所在的目录,默认值是 ["**/*"],** 表示任意目录,* 表示任意文件
"include": [
    "src/**/*", 
    "tests/**/*"
],
 //定义需要排除在外的目录,默认值 ["node_modules","bower_components","jspm_packages"]
"exclude": ["./src/hello/**/*"],
//定义被继承的配置文件
"extends": "./configs/base",
//指定被编译文件的列表,只有需要编译文件少时才会用到
"files": [
    "core.ts",
    "sys.ts",
    "types.ts",
    "scanner.ts",
    "parser.ts",
    "utilities.ts",
    "binder.ts",
    "checker.ts",
    "tsc.ts"
],
//编译器选项 compilerOptions 包含多个子选项,用来完成编译的配置
"compilerOptions":{
    //设置 ts 代码编译为的 ES 版本,可选值有 ES3(默认)、ES5、ES6/ES2015、ES7/ES2016、ES2017、ES2018、ES2019、ES2020、ESNext(表示最新版本 js)
    "target": "ES6",

    //设置编译后代码使用的模块化规范,可选值:CommonJS、UMD、AMD、System、ES2020、ESNext、None
    "module": "ES2015",

    //指定代码运行时所包含的库(宿主环境),默认值是浏览器中运行所需库,一般不需要改,可选值有 ES5、ES6/ES2015、ES7/ES2016、ES2017、ES2018、ES2019、ES2020、ESNext、DOM、WebWorker、ScriptHost...
    //"lib": ["ES6","DOM"],

    //编译后文件的所在目录,默认编译后的 js 文件会和 ts 文件位于相同的目录
    "outDir": "./dist",

    //将代码合并为一个文件,所有的全局作用域中的代码合并到同一个文件中
    "outFile": "./dist/xxx.js",

    //是否对 js 文件进行编译,默认为 false
    "allowJs": true,

    //是否检查 js 代码是否符合语法规范,默认是 false
    "checkJS": true,

    //是否移除注释
    "removeComments": true,

    //不生成编译后的文件,默认为 false
    "noEmit": false,

    //当有错误时不生成编译后的文件
    "noEmitOnError": true,

    //所有严格检查的总开关,设置为 true 或 false 后下面的各选项都会跟着它的值
    "strict": true,

    //设置编译后的文件是否使用严格模式,默认是 false,为true 时会在编译后 js 中第一行添加 'use strict',使用严格模式语法更严格,性能也更好
    "alwaysStrict": true,

    //不允许隐式 any 类型(如函数形参中忘记写类型了)
    "noImplicitAny": true,

    //不允许不明确类型的 this,如需要指定 this:Window
    "noImplicitThis": true,

    //严格的检查空值,如有时使用 getElementById 可能会获取到空值是否需要关注
    "strictNullChecks":true,
}

六、使用 Webpack 打包 ts 代码

(1)首先初始化项目

npm init -y

(2)安装依赖

npm i -D webpack webpack-cli typescript ts-loader

帮助自动生成 html 文件

npm i -D html-webpack-plugin

webpack 开发服务器

npm i -D webpack-dev-server

打包时先清除 dist 目录的插件

npm i -D clean-webpack-plugin

处理浏览器兼容性问题

npm i -D @babel/core @babel/preset-env babel-loader core-js

(3)设置 webpack.config.js 配置文件

const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
    //入口文件
    entry: "./src/index.ts",

    //打包文件所在目录
    output: {
        //打包文件的目录
        path: path.resolve(__dirname, 'dist'),
        //打包后文件的名字
        filename: "bundle.js",
        //配置打包环境,告诉 webpack 不使用箭头函数,不使用 const
        environment: {
            arrowFunction:false,
            const: false
        }
    },

    //打包时要使用的模块
    module: {
        rules: [
            {
                test: /\.ts$/,
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            presets:[
                                [
                                    "@babel/preset-env",
                                    {
                                        //要兼容的目标浏览器
                                        targets:{
                                            "chrome":"58", //兼容到 chrome 58
                                            "ie":"11"
                                        },
                                        "corejs":"3", //core-js 的版本开头,使得 IE 可以使用 promise
                                        "useBuiltIns":"usage" //按需加载
                                    }
                                ]
                            ]
                        }
                    },
                    'ts-loader'
                ],
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HTMLWebpackPlugin(options: {
            template: "./src/index.html"
        });
    ],

    //用来设置引用模块,即哪些文件可作为模块
    resolve: {
        extensions: ['.ts','.js']
    }
};

(4)设置 ts 编译规范,在 tsconfig.json 文件中写入如下内容

{
    "compilerOptions": {
        "module": "ES2015",
        "target": "ES2015",
        "strict": true
    }
}

(5)在 package.json 的 “script” 选项中添加命令

"build": "webpack",
"start": "webpack serve --open chrome.exe"

build 命令用于打包,start 命令在开发时使用 webpack-dev-serve

(6)执行打包

npm run build

或在开发时运行 webpack 服务器

npm start

七、面向对象

在程序中所有对象都被分成数据和功能两部分

要创建对象必须先定义类,类可以理解为对象的模型

class 类名{
    //定义实例属性
    属性名: 类型;
    属性名: 类型 = 值;
    readonly: 属性名: 类型 = 值; //只读属性
    //定义类属性/静态属性,在属性前使用 static
    static 属性名: 类型 = 值;
    static readonly 属性名: 类型 = 值; //只读静态属性

    //定义实例方法
    方法名(){...}
    //定义类方法
    static 方法名(){...}
}

直接定义的属性是实例实现,需要通过对象的实例去访问

const per = new Person();
per.name

类属性/静态属性(或方法)是通过类直接访问,无需创建对象就可以使用的属性,在属性前使用 static

Person.age

readonly 开头的属性表示一个只读属性,无法修改

构造函数和 this

构造函数 constructor 会在对象创建时(即 new 时)调用

class 类名{
    属性名: 类型;
    constructor(参数: 类型){
        this.属性名 = 参数;
    }
    方法名(){...}
}

在实例方法中,this 表示当前实例

在构造函数中当前对象就是当前新建的实例对象,可通过 this 向新建的对象中添加属性

属性在类中定义,在构造函数中赋值

class Dog{
    name: string; //定义属性
    age: number; //定义属性
    constructor(name:string,age:number){
        this.name = name; //给属性赋值
        this.age = age; //给属性赋值
    }
    bark(){
        alert('xx');
    }
}
const dog = new Dog(name:'xx',age:2);

类的继承

使用继承后,子类将会拥有父类所有的方法和属性

通过继承可以将多个类中共有的代码写在一个类中,只需写一次即可让所有子类同时拥有父类中的属性和方法

若要在子类中添加父类中没有的属性或方法直接加就可以

若在子类中添加了和父类相同的方法,则子类方法会覆盖掉父类的方法,这种子类覆盖父类方法的形式称为方法重写

在类的方法中 super 就表示当前类的父类

若在子类中写了构造函数,相当于把父类构造函数覆盖掉,因此在子类构造函数中必须对父类构造函数进行调用

class Animal{ //父类
    name: string;
    constructor(name:string){
        this.name = name;
    }
    sayHello(){
        console.log('xx');
    }
}
class Dog extends Animal{ //Dog 类继承 Animal 类
    age: number;
    constructor(name:string,age:number){
        super(name); //调用父类构造函数
        this.age = age;
    }
    run(){
        console.log(${this.name})
    }
    sayHello(){
        super.sayHello();
        console.log('www')
    }
}
class Cat extends Animal{
    sayHello(){
        console.log('mmm')
    }
}
const dog = new Dog(name:'xx',age:2);
dog.sayHello();
dog.run();
const cat = new Cat(name:'xxx');
cat.sayHello();

抽象类

以 abstract 开头的类是抽象类,抽象类和其他类区别不大,只是不能用来继承

抽象类就是专门用来被继承的类

抽象类中可以添加抽象方法,抽象方法以 abstract 开头,没有方法体,抽象方法只能定义在抽象类中,子类必须对抽象方法进行重写

abstract class Animal{ //父类
    name: string;
    constructor(name:string){
        this.name = name;
    }
    abstract sayHello():void;  //抽象方法
}
class Dog extends Animal{ //Dog 类继承 Animal 类
    sayHello(){
        console.log('www');
    }
}
const dog = new Dog(name:'xx');

上述代码中不能使用 new Animal 创建 Animal 实例

接口

接口用来定义一个类结构,用来定义一个类中应该包含哪些属性和方法

接口只定义对象的结构,而不考虑实际值,接口中的所有属性都不能有实际值

接口中所有方法都是抽象方法

定义类时,可使类去实现一个接口,实现接口就是使类满足接口的要求

接口其实就是定义一个规范,对类做一些限制

interface myInter{
    name: string;
    sayHello():void;
}
class MyClass implements myInter{
    name: string;
    constructor(name: string){
        this.name = name;
    }
    sayHello(){
        console.log('xxx');
    }
} 

同时接口也可以当成类型声明去使用

注意:接口可定义多个同名的接口,使用时是把多个同名接口的内容都加一块看,而 type 类型声明的类型名不能重复

interface myInterface{
    name: string,
    age: number
}
interface myInterface{
    gender: string
}
const obj: myInterface = {
    name:'xx',
    age:111,
    gender:'male'
}

上述代码相当于

type myType = {
    name: string,
    age: number,
    gender: string
}
const obj: myType = {
    name:'xx',
    age:111,
    gender:'male'
}

接口和抽象类的区别:

(1)抽象类中可以有普通方法或抽象方法,而接口中都是抽象方法

(2)抽象类通过 extends 继承,接口通过 implements 实现

属性的封装

之前是属性在对象中设置,属性可任意被修改,这会导致对象中的数据变得非常不安全

TS 可在属性前添加属性的修饰符

public 这是默认值,修饰的属性可在任意位置访问(修改),包括子类

private 私有属性只能在类内部访问(修改),通过在类中添加方法使得私有属性可以被外部访问

protected 受保护的属性,只能在当前类和当前类的子类中访问(修改),不能在实例对象中访问

getter 方法用来读取属性,setter 方法用来设置属性,它们被称为属性的存取器

class Person{
    private _name:string;
    private _age:number;
    constructor(name:string,age:number){
        this._name = name;
        this._age = age;
    }
    //设置 getter 方法
    get name(){
        return this._name;
    }
    //getName(){  //也可以设置一个方法来获取属性
    //    return this._name;
    //}
    //设置 setter 方法
    set name(value: string){
        this._name = name;
    }
    //setName(value: string){     //也可以设置一个方法来设置属性值
    //    this._name = value;
    //}

}
const per = new Person(name:'xxx',age:1)
console.log(per.name); //此处会调用 get name
per.name = 'xxxx'; //此时不是直接去访问 name 属性(因为 name 是私有属性),而是去调用 set name 方法

属性定义也可写在 constructor 中

class A{
    constructor(public name:string,public age:number){
        this.name = name;
        this.age = age;
    }
}

相当于

class A{
    name:string;
    public age:number;
    constructor(name:string,age:number){
        this.name = name;
        this.age = age;
    }
}

泛型

泛型就是不确定的类型,在定义函数或类时,若遇到类型不明确的情况就可以使用泛型

泛型可以同时指定多个

function fn<T>(a:T):T{
    return a;
}

function fn2<T,K>(a:T,b:K):T{
    return a;
}

interface Inter{
    length: number;
}
function fn3<T extends Inter>(a:T):number{ //这里泛型要实现接口 Inter,是 Inter 实现类(或子类)
    return a.length;
}

class MyClass<T>{
    name: T;
    constructor(name: T){
        this.name = name;
    }
}

可直接调用具有泛型的函数

let result = fn(a:10);  //不指定泛型,TS 可自动对类型进行推断
let result1 = fn<string>(a:'hello'); //指定泛型
fn2(a:123,b:'hello')
fn2<number,string>(a:123,b:'hello')
fn3(a:'123');
fn3(a: {length:10});
const mc = new MyClass<string>(name:'xx');

其他

向一个 DOM 元素里的末尾添加一个 div

DOM元素.insertAdjacentHTML(where:"beforend", html:"<div></div>")

键盘事件处理函数

函数(event: KeyboardEvent){...}

bind

bind 作用是创建一个新的函数,把调用 bind 的函数的 this 变为参数中的 this

document.addEventListener(type: 'keydown', 回调函数.bind(this));
则回调函数中的 this 就不是正常情况下的发生事件的 DOM 元素,而是创建函数的当前对象(如外面的类或 window(视情况而定))