1. TypeScript概述

1-1. 为什么要学习TypeScript

  • 就业 或 获得更大的竞争优势
  • 获得更好的开发体验
  • 解决JS中一些难以处理问题

1-2. JS开发中的问题

  • 使用了不存在的变量、函数或成员
  • 把一个不确定的类型当作一个确定的类型处理
  • 在使用null或undefined的成员

js的原罪:

  • js语言本身的特性,决定了该语言无法适应大型的复杂的项目
  • 弱类型:某个变量,可以随时更换类型。
  • 解释性:错误发生的时间,是在运行时

前端开发中,大部分的时间都是在排错

1-3. TypeScript

简称TS

TypeScript是JS的超集,是一个可选的、静态的类型系统

  • 超集
    整数、正整数, 整数是正整数的超集
    超集
  • 类型系统
    对代码中所有的标识符(变量、函数、参数、返回值)进行类型检查
  • 可选的
    学习曲线非常平滑。
  • 静态的(在运行之前)
    无论是浏览器环境,还是node环境,无法直接识别ts代码

babel: es6 -> es5

tsc: ts -> es

tsc: ts编译器

静态:类型检查发生的时间,在编译的时候,而非运行时

TS不参与任何运行时的类型检查。

TS的常识:

  • 2012年微软发布 (ES6,ES2015)
  • Anders Hejlsberg 负责开发TS项目
  • 开源、拥抱ES标准
  • 版本3.4
  • 官网:http://www.typescriptlang.org/

中文网:https://www.tslang.cn/ 个人翻译

额外的惊喜:

有了类型检查,增强了面向对象的开发

js中也有类和对象,js支持面向对象开发。没有类型检查,很多面向对象的场景实现起来有诸多问题。

使用TS后,可以编写出完善的面向对象代码。

2. 在node中搭建TS开发环境

2-1. 安装TypeScript

推荐全局安装

1
npm install -g typescript

使用:

1
2
#编译指定文件文件
tsc 文件名称

默认情况下,TS会做出下面几种假设:

  1. 假设当前的执行环境是dom(浏览器环境)
  2. 如果代码中没有使用模块化语句(import、export),便认为该代码是全局执行
  3. 编译的目标代码是ES3

有两种方式更改以上假设:

  1. 使用tsc命令行的时候,加上选项参数

    1
    2
    #文件改变自动执行编译
    tsc --watch
  2. 使用ts配置文件,更改编译选项

    1
    2
    # 使用了配置文件后,使用tsc进行编译时,不能跟上文件名
    tsc

2-2. TS的配置文件

创建配置文件方法:

  1. 手动创建tsconfig.json文件
  2. 通过命令创建:tsc --init

使用了配置文件后,使用tsc进行编译时,不能跟上文件名,如果跟上文件名,会忽略配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// tsconfig.json
// 配置根据需求选择,以下为常用配置说明
{
"compilerOptions": { //编译选项
"target": "ES2016", //配置编译目标单买的版本标准
"module": "CommonJS", //配置编译目标使用的模块化标准
"lib": ["ES2016"], //配置运行环境,如["ES2016", "dom"]指定浏览器环境,默认配置没有node环境需,要使用第三方库支持
"outDir": "./dist", //配置编译后文件生成位置;默认为源文件位置
"strictNullChecks": true, //严格空值检查,严格空值检查下null和undefined无法赋值给其他类型的变量
"removeComments": true, //编译结果移除注释
"noEmitOnError": true, //错误时不生成编译结果
},
"include": ["./src"], //指定编译路径,可以配置多个;使用指令tsc进行编译时只会编译配置路径下ts文件
"files": ["./src/index.ts"], //指定编译文件,可以配置多个;编译时只会编译配置文件和其依赖的文件
}

使用第三方库支持node环境:

1
npm i -D @types/node

@types是一个ts官方的类型库,其中包含了很多对js代码的类型描述。

JQuery:用js写的,没有类型检查
安装@types/jquery,为jquery库添加类型定义

2-3. 使用第三方库简化流程

2-3-1. ts-node

将ts代码在内存中完成编译,同时完成运行

编译并运行,但是不会生成编译后文件
且修改后代码后需重新执行命令

  1. 安装

    1
    npm install -g ts-node
  2. 使用

    1
    2
    # 指定入口文件,这里可以指定文件名称
    ts-node src/index.ts

2-3-2. nodemon

用于检测文件的变化

  1. 安装

    1
    npm install -g nodemon
  2. 使用

    • 检测所有文件
    1
    2
    3
    4
    # nodemon 检测文件变化
    # --exec 执行 xx 命令
    # 现在监测的是所有文件,配置文件修改也会重新编译
    nodemon --exec ts-node src/index.ts
    • 只检测ts文件
    1
    2
    # -e ts 监控文件的扩展名为ts
    nodemon -e ts --exec ts-node src/index.ts
    • 只检测src文件夹下的ts文件
    1
    2
    # --watch src 指定监听文件夹
    nodemon --watch src -e ts --exec ts-node src/index.ts
  3. 添加到脚本使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    "scripts": {
    "dev": "nodemon --watch src -e ts --exec ts-node src/index.ts",
    "build": "rd /s /q dist & tsc"
    },
    "devDependencies": {
    "@types/node": "^20.4.5"
    }
    }

    rd /s /q dist & tsc 说明:
    先运行 rd /s /q dist 在运行 tsc

    rd 这是”remove directory”的缩写,表示删除目录(windows)
    /s 这个参数表示递归删除目录及其所有子目录和文件
    /q 这个参数表示”quiet”,也就是在删除时不显示确认提示。

    mac中可以使用 rm -rf dist 指令

3. 基本类型约束

TS是一个可选的静态的类型系统

3-1. 类型约束和编译结果对比

3-1-1. 如何进行类型约束

  • 仅需要在 变量、函数的参数、函数的返回值位置加上:类型

    1
    2
    3
    4
    5
    let name: string;

    function add(a:number, b:number):number{
    return a + b;
    }
  • ts在很多场景中可以完成类型推导

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let name: string;

    //可以推导出add返回是为number
    function add(a:number, b:number){
    return a + b;
    }

    //可以推导出num的类型为number
    let num = add(3, 4)

    //可以推导出name为string类型;后续无法赋值为其他类型
    let name = "sss"
  • any: 表示任意类型,对该类型,ts不进行类型检查

    1
    2
    3
    4
    let age;    //推导不出则为any类型,any类型ts不进行检查

    age = 23;
    age = "23";

小技巧,如何区分数字字符串和数字,关键看怎么读?
如果按照数字的方式朗读,则为数字;否则,为字符串。

3-1-2. 源代码和编译结果的差异

编译结果中没有类型约束信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// let name: string;
// name = 333;

function add(a:number, b:number):number{
return a + b;
}

let num:number = add(3, 4)

let name = "sss"


let age;
age = 23;
age = "23";
1
2
3
4
5
6
7
8
9
10
// let name: string;
// name = 333;
function add(a, b) {
return a + b;
}
let num = add(3, 4);
let name = "sss";
let age;
age = 23;
age = "23";

3-2. 基本类型

  • number: 数字

    1
    let age: number = 18;
  • string: 字符串

    1
    let name: string='luckystar';
  • boolean: 布尔

    1
    let married: boolean=false;
  • 数组

    1
    2
    3
    let arr1: number[];       //【方式一】直接指明类型
    let arr3: Array<number> //【方式二】语法糖
    let arr2 = [1,4,5]; //使用类型推导
  • object: 对象

  • null 和 undefined

null和undefined是所有其他类型的子类型,它们可以赋值给其他类型

通过添加strictNullChecks:true,可以获得更严格的空类型检查,null和undefined只能赋值给自身。

3-3. 其他常用类型

  • 联合类型:多种类型任选其一

    1
    let name: string | undefined;

    配合类型保护进行判断

    类型保护:当对某个变量进行类型判断之后,在判断的语句块中便可以确定它的确切类型,typeof可以触发类型保护。

    1
    2
    3
    if(typeof name === 'string'){
    //类型保护
    }
  • void类型:通常用于约束函数的返回值,表示该函数没有任何返回

    1
    2
    3
    function printMenu(): void {
    //...
    }
  • never类型:通常用于约束函数的返回值,表示该函数永远不可能结束

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function throwError(msg: string): never {
    throw new Error(msg);
    }

    function alwaysDoSomething(): never {
    while (true) {
    //...
    }
    }
  • 字面量类型:使用一个值进行约束

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 基本类型约束,不可赋值为其他内容
    let gender: "男" | "女";
    gender = "女";
    gender = "男";

    //对象约束,必须存在约束属性,且类型符合约束条件
    let user: {
    name:string
    age:number
    }

    user = {
    name:"34",
    age:33
    }
  • 元祖类型(Tuple): 一个固定长度的数组,并且数组中每一项的类型确定

    1
    2
    3
    4
    // tu数组为两项,并且第一项为string,第二项为number
    let tu: [string, number];

    tu = ["3", 4];
  • any类型: any类型可以绕过类型检查,因此,any类型的数据可以赋值给任意类型

    1
    2
    3
    let data:any = "luckystar";

    let num:number = data;

4. 类型别名

对已知的一些类型定义名称

1
type 类型名 = ...

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义类型别名
type Gender = "男" | "女"
type User = {
name:string
age:number
gender:Gender
}

let u:User

u = {
name:"sdfd",
gender:"男",
age:34
}

// 返回类型为数组,数组中的对象类型为User
function getUsers(g:Gender):User[] {
return [];
}

5. 函数的相关约束

5-1. 函数重载

函数重载:在函数实现之前,对函数调用的多种情况进行声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 函数的重载;规定combine函数的参数情况只能同时为number或者string
// 这里只是typescript的约束条件
function combine(a:number, b:number):number;
function combine(a:string, b:string):string;

// 函数体
function combine(a: number | string, b: number | string): number | string {
if (typeof a === "number" && typeof b === "number") {
return a * b;
}
else if (typeof a === "string" && typeof b === "string") {
return a + b;
}
throw new Error("a和b必须是相同的类型");
}

// 添加约束后这里即可推导出返回类型,否则无法推导出准确返回类型
const result = combine("a","b")

5-2. 可选参数

可选参数:可以在某些参数名后加上问号,表示该参数可以不用传递。可选参数必须在参数列表的末尾。

1
2
3
4
5
6
7
// 参数c为可选参数
function sum(a: number, b: number, c?: number) {
//...
}

sum(3, 4);
sum(3, 4, 5);

5-3. 默认参数

1
2
3
4
5
6
7
// 默认参数为可选参数
function sum(a: number, b: number, c: number = 0) {
//...
}

sum(3, 4);
sum(3, 4, 5);

6. 扩展类型-枚举

扩展类型:类型别名、枚举、接口、类

枚举通常用于约束某个变量的取值范围。

字面量和联合类型配合使用,也可以达到同样的目标。

6-1. 字面量类型的问题

  • 在类型约束位置,会产生重复代码。可以使用类型别名解决该问题。

    1
    2
    3
    4
    5
    6
    let gender: "男" | "女";
    gender = "女";
    gender = "男";
    function getUsers(g:"男" | "女"):User[] {
    return [];
    }
  • 逻辑含义和真实的值产生了混淆,会导致当修改真实值的时候,产生大量的修改。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 若果需要修改Gender的字面量类型会导致后面使用全部都需要修改
    // 需求更改:帅哥 美女
    type Gender = "男" | "女"

    let gender: Gender;
    gender = "女";
    gender = "男";
    function getUsers(g:Gender):User[] {
    return [];
    }
    //...
  • 字面量类型不会进入到编译结果。

6-2. 枚举

如何定义一个枚举:

1
2
3
4
5
enum 枚举名{
枚举字段1 = 值1,
枚举字段2 = 值2,
...
}

代码示例:

1
2
3
4
5
6
7
8
9
10
11
//可以修改实际值,不会影响后续代码,而且枚举会出现在编译结果中,编译结果中表现为对象。
enum Gender {
male = "男",
female = "女",
}

let gender:Gender;

//操作时使用逻辑名称
gender = Gender.male;
gender = Gender.female;

枚举会出现在编译结果中,编译结果中表现为对象。

枚举的规则:

  • 枚举的字段值可以是字符串或数字

  • 数字枚举的值会自动自增(默认从零开始)

  • 被数字枚举约束的变量,可以直接赋值为数字

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //默认 0 1 2
    enum Level {
    level1,
    level2,
    level3
    }

    //可以赋值,尽量不要这样做
    // let str1: Level = 1;
    // str1 = 2;

    //正确做法
    let str2: Level = Level.level1;
    str2 = Level.level2;
  • 数字枚举的编译结果 和 字符串枚举有差异

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //数字枚举结果
    var Level;
    (function (Level) {
    Level[Level["level1"] = 0] = "level1";
    Level[Level["level2"] = 1] = "level2";
    Level[Level["level3"] = 2] = "level3";
    })(Level || (Level = {}));

    /*
    {
    level1:0,
    level2:1,
    level3:2,
    0:"level1",
    1:"level2",
    2:"level3"
    }
    */

最佳实践:

  • 尽量不要在一个枚举中既出现字符串字段,又出现数字字段
  • 使用枚举时,尽量使用枚举字段的名称,而不使用真实的值

6-3. 扩展知识:位枚举(枚举的位运算)

针对的数字枚举

位运算:两个数字换算成二进制后进行的运算

代码示例: 实现权限的修改和判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//定义权限
enum Permission {
Read = 1, // 0001
Write = 2, // 0010
Create = 4, // 0100
Delete = 8 // 1000
}

//1. 如何组合权限
//使用或运算
//0001
//或
//0010
//0011
let p: Permission = Permission.Read | Permission.Write;

//2. 如何判断是否拥有某个权限
//判断变量p是否拥有可写权限
//0011
//且
//0010
//0010
function hasPermission(target: Permission, per: Permission) {
return (target & per) === per;
}

//3. 如何删除某个权限
//0011
//异或
//0010
//0001
p = p ^ Permission.Write;
console.log(hasPermission(p, Permission.Write));

7. 模块化

本节课相关配置:

配置名称 含义
module 设置编译结果中使用的模块化标准
moduleResolution 设置解析模块的模式
noImplicitUseStrict 编译结果中不包含”use strict”
removeComments 编译结果移除注释
noEmitOnError 错误时不生成编译结果
esModuleInterop 启用es模块化交互非es模块导出

前端领域中的模块化标准:ES6、commonjs、amd、umd、system、esnext

TS中如何书写模块化语句?
编译结果是什么?

7-1. TS中如何书写模块化语句

TS中,导入和导出模块,统一使用ES6的模块化标准

7-2. 编译结果中的模块化

可配置

TS中的模块化在编译结果中:

  • 如果编译结果的模块化标准是ES6: 没有区别
  • 如果编译结果的模块化标准是commonjs:导出的声明会变成exports的属性,默认的导出会变成exports的default属性;

7-3. 解决默认导入的错误

1
2
3
4
5
6
7
8
9
10
//编译为 commonjs 会报错,因为commonjs 是没有默认导出的
import fs from 'fs';
fs.readFileSync("./")

//解决方法一
import {readFileSync} from "fs";
//解决方法二
import * as fs from "fs";
//解决方法三
//修改配置文件,添加 esModuleInterop 配置为 true

7-4. 如何在TS中书写commonjs模块化代码

开启esModuleInterop配置

或者采用下方写法(不推荐):

导出:export = xxx

导入:import xxx = require(“xxx”)


代码示例:

不推荐 这总写法无法推导出类型

1
2
3
4
5
6
7
8
9
10
// index.js
const myModule = require('./myModule');

// myModule.ts
module.exports = {
name: "kevin",
sum(a: number, b: number) {
return a + b;
}
}

ts推荐写法

1
2
3
4
5
6
7
8
9
10
// index.js
import myModule = require("./myModule");

// myModule.ts
export = {
name: "kevin",
sum(a: number, b: number) {
return a + b;
}
};

7-5. 模块解析

模块解析:应该从什么位置寻找模块

TS中,有两种模块解析策略

  • classic:经典
  • node:node解析策略(唯一的变化,是将js替换为ts)
    • 相对路径require("./xxx")
    • 非相对模块require("xxx")

8. 接口和类型兼容性

扩展类型-接口

8-1. 接口的概念

接口:inteface

扩展类型:类型别名、枚举、接口、类

TypeScript的接口:用于约束类、对象、函数的契约(标准)

契约(标准)的形式:

  • API文档,弱标准
  • 代码约束,强标准

8-2. 接口的使用

和类型别名一样,接口,不出现在编译结果中

  1. 接口约束对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //在约束对象时,接口(interface)和类型别名(type)无太大区别
    interface User {
    name: string
    age:number
    }

    let u: User = {
    name: "luckystar",
    age: 18
    }
  2. 接口约束函数

    对象中的函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    interface User {
    name: string
    age:number
    // 约束函数:无参数,无返回值;注意不能写函数实现(两种写法都可以)
    // sayHello: () => void
    sayHello():void
    }

    let u: User = {
    name: "luckystar",
    age: 18,
    sayHello(){
    console.log("sayHello");
    }
    }

    普通函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 在约束函数时,接口(interface)和类型别名(type)无太大区别
    // type Condition = (n:number) => boolean

    //接口约束函数写法
    interface Condition {
    (n:number):boolean;
    }
    function sum (numbers:number[], callBack: Condition) {
    let s = 0;
    numbers.forEach(n => {
    if(callBack(n)){
    s += n;
    }
    })
    return s;
    }
    const result = sum([3,4,5,7,11], n=>n%2 !== 0);
    console.log(result);

    当使用类型别名约束方法时也可以如下书写

    1
    2
    3
    4
    //当花括号中只有方法约束时,花括号相当于定界符;并不是对象
    type Condition = { //定界符
    (n:number):boolean;
    }

8-3 接口的继承

可以通过接口之间的继承,实现多种接口的组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface A {
T1: string;
}

interface B {
T2: number
}

interface C extends A, B {
T3: boolean
}

let u: C = {
T1: "luckyStar",
T2: 18,
T3: true
}

使用类型别名可以实现类似的组合效果,需要通过&,它叫做交叉类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//交叉类型
type A = {
T1: string;
}

type B = {
T2: number
}

type C = {
T3: boolean
} & A & B

let u: C = {
T1: "luckyStar",
T2: 18,
T3: true
}

它们的区别:

  • 子接口不能覆盖父接口的成员
  • 交叉类型会把相同成员的类型进行交叉

8-4. readonly 修饰符

只读修饰符,修饰的目标是只读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//修饰变量
//限制数组不能被修改
let arr1: readonly number[] = [3, 4, 6];

//限制arr2变量和数组不能被修改
const arr2: readonly number[] = [3, 4, 6];


//修饰对象中的属性
type User = {
readonly id: string
name: string
age: number,
//限制arr不能被重新赋值并且数组内容也不能被修改
readonly arr: readonly string[]
}

let u: User = {
id: "123",
name: "Asdf",
age: 33,
arr:["Sdf", "dfgdfg"]
}

注意: 只读修饰符不在编译结果中

8-5. 类型兼容性

B->A,如果能完成赋值,则B和A类型兼容

鸭子辨型法(子结构辨型法):目标类型需要某一些特征,赋值的类型只要能满足该特征即可

  • 基本类型:完全匹配

  • 对象类型:鸭子辨型法

类型断言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Duck {
sound: "嘎嘎嘎"
swin(): void
}

let person = {
name: "伪装成鸭子的人",
age: 11,
//使用类型断言; 因为interface中sound类型是“嘎嘎嘎”;这里不是用as类型则是string
sound: "嘎嘎嘎" as "嘎嘎嘎",
swin() {
console.log(this.name + "正在游泳,并发出了" + this.sound + "的声音");
}
}

//可以进行赋值
let duck:Duck = person;

当直接使用对象字面量赋值的时候,会进行更加严格的判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
interface Duck {
sound: "嘎嘎嘎"
swin(): void
}

//不可以赋值;这里会报错;会报错;会报错
let duck:Duck = {
name: "伪装成鸭子的人",
age: 11,
sound: "嘎嘎嘎" as "嘎嘎嘎",
swin() {
console.log(this.name + "正在游泳,并发出了" + this.sound + "的声音");
}
};

//可以使用类型断言解决
//方式一
let duck:Duck = {
name: "伪装成鸭子的人",
age: 11,
sound: "嘎嘎嘎" as "嘎嘎嘎",
swin() {
console.log(this.name + "正在游泳,并发出了" + this.sound + "的声音");
}
} as Duck;

//方式二
let duck:Duck = <Duck> {
name: "伪装成鸭子的人",
age: 11,
sound: "嘎嘎嘎" as "嘎嘎嘎",
swin() {
console.log(this.name + "正在游泳,并发出了" + this.sound + "的声音");
}
}
  • 函数类型

一切无比自然

参数:传递给目标函数的参数可以少,但不可以多

返回值:要求返回必须返回;不要求返回,你随意;

9. TS中的类

面向对象思想

基础部分,学习类的时候,仅讨论新增的语法部分。

9-1. 属性

使用属性列表来描述类中的属性

1
2
3
4
5
6
7
8
9
10
11
//typescript
class User {
//TS 中需要明确确类的属性,否则会报错
name: string
age: number
constructor(name: string, age: number){
this.name = name;
this.age = age;
}
}
const u = new User("luckystar", 18);

编译后:

1
2
3
4
5
6
7
8
//javascript
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const u = new User("luckystar", 18);

9-1-1. 属性的初始化检查

在配置文件tsconfig.json中添加如下配置,当类中的属性在构造函数中没有赋值时会提示
strictPropertyInitialization:true

9-1-2. 属性的初始化位置

  1. 构造函数中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class User {
    name: string
    age: number
    gender: "男" | "女"
    constructor(name: string, age: number, gender:"男" | "女" = "男"){
    this.name = name;
    this.age = age;
    this.gender = gender;
    }
    }
    const u = new User("luckystar", 18);
  2. 属性默认值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class User {
    name: string
    age: number
    gender: "男" | "女" = "男"
    constructor(name: string, age: number){
    this.name = name;
    this.age = age;
    }
    }
    const u = new User("luckystar", 18);

9-1-3. 属性可以修饰为可选的

1
2
3
4
5
6
7
8
9
10
11
class User {
name: string
age: number
pid?: string //可选属性
constructor(name: string, age: number){
this.name = name;
this.age = age;
}
}
const u = new User("luckystar", 18);
u.pid = "xxx"

9-1-4. 属性可以修饰为只读的

1
2
3
4
5
6
7
8
9
10
11
12
class User {
readonly id: number //只读属性
name: string
age: number
pid?: string
constructor(name: string, age: number){
this.id = Math.random();
this.name = name;
this.age = age;
}
}
const u = new User("luckystar", 18);

9-1-5. 使用访问修饰符

访问修饰符可以控制类中的某个成员的访问权限

  • public:默认的访问修饰符,公开的,所有的代码均可访问
  • private:私有的,只有在类中可以访问
  • protected:暂时不讲

访问修饰符可以修饰属性也可以修饰方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class User {
readonly id: number
pid?: string
private _publishNumber: number = 3; //每天一共可以发布多少篇文章
private _curNumber: number = 0; //当前可以发布的文章数量
constructor(name: string, age: number){
this.id = Math.random();
}

publish(title: string) {
if (this._curNumber < this._publishNumber) {
console.log("发布一篇文章:" + title);
this._curNumber++;
}
else {
console.log("你今日发布的文章数量已达到上限");
}
}
}
const u = new User("luckystar", 18);
u.publish("文章1")
u.publish("文章2")
u.publish("文章3")
u.publish("文章4") //你今日发布的文章数量已达到上限
u.publish("文章5") //你今日发布的文章数量已达到上限
u.publish("文章6") //你今日发布的文章数量已达到上限

9-1-6. 属性简写

如果某个属性,通过构造函数的参数传递,并且不做任何处理的赋值给该属性。可以进行简写

1
2
3
4
5
6
7
8
9
class User {
readonly id: number //只读属性
pid?: string
// 简写属性name和age;(省略属性列表和构造函数,并在参数前添加修饰符)
constructor(public name: string, public age: number){
this.id = Math.random();
}
}
const u = new User("luckystar", 18);

9-1-7. 访问器

作用:用于控制属性的读取和赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class User {
readonly id: number
gender: "男" | "女" = "男"
constructor(public name: string, private _age: number) {
this.id = Math.random();
}
set age(value: number) {
if (value < 0) {
this._age = 0;
}
else if (value > 200) {
this._age = 200;
}
else {
this._age = value;
}
}
get age() {
return Math.floor(this._age);
}
}

const u = new User("aa", 22);
u.age = 1.5;
console.log(u.age);

10. 泛型

有时,书写某个函数时,会丢失一些类型信息(多个位置的类型应该保持一致或有关联的信息)

泛型:是指附属于函数、类、接口、类型别名之上的类型

泛型相当于是一个类型变量,在定义时,无法预先知道具体的类型,可以用该变量来代替,只有到调用时,才能确定它的类型

很多时候,TS会智能的根据传递的参数,推导出泛型的具体类型

如果无法完成推导,并且又没有传递具体的类型,默认为空对象

泛型可以设置默认值

10-1. 在函数中使用泛型

在函数名之后写上<泛型名称>

1
2
3
4
5
6
7
8
9
10
11
12
13
//声明类型变量T;定义函数时无法确定类型,只有调用时才能确定
function take<T>(arr: T[], n: number): any[] {
if(n >= arr.length) {
return arr;
}
const newArr: T[] = [];
for (let i = 0; i < n; i++) {
newArr.push(arr[i]);
}
return newArr;
}

const result = take<string>(["1","2","3"], 2);

10-2. 如何在类型别名、接口、类中使用泛型

直接在名称后写上<泛型名称>

  1. 类型别名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //回调函数:判断数组中的某一项是否满足条件
    // type callback = (n: number, i: number) => boolean;
    type callback<T> = (n: T, i: number) => boolean;

    function filter<T>(arr: T[],callback: callback<T>): T[] {
    const newArr: T[] = [];
    arr.forEach((n, i) => {
    if(callback(n, i)){
    newArr.push(n);
    }
    })
    return newArr;
    }

    const arr = [1,2,3,4,5];
    //这里TS 是可以推断出类型的
    console.log(filter(arr, n => n % 2 !== 0));
  2. 接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    interface callback<T>{
    (n: T, i: number): boolean;
    }

    function filter<T>(arr: T[],callback: callback<T>): T[] {
    const newArr: T[] = [];
    arr.forEach((n, i) => {
    if(callback(n, i)){
    newArr.push(n);
    }
    })
    return newArr;
    }

    const arr = [1,2,3,4,5];
    console.log(filter(arr, n => n % 2 !== 0));
  3. 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    export class ArrayHelper<T> {

    constructor(private arr: T[]) { }

    take(n: number): T[] {
    if (n >= this.arr.length) {
    return this.arr;
    }
    const newArr: T[] = [];
    for (let i = 0; i < n; i++) {
    newArr.push(this.arr[i]);
    }
    return newArr;
    }

    shuffle() {
    for (let i = 0; i < this.arr.length; i++) {
    const targetIndex = this.getRandom(0, this.arr.length);
    const temp = this.arr[i];
    this.arr[i] = this.arr[targetIndex];
    this.arr[targetIndex] = temp;
    }
    }

    private getRandom(min: number, max: number) {
    const dec = max - min;
    return Math.floor(Math.random() * dec + max);
    }
    }

10-3. 泛型约束

泛型约束,用于现实泛型的取值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface hasNameProperty {
name: string
}

/**
* 将某个对象的name属性的每个单词的首字母大小,然后将该对象返回
* 这里T 继承 hasNameProperty 约束了 T 中必须有name属性
* 约束不一定为接口、类型别名等其他也可以
*/
function nameToUpperCase<T extends hasNameProperty>(obj: T): T {
obj.name = obj.name
.split(" ")
.map(s => s[0].toUpperCase() + s.substr(1))
.join(" ");
return obj;
}

const o = {
name:"kevin yuan",
age:22,
gender:"男"
}

const newO = nameToUpperCase(o);
console.log(newO.name); //Kevin Yuan

10-4. 多泛型

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//将两个数组进行混合
//[1,3,4] + ["a","b","c"] = [1, "a", 3, "b", 4, "c"]
function mixinArray<T, K>(arr1: T[], arr2: K[]): (T | K)[] {
if (arr1.length != arr2.length) {
throw new Error("两个数组长度不等");
}
let result: (T | K)[] = [];
for (let i = 0; i < arr1.length; i++) {
result.push(arr1[i]);
result.push(arr2[i]);
}
return result;
}

const result = mixinArray([1, 3, 4], ["a", "b", "c"]);

result.forEach(r => console.log(r));

__END__