Typescript 不是一门全新的语言,Typescript是 JavaScript 的超集,它对 JavaScript进行了一些规范和补充。使代码更加严谨。
https://www.typescriptlang.org/play
npm init生成package.json包管理文件
//package.json
{
"name": "typescript-coding",
"version": "1.0.0",
"description": "typeScript-learning",
"main": "./src/index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+ssh://[email protected]/Wanghe0428/typeScript-learning.git"
},
"keywords": [
"typeScript",
"source_code",
"wang_He"
],
"author": "wang_He",
"license": "MIT",
"bugs": {
"url": "https://github.com/Wanghe0428/typeScript-learning/issues"
},
"homepage": "https://github.com/Wanghe0428/typeScript-learning#readme"
}
需要全局安装typeScript包和tslint(对代码风格进行检测的工具,专注于ts,与eslint类似),安装完这两个依赖包之后,就可以使用tsc命令了。
tsc --init进行初始化ts项目,运行此代码之后会新增一个tsconfig.json文件,这个文件主要是用于对ts项目进行配置的,且在这里面可以书写注释代码。
这里采用webpack4。
cnpm i webpack webpack-cli webpack-dev-server -D
在package.json中的script几点下配置build打包:
“build”: “webpack --config ./build/webpack.config.js”,
在终端输入命令npm run buind后就会将打包的文件输出到指定文件目录下(/dist目录)。
// 布尔类型
// 声明变量并初始化
let bool: boolean = false
// bool = 123 //这里是错误赋值,由于bool变量设置为布尔型,所以他不能赋值为其他类型number
// 数值类型
let num: number = 123
// num = "w" //错误写法
// 在ts中支持ES6中新增的二进制、八进制、十进制、十六进制写法
num = 0b00000001 //0b二进制写法 数值1
num = 0o1 //0o八进制 数值1
num = 0x1 //十六进制 数值1
// 字符串类型
let str: string = "ww"
// 也可以使用ES6新增的模块化字符串``进行拼接字符
str = `数值是${num}`
console.log(str);
// 数组类型[1,2,3]
// 写法1,arr1为所有数值都为number类型的数组
let arr1: number[]
// 写法二,有点儿java中泛型编程的意思
let arr2: Array<number>
// 上面的arr1和arr2数组中的数值只能是一种类型,下面是一种联合写法
let arr3: (number | string)[]
// 也可以这样写:
let arr4: Array<number | string>
// 元组类型,与数组类似,是数组的拓展,区别是数组只要指定了元素的数据类型,你写多少数据都可以即不固定长度length
// 而元组是固定长度固定位置上的类型的
let tuple: [string, number, boolean]
tuple = ["wh", 23, true] //元组数值类型必须与元组对应索引原先设定的类型相同
// tuple = ["wh", "ww", true,2] //"ww"类型不同会报错,且长度不能超过,会越界
// enum枚举类型,c++中常见枚举类型,枚举类型中的值都会对应一个序列号
// 枚举值一般都大写开头
// 定义一个角色用户枚举对象
// 像设置一个对象类似
enum Roles {
// 也可以手动设置序列号,默认序列号是从0开始枚举依次递增
SUPER_ADMIN = 1,
ADMIN = 2,
USER = 3
}
console.log(Roles.SUPER_ADMIN, Roles.ADMIN, Roles.USER); //分别是1,2,3
console.log(Roles[1]); //SUPER_ADMIN
console.log(Roles); //{1: 'SUPER_ADMIN', 2: 'ADMIN', 3: 'USER', SUPER_ADMIN: 1, ADMIN: 2, USER: 3}
//以上创建Roles枚举对象的ts代码等同于一下js代码
(function (Roles) {
// 也可以手动设置序列号,默认序列号是从0开始枚举依次递增
Roles[Roles["SUPER_ADMIN"] = 1] = "SUPER_ADMIN";
Roles[Roles["ADMIN"] = 2] = "ADMIN";
Roles[Roles["USER"] = 3] = "USER";
})(Roles || (Roles = {}));
// any类型,任何类型,可以给一个变量赋任何类型的值,但是any我不能总是用any,开发习惯是能不用any的时候就不要用!!无语
let any: any = "w"
any = 133
const arr5: any[] = [2, "ww", false]
// void类型,经常用于函数没有返回值时,返回void类型
const say = function (mes: string): void {
console.log(mes);
// 此函数没有任何返回值就需要设置返回值为void类型
}
say("吃饭")
// 其实void类型数据可以赋值undefined和null,函数中没有返回值,就是返回undefined,undefined属于void类型
let v: void = undefined
let n: void = null
// null和undefined,在ts中他们既是值又是类型,undefined和null是其他类型的子类型,比如可以为number类型数据赋值null
let u: undefined = undefined
let n: null = null
// n=2 ,报错
// undefined和null是其他类型的子类型,比如可以为number类型数据赋值null
// let num1: number = null
// num1 = undefined
// console.log(num1);
// never类型,主要用于两种情况,
// 1、用于抛出异常时函数返回的类型
const errorFunction = function (mes: string): never {
throw new Error(mes)
}
// errorFunction("出错了")
// 2、函数中由死循环的,返回值类型为never类型
const infiniteFunction = (): never => {
while (true) { }
}
let nev: never = infiniteFunction()
// never类型也是其他类型的子类型,可以为其他类型赋值的变量,比如:
num = nev
// object类型,简单数据类型即数值类型的它们存的是值,是对象类型的值是存的是对象在内存中地址的引用
let obj = {
name: "wh"
}
function getObject(obj: object): void {
console.log(obj);
}
getObject(obj)
主要是针对变量类型没有某些属性,而让此变量类型强制类型转化为某种类型
类型断言写法(变量)或者(变量 as string) ,表示将变量转化为string类型
// 类型断言
const getLength = (target: (string | number)) => {
// 传过来的target参数有length属性,对类型进行判断会出错(因为number类型没有length属性,而代码中使用了length),
// 所以我们要采用类型断言来解决此问题,类型断言写法(target)或者(target as string)
if ((<string>target).length) {
return (<string>target).length
} else {
// 如果传入的是数值,数值类型是没有length属性的,我们就需要先把他转化为字符串
return target.toString().length
}
}
symbol是es6新增的一种基本数据类型,symbol主要是用来表示一种独一无二的值
// symbol是es6新增的一种基本数据类型,symbol主要是用来表示一种独一无二的值
let s1= Symbol("2")
let s2 = Symbol("2")
console.log(s1 == s2); //false,体现了symbol的值是独一无二的
console.log(s1); //symbol(2)
// symbol值是不能和其他值做运算的
// s1+2 //会报错
// symbol类型的数据有toString方法,将值转换为字符串类型
console.log(s1.toString()); //Symbol(2)
// symbol一个最大的用处就是可以作为属性名来表示属性名的独一无二性
let prop = "name"
// es5写法
let obj1 = {
name: "wh"
}
// es6写法
let obj2 = {
// 用中括号括起来,以下代码等同于:name:'wh', myname:'wh'
[prop]: 'wh',
[`my${prop}`]: 'wh'
}
console.log(obj2); //{name: 'wh', myname: 'wh'}
// 用symbol值作为属性名,写法:
let s3 = Symbol("name")
let obj3 = {
// 独一无二,不会被别的同名属性覆盖
[s3]: 'wh',
[Symbol("age")]: 23, //尽量不要这样写,因为在获取obj3对象的值时不能使用obj3[Symbol("age")]
sex: 'man'
}
console.log(obj3[Symbol("age")]); //undefined
console.log(obj3); //{Symbol(name): 'wh'}
obj3[s3] = 'ww' //修改键对应的值
console.log(obj3); //{Symbol(name): 'ww'}
// console.log(obj3.s3); //不能这样获取属性
console.log(obj3[s3]);
// 使用symbol时对象对象的遍历时,有的时候不能遍历到Symbol类型的属性名,以下几种方式都不能获取到Symbol类型的属性名
// 第一种方式,使用for in,使用for in是遍历不到对象上属性名为Symbol类型的属性的
for (let key in obj3) {
console.log(key); //只能打印出sex,其他的Symbol类型的属性名都不能打印出来
}
// 第二种方式,Object.keys()
console.log(Object.keys(obj3)) //['sex']同样返回的key数组中只有sex属性,没有其他Symbol类型的属性
// 第三种方式Object.getOwnPropertyNames()
console.log(Object.getOwnPropertyNames(obj3));//['sex']样返回的key数组中只有sex属性,没有其他Symbol类型的属性
console.log(JSON.stringify(obj3)) //{"sex":"man"},返回的字符串中没有symbol类型的属性
// 以上几种方式都是不能获取到以symbol类型作为属性名的属性
// 获取Symbol类型的属性:
// 第一种方式:采用Object.getOwnPropertySymbols()
console.log(Object.getOwnPropertySymbols(obj3)); //[Symbol(name), Symbol(age)],只能获取Symbol类型属性名,不能获取字符串类型的属性名
// 第二种方式,采用ES6新提供的Reflect对象
console.log(Reflect.ownKeys(obj3)); //['sex', Symbol(name), Symbol(age)],会拿到所有属性名keys,不论是Symbol类型还是string类型的属性名
// Symbol的两个静态方法!!Symbol.for()和Symbol.keyFor()
// symbol.for()和Symbol()都会返回创建的Symbol类型的值,不同之处是symbol.for(str)会检查之前有没有用过str创建过Symbol值,如果用过那么就直接返回之前创建的Symbol值,感觉和单例模式有点儿像诶
let s4 = Symbol.for("wh")
let s5 = Symbol.for("wh")
let s6 = Symbol("wh")
console.log(s4 == s5); //true
console.log(s6 == s5); //false,因为采用Symbol()创建的值是独一无二的
//Symbol.keyFor(Symbol(str))是用来返回创建的Symbol值的标识,返回的是个字符串
console.log(Symbol.keyFor(s4)); //wh
// es6提供了11个内置的Symbol值指向es内部得一些属性和方法,了解知识,可以自己用时搜索,暂时跳过!!!!
//1. Symbol.hasInstance,在instanceof时判断当前实例是否是后面类的实例时可以用到
let obj4 = {
// 当后续调用instanceof时触发
[Symbol.hasInstance](otherobj) {
console.log(otherobj); //打印出{name:"wh"}
}
}
console.log({ name: "wh" } instanceof <any>obj4); //false
// 2......
ts经常对一些数据的结构进行检测,接口就能够为我们的代码定义一些结构,让其他人在使用工具或函数时能够遵循一些规定。
// typescript的接口基本用法
// let getFullName = ({ firstName, lastName }) => {
// return firstName + lastName
// }
// let c = getFullName({
// firstName: "w",
// lastName: 23 //将lastName赋值数值类型肯定不是我们预期想要传的参数,那么我们就可以使用接口来限定传入的参数的结构
// })
// console.log(c); //w23
// 定义接口,与java中的接口类似,就是为了实现某种功能
// 以下代码使用接口来实现以上代码返回名字的函数
interface NameInfo {
firstName: string,
lastName: string
}
// 函数参数使用接口进行限制,限制传入的参数只能是string类型的,如果是其他类型的就会报错
// 传参是采用了结构的写法
let getFullName = ({ firstName, lastName }: NameInfo): string => {
return firstName + lastName
}
let c = getFullName({
firstName: "w",
// lastName: 23 //因为函数采用了NameInfo接口,所以lastName只能为string类型,传其他类型会报错
lastName: 'h'
})
console.log(c); //wh
interface Vegitable {
// 属性后加一个问号代表可选属性,可有可无
color?: string,
type: string,
// 方法2:采用索引签名
// 指定属性名为string类型,属性值为any类型
// [prop: string]: any //多余属性
}
// 传入的color参数可由可无
let getVegitables = ({ color, type }: Vegitable) => {
return `A ${color ? (color + '') : ''} ${type}`
}
console.log(getVegitables({ type: "tomato" })); //A tomato
// 对于传入的参数过多,有两种方式解决,分别是类型断言、索引签名、类型兼容性
// 方法1:采用类型断言,传入的参数就是Vegitable所规定的的那样,这样编译器就不会报错啦
console.log(getVegitables(<Vegitable>{ color: "white", type: "tomato", size: 2 })); //A white tomato
// 方法3:采用类型兼容性
let obj1 = { color: "white", type: "tomato", size: 2 }
// 当前传参是一个变量,而非一个常量对象
console.log(getVegitables(obj1)); //A white tomato
// 设置只读属性
interface Person {
name: string,
// 设置只读属性,一旦设置,后续就不能够对其值进行修改
readonly sex: string
}
let wh: Person = {
name: 'wh',
sex: 'man'
}
// wh.sex = "c" 会报错,因为sex是只读属性
// 设置数组中的数据为只读
interface arrInter {
0: string,
readonly 1: number
}
let arr: arrInter = ["wh", 2]
// arr[1] = 3 报错,数组中索引1元素为只读属性,不能重新赋值
// 接口除了能够定义对象的结构,他还可以定义函数的结构
// 定义函数时的结构形式:
// interface addFunc {
// // (参数):返回值类型
// (num1: number, num2: number): number
// }
interface String {
//定义有名函数
getFirstLetter(): string
}
//eslint推荐使用类型别名的形式: 以下代码等同于以上代码定义:
// 使用type给一个类型定义一个名字
type addFunc = (num1: number, num2: number) => number
// 定义一个add函数,采用addFunc接口
let add: addFunc = (n1, n2) => {
return n1 + n2
}
// 可以给索引即属性名指定一个类型
interface roleDic {
[id: number]: string
}
let role1: roleDic = {
0: 'super_admin'
// "a":"wda" //报错,因为属性名必须为数值类型
}
console.log(role1);
interface roleDic1 {
[id: string]: string
}
let role2: roleDic1 = {
"name": "wh",
//注意!! 这里属性为number类型也没有报错,原因是在js中属性名为number类型的属性会自动转化为字符串string类型
1: "ww"
}
console.log(role2.name)
console.log(typeof Object.keys(role2)[1]); //string ,表明属性名1已经自动转化为string类型
// 混合类型接口
// 这个接口的功能就是要制造一个符合():void函数规范,并且这个函数要有number类型的count属性
interface Counter {
// 函数类型
(): void,
// count属性
count: number
}
//个人理解:getCounter这个函数用Counter接口来限制,所以包括一个返回值为void类型的函数,并设定此函数拥有一个number类型的属性
// 这个接口是给函数内部的函数定义,并定义内部函数的属性
let getCounter = (): Counter => {
// 制造一个返回值为void类型的函数
const c = () => {
c.count++
}
// 且此函数拥有一个number类型的属性
c.count = 0
return c
}
let c1 = getCounter()
c1()
console.log(c1.count); //1
c1()
console.log(c1.count); //2
接口的继承和类的继承相似,都可以提供复用性
// 接口的继承和类的继承相似,都可以提供复用性
// 小栗子:
interface Vegitables {
color: string
}
// 让接口Tomoto继承于Vegitables,此时Tomato接口就继承了Vegitables接口的所有属性包括color,与java中的接口继承写法相似
interface Tomato extends Vegitables {
//与Vegitable接口都有一个公共的属性color,这时就可以使用接口继承
// color: string
// 半径
radius: number
}
let tomato: Tomato = {
color: "yellow1",
radius: 100
}
// ES5中创建函数
function add(num1: number, num2: number): number {
return num1 + num2
}
// es6的箭头函数
let add1 = (num1: number, num2: number): number => {
return num1 + num2
}
// 声明一个函数类型的变量
let add2: (x: number, y: number) => number
// 给此变量其赋一个函数
add2 = (num1: number, num2: number): number => {
return num1 + num2
}
// 接口定义一个函数类型
interface Add {
(num1: number, num2: number): number
}
// 同上,eslint使用类型别名创建一个函数接口Add1
type Add1 = (num1: number, num2: number) => number
let addFunc: Add1 = (num1: number, num2: number) => {
return num1 + num2
}
// 自己创建一个isString类型,其等同于string类型
type isString = string
let str: isString = "wh"
console.log(str); // 字符串wh
console.log(typeof str); //string
// 2. 函数参数
// ts中定义设置可选参数,可选参数指的就是调用函数时传入则使用,不传参则忽略,即某参数可有可无
// ts中可选参数必须置于必须参数之后,加上问号?即代表此参数为可选参数
type AddFunction = (arg1: number, arg2: number, arg3?: number) => number
let add3: AddFunction = (num1: number, num2: number) => {
return num1 + num2
}
let add4: AddFunction = (num1: number, num2: number, z: number) => {
return num1 + num2 + z
}
// 设置默认参数,ES6中本身就有 ,num2 = 3 或 num2: number = 3即可将number类型省略,会根据字面量自动判断num2为什么类型
let add5 = (num1: number, num2 = 3): number => {
return num1 + num2
}
// 剩余参数,采用ES5中的arguments类数组或ES6的...argsArr 展开运算符进行收集参数,此时argsArr就是一个接收剩余参数的数组
let handleData = (arg1: number, ...args: number[]) => {
//写逻辑...
}
ts中重载是指允许用function来定义多个函数进行重载,然后再写此函数的实体,实体函数中需要写判断逻辑根据传入不同个数不同参数类型的参数,来对应实际返回的结果
//3. 函数重载
// 在java中的重载是指一个类中会预先先多个同名方法,然后会根据在调用此类的方法时传入的参数类型、参数个数的不同会自动调用不同的重载函数,
// 常见面试题:重载和重写的区别:java中重写是指子类继承父类时可以重写继承的父类的一些方法
// 而在ts中重载的用法不同,ts中是指允许用function来定义多个函数进行重载,然后再写此函数的实体
// 然后会根据传入不同个数不同参数类型的参数,然后会自动判断出实际返回的结果
// 定义函数重载形式:
function handleData1(x: string): string[]
function handleData1(x: number): number[]
// 以上两个函数就是函数重载
// 然后再定义函数的实体
function handleData1(x: any): any {
if (typeof x === "string") {
// 返回字符串x拆分成的数组
return x.split("")
} else {
return x.toString().split("").map(item => parseInt(item))
}
}
let res = handleData1("ww")
console.log(res); //['w', 'w']
let res1 = handleData1(34)
console.log(res1); //[3, 4]
// 注意一定不能向一下函数一样这样定义,这类似于java中的定义形式,在ts中时错误的,ts中必须先定义函数重载,再写实体函数
// function handleData2(num1: number): number[] {
// return num1.toString().split("").map(item => parseInt(item))
// }
// function handleData2(num1: string): string[] {
// return num1.split("")
// }
考虑到可重用性,我们可以在ts中使用泛型来进行约束,让API支持多种类型数据同时用能够支持类型结构的检查。
// 1. 泛型的简单使用
let getArray = (value: any, times: number = 5): any[] => {
// 创建一个数组的函数
return new Array(times).fill(value)
}
// 由于数组中的每一项都是number类型的2,没有length属性,所以map之后的元素都是undefined,
// 这肯定不是我们想要的结果(都是undefined),所以我们使用ts中的泛型进行约束
console.log(getArray(2, 3).map(item => item.length)); //[undefined, undefined, undefined]
// 使用泛型来对以上函数进行约束,写法:用尖括号
let getArray1 = <T>(value: T, times: number = 5): T[] => {
return new Array(times).fill(value)
}
// 普通函数泛型的尖括号位置
function getArray2<T>(value: T, times: number = 5): T[] {
return new Array(times).fill(value)
}
// 使用泛型时的函数调用:在函数调用是也需要传入泛型变量类型
let arr1 = getArray1<number>(2, 4)
// let arr2 = getArray1(2,3) 会报错,因为泛型变量的类型为string,所以传入的第一个参数要保持一致也是string类型
// 2. 泛型变量
let getArray3 = <T, U>(arg1: T, arg2: U, times: number): Array<[T, U]> => {
return new Array(times).fill([arg1, arg2])
return []
}
// 传入泛型变量的类型number、string,这里泛型变量的类型可传可不传,不传的话会根据传入的实参进行判断其类型
let arr3 = getArray3<number, string>(2, "wh", 3)
// let arr4 = getArray3(2, "wh", 3) //不传泛型变量的类型
console.log(arr3); //二维数组[[2, 'wh'], [2, 'wh'], [2, 'wh']]
// 泛型在类型定义中的使用,使用泛型来定义函数类型
// 定义getArray4的函数类型,为返回一个泛型变量类型的元素的数组
let getArray4: <T>(arg: T, times: number) => T[]
// 定义getArray4的函数体
getArray4 = <T>(arg: T, times: number): T[] => {
return new Array(times).fill(arg)
}
// getArray4(123,3).map(item => item.length) 会报错,因为预先设定的数组元素为number属性,无length属性
//利用泛型变量时来使用类型别名定义一个函数的类型
type getArray = <T>(arg: T, times: number) => T[]
// 定义函数实体
let getArray5: getArray = <T>(arg: T, times: number): T[] => {
return new Array(times).fill(arg)
}
//如何利用泛型时来使用接口来定义函数类型,其实使用接口定义函数类型会被tslint自动转换为使用类型别名的形式创建函数类型
interface getArrayInte {
<T>(arg: T, times: number): T[]
}
// 也可以将泛型变量提升到最外层
interface getArrayInte1<T> {
(arg: T, times: number): T[],
// 为这个函数添加属性
arr: T[]
}
泛型约束,也就是对泛型变量进行条件限制约束,是采用extends来限制泛型变量。
// 3. 泛型约束,也就是对泛型变量进行条件限制约束,是采用extends来限制泛型变量
// 实现一些只能是具有length属性的属性
interface ValueWithLength {
length: number
}
// 使用泛型继承于ValueWithLength接口来实现
let getArray6 = <T extends ValueWithLength>(arg: T, times) => {
return new Array(times).fill(arg)
}
// 我们想染在getArray6调用传参时,不想让你随意传类型变量,只想让你传代length属性的,此时就需要采用泛型约束了
getArray6([1, 2], 3) // [1, 2]为数组,有length属性
// getArray6(12, 3) 会报错,因为12为number类型,无length属性
// 4.在泛型约束中使用类型参数
// 当定义一个对象,当只能访问对象上存在的属性时,就要用到在泛型约束中使用类型参数了。
let getProps = (obj, propName) => {
return obj[propName]
}
console.log(getProps({ a: "a", b: "b" }, "a"));//a
console.log(getProps({ a: "a", b: "b" }, "c"));//返回undefined,假如我们想对这个函数当传入一个obj没有的属性名时,
// 来让用户在编译阶段就意识到这个错误,我们就需要用到泛型变量来做
//以下代码来实现上面那种需求
//keyof是一个索引类型,返回T类型变量上所有属性名构成的数组,然后K继承于这个数组,这样K就有了T中的属性名,所以以后再传propName实参时,就必须传入obj拥有的属性,否则就会报错
let getProps1 = <T, K extends keyof T>(obj: T, propName: K) => {
return obj[propName]
}
// getProps1({a: "a", b: "b"},c) //会报错,“找不到类型c”
ts中的类和es中类含义相同,但是类的写法有些不同。
// 一:ts类的基础
// ts中类的定义
class Point {
// 设定在Point构造函数上的属性,用修饰符进行修饰
public x: number
public y: number
constructor(x: number, y: number) {
this.x = x
this.y = y
}
public getPosition() {
}
}
let p = new Point(1, 2)
console.log(p);
// 父类
class Parent {
public name: string
constructor(name: string) {
this.name = name
}
}
// 子类。继承于Parent父类
class Child extends Parent {
constructor(name: string) {
super(name)
}
}
// 二:ts修饰符
// 在ts中可使用修饰符public,private来实现一些私有属性和共有属性
//1. public修饰符,表示公共属性或方法,代表外部实例可访问到的属性和方法
// 2. private修饰符,表示私有的,private修饰的属性在类的外部是无法访问的
class Pri {
private age: number
// 受保护的构造函数只能用来被子类继承,不能够用来创建实例
protected constructor(age: number) {
this.age = age
}
}
//
// let pri = new Pri(18) 会报错,因为其构造函数加上了protected修饰符,只能被子类继承,不能够创建实例。而在es6中实现是通过new.target.name来判断是谁在创建实例来实现此需求
// console.log(pri.age); //会报错。属性“age”为私有属性,只能在类“Pri”中访问。
// console.log(Pri.age); //同样会报错。 类型“typeof Pri”上不存在属性“age” ,即私有属性只能在类定义中访问,在实例或者类外(即类.属性。不同于静态属性static)不能访问
class Child1 extends Pri {
constructor(age: number) {
super(age)
// console.log(this.age); //报错,以private修饰的属性“age”为私有属性,只能在类“Pri”中访问,这也和protected修饰符区分,protected可以访问到
}
}
let child = new Child1(2)
// console.log(child.age); // //报错,以private修饰的属性“age”为私有属性,只能在类“Pri”中访问,这也和protected修饰符区分,protected可以访问到
// 3. protected修饰符。受保护的修饰符。与private相似但依然不同,protected修饰的成员(属性)在继承的子类中可以访问该成员
class Pro {
// 设置age属性为受保护的成员属性
protected age: number
constructor(age: number) {
this.age = age
}
protected getAge() {
return this.age
}
}
class Child2 extends Pro {
constructor(age: number) {
super(age)
// this关键词指向函数所在的当前对象
// super指向的是当前对象的原型对象
console.log(super.age); //undefined 在子类中也是不能访问父类的受保护的protected属性,但是可以访问父类的protected方法
console.log(super.getAge()); //23,所以在子类中可以访问父类的protected成员方法,但是不能访问父类的protected成员属性
}
}
let c1 = new Child2(23)
// console.log(c1.age);
//console.log(c1.getAge()); 在实例上既拿不到protected成员属性也拿不到protected成员方法,受保护成员属性只能在其类中拿到其属性,在其子类以及实例对象上拿不到成员属性,但是成员方法可以在其子类中拿到
//三:readonly修饰符,在类中可以设置属性是只读的,不能重新对只读属性进行赋值
class UserInfo {
// 设置name为只读属性,
public readonly name: string
constructor(name: string) {
this.name = name
}
}
let user = new UserInfo("wh")
// user.name = "wmg" //会报错 。无法分配到 "name" ,因为它是只读属性。
// 四:参数属性
class A {
// 指定name参数属性为public,表明此时name为这个类上的公共属性了,此时就可以不写this.name = name了
constructor(public name: string) {
}
}
let a1 = new A("wh")
console.log(a1.name); //wh
// 五:静态属性
// 在es中可使用static修饰符来修饰类的属性和方法,同样在ts中也存在static静态属性。使得实例不能访问静态属性和方法,只能类本身来访问静态资源
class Parent1 {
// 公共的静态属性
public static age: number = 23
// 公共的静态方法
public static getAge() {
return Parent1.age
}
}
let p1 = new Parent1()
// console.log(p1.age); //报错。属性“age”在类型“Parent1”上不存在。你的意思是改为访问静态成员“Parent1.age”吗?
console.log(Parent1.age); //23
// 六:可选类属性,也是使用问号?来标记
class Info {
public name: string
public age?: number
constructor(name: string, age?: number, public sex?: string) {
this.name = name
this.age = age
}
}
let info1 = new Info("wh")
let info2 = new Info("ww", 23, "man")
console.log(info1, info2); //Info {sex: undefined, name: 'wh', age: undefined} Info {sex: 'man', name: 'ww', age: 23}
// 七:存取器
// 也就是ES6中的存值函数set和取值函数get,即在设置属性值的时候调用存值函数,在调用属性的时候调用取值函数
class Info1 {
public name: string
public age?: number
private _infoStr: string
constructor(name: string, age?: number, public sex?: string) {
this.name = name
this.age = age
}
// 取值器
get infoStr() {
return this._infoStr
}
// 存值器,对一些属性进行赋值,value为赋的新值
set infoStr(value) {
console.log(value);
this._infoStr = value
}
}
let info3 = new Info1("wh", 23)
info3.infoStr = "wh, 23" //因为在赋值,所以会触发infoStr的存值器函数
console.log(info3.infoStr); //wh, 23,在取值,所以会触发infoStr的取值器函数
抽象类一般是用作被其他类继承,而不是用此类创建实例,使用abstract关键字来定义
// 八. 抽象类
// 抽象类一般是用作被其他类继承,而不是用此类创建实例,使用abstract关键字来定义
abstract class People {
constructor(public name: string) { }
// 抽象方法
public abstract printName(): void
}
// let p2 = new People("wh") //会报错,无法创建抽象类的实例。
// 定义类Man继承于抽象类People
class Man extends People {
constructor(public name: string) {
super(name)
}
// 必须实现一下抽象类父类的抽象方法,如果不实现,则会报非抽象类“Man”不会实现继承自“People”类的抽象成员“printName”
public printName() {
console.log(this.name);
}
}
let m = new Man("wh")
console.log(m.name); //wh
// abstract不仅可以修饰类和类里面的方法,还可以修饰类里面的属性和存取器
abstract class People1 {
// 标记_name为一个抽象属性
abstract _name: string
// 标记一个抽象取值器
abstract get insideName(): string
//标记一个抽象存值器
abstract set insideName(value: string) //注意,"set" 访问器不能具有返回类型批注,也就是不写返回类型
constructor(name: string) {
}
}
class P extends People1 {
// 实现抽象父类的一些抽象属性和抽象存取器
public _name: string
public insideName: string
constructor(name: string) {
super(name)
}
}
// 九. 实例属性
// 当定义一个类,这个类的实例的类型就是创建它的类,也就是说这个类既是一个值也是一个类型
class People2 {
constructor(public name: string) {
}
}
// 实例p2的类型就是类People2
let p2 = new People2("wh")
// 也可以显式定义实例类型:
let p3: People2 = new People2("ww")
class Animal {
constructor(public name: string) {
}
}
p3 = new Animal("wh") //这里并没有报错,这样就不太好了,因为本身p3为People2类型,而这里定义了Animal类的实例,所以就需要先用instanceof先判断实例对象的类型,确定对象类型是否一致再进行赋值
//十.接口知识点补充(与类 相关的知识)
//类来实现某个接口
// 定义一个接口
interface FoodInterface {
type: string
name: string
}
// 定义一个类来实现某个接口,与java中类似,java中也是采用implements来实现某个接口
// 在实现某个接口时,必须在此类中把接口中的所有预设属性全部实现(定义一下接口中的属性),否则会报类“FoodClass”错误实现接口“FoodInterface”。类型 "FoodClass" 中缺少属性 "type",但类型 "FoodInterface" 中需要该属性。
class FoodClass implements FoodInterface {
public type: string
public name: string;
constructor() {
}
}
// 接口继承某个类。此接口只能够继承到继承此类的成员但不包括实现,接口也可以继承类的private和protected修饰的属性和方法(成员)
// 当某个接口继承了一个类中包含有private和protected修饰符时,此接口只能被这个类和这个类的子类来实现
class B {
protected name: string
constructor(name: string) {
this.name = name
}
}
// 定义一个接口继承于类B
interface C extends B { }
//会报错,类“D”错误实现接口“C”。属性“name”受保护,但类型“D”并不是从“B”派生的类。
/**
class D implements C {
public name: string
constructor(parameters) {
}
}
*/
// 以上代码正确写法:必须要先继承父类,然后再实现这个接口
class D extends B implements C {
public name: string;
constructor(name: string) {
super(name)
}
}
// 在泛型中使用类,里面这个形参就是代表是一个类的形参,new() => T就代表将参数c限制为class类的类型。其中这个new()代表调用这个类的构造函数,
// 然后返回的就是T也就是这个类的类型
let create = <T>(c: new () => T): T => {
return new c()
}
class Info2 {
public age: number
}
// 传入一个泛型变量Info,传入的参数是一个类,这个函数就是用来你传入什么类,返回一个什么类的实例对象
console.log(create<Info2>(Info2)); //Info2 { }
枚举是ts新增的数据类型,使用枚举可以使得一些难以理解的常量赋予一组直观的名字使其更加直观,可以理解枚举就是一个字典。枚举使用enum关键字进行定义,ts支持数字和字符串两种枚举。
也就是枚举值为数字number类型
// 一. 数字枚举
// 定义一数字枚举
enum Status {
Uploading = 1, //默认不设置编码号则第一个为0,然后后续状态编码号为递增形式 1、2、3
Success, //2
Failed //3
}
// 获取上面每种状态对应的编码索引,有两种方法
// 方法一:通过点操作符后跟状态名字
console.log(Status.Uploading); //1
console.log(Status.Success); //2
//方法二:通过方括号
console.log(Status["Success"]) //2
// 二. 反向映射
// 不仅可以通过枚举字段名得到枚举值,也可以通过枚举值得到字段名(反向映射即后者)
console.log(Status); //1: "Uploading" 2: "Success" 3: "Failed"。 Failed: 3, Success: 2 ,Uploading: 1
字符串枚举,也就是枚举值为字符串
// 三. 字符串枚举,也就是枚举值为字符串
enum Message {
Error = "sorry,error!",
Success = "nice!",
// 在字符串枚举中,枚举值既可以使用常量,也可以使用这个枚举类型里面成员的字段
Failed = Error
}
console.log(Message.Success); //nice!
console.log(Message.Failed); //sorry,error!
异构枚举,简单理解就是枚举值既包含数字number类型值又包含string类型值。
// 四. 异构枚举
// 异构枚举,简单理解就是枚举值既包含数字number类型值又包含string类型值。
// 异构枚举是看需求尽量少使用异构枚举
enum Result {
Failed = 0,
Success = "nice"
}
当一个枚举值满足一定条件时,这个枚举的每一个成员和枚举本身都能够作为类型来使用,然后当将其使用类型时主要有两种情况:枚举成员类型(使用枚举成员)、联合枚举类型。
// 五.枚举成员类型和联合枚举类型
// 当一个枚举值满足一定条件时,这个枚举的每一个成员和枚举本身都能够作为类型来使用
// 满足哪些条件? 以下:
// 条件1:不带初始值的枚举成员
enum A {
Success
}
// 条件2:枚举值为字符串字面量
enum B {
Success = "success"
}
// 条件3:枚举值为数字字面量
enum C {
Suncess = -1
}
//以上三种情况就可以使用枚举成员或枚举本身来作为类型,然后当将其使用类型时主要有两种情况:枚举成员类型(使用枚举成员)、联合枚举类型
// 比如:此时Animals就属于条件3,此时此枚举的成员Dog、Cat以及枚举本身Animals就可以作为类型来使用
// 情况1:枚举成员类型
enum Animals {
Dog = 1,
Cat = 2
}
interface Dog {
// 将Dog作为类型来使用,此时Animals.Dog为类型
type: Animals.Dog
}
// 定义一个用Dog接口约束的dog对象
let dog: Dog = {
// type: Animals.Cat //会报错,因为此时type类型就是Animals.Dog值为1,不能将类型“Animals.Cat”分配给类型“Animals.Dog”。
// 此时Animals.Dog其实是值1
type: Animals.Dog
}
// 情况二:联合枚举类型
// 联合类型就是用下划线隔开的,比如: type: string | number,指的是type这个变量必须是string或者number类型的
// 在枚举中使用联合类型:
enum Status {
Off,
On
}
interface Light {
// 定义变量灯的状态类型为Status,这就表示以后status变量在赋值时必须是Status.on | Status.off联合的类型
status: Status
}
let light: Light = {
// status: Animals.Cat //会报错,所需类型来自属性 "status",在此处的 "Light" 类型上声明该属性
status: Status.On
}
也就是在代码运行时的逻辑中使用枚举值。
// 七. const enum,
// 也就是在定义枚举时,在enum前加const关键字,就是用来在编译时将枚举值编译为一个真实存在的对象,而不会在由ts编译为js代码时,将此对象显式的存在js代码中
// 比如这段ts代码在编译成js代码时,就不会显式出现在js代码中,如果去掉const则会
const enum Animals1 {
Dog = 1
}
// resizeBy.code == Animals1.Dog ,在实际开发中就是这么用的
我们在一些时候可以省略一些类型的指定,ts会帮我们推断出省略的地方适合的类型,所以可以通过学习类型推论来得出ts的推论规则。
类型兼容性就是为了适应js灵活的特点从而在一些情况下只要兼容的类型即可通过检测。
// 第一部分:类型推论
//1.基础例子:
// name1虽然没有显式指定为string类型,但是ts会推断出name1是string类型,通过字面量"wh"可得出name1是string类型,那么以后就不能为name1变量赋值除了string外的其他类型
let name1 = "wh"
// name1 = 22 //会报错,不能将类型“number”分配给类型“string”。
// 2. 多类型联合,此时arr是一个Array类型数组,可以将类型去掉也不会报错
let arr: Array<number | string> = [1, 'wh']
// 3. 上下文类型,根据window.onmousedown会推断出mouseEvent是鼠标点击事件
window.onmousedown = function (mouseEvent) {
console.log(mouseEvent);
}
// 一. 基础
// 简单例子:
interface Info {
name: string
}
let infos: Info
let info1 = { name: 'wh' }
let info2 = { age: 18 }
let info3 = {
name: 'wh',
age: 18
}
infos = info1 //正确
// infos = info2 //报错,必须要有name属性,类型 "{ age: number; }" 中缺少属性 "name",但类型 "Info" 中需要该属性。
infos = info3 //正确
//二. 函数兼容性,函数兼容性即函数中的一些属性的兼容性,比如形参个数、形参类型...
//a. 函数类型参数个数,它的要求就是赋值等号右边的函数的参数个数必须要小于等于左边的参数个数
let x = (a: number) => 0
let y = (b: number, c: string) => 0
y = x //正确
// x = y //错误,因为y有两个参数,赋值的函数参数个数必须比他少
//b. 参数类型要求
let x1 = (a: number) => 0
let x3 = (b: string) => 0
// x1 = x3 会报错,参数类型必须统一
//c.可选参数和剩余参数
let getSum = (arr: number[], callback: (...agrs: number[]) => number): number => {
return callback(...arr)
}
let res = getSum([1, 2], (...args: number[]): number => args.reduce((pre, per) => {
return pre + per
}, 0))
console.log(res); ///3
let res1 = getSum([1, 2, 3], (arg1: number, arg2: number, arg3: number): number => arg1 + arg2 + arg3)
console.log(res1); //6
// d.函数参数双向协变,也就是参数类型是联合类型时讨论双向协变
let funcA = function (arg: number | string): void { }
let funcB = function (arg: number): void { }
funcA = funcB //不会报错
funcB = funcA //也不会报错
// e. 返回值类型
let f = (): string | number => 0
let fx = (): string => 'a'
f = fx //不会报错
// fx = f //会报错,不能将返回值类型“number”分配给类型“string”。
let fy = (): boolean => true
// fx = fy //也会报错,返回值类型不同
// f. 函数重载类型兼容性
// 先定义两个函数重载
function merge(arg1: number, arg2: number): number
function merge(arg1: string, agr2: string): string
// 真正用于实现的函数体
function merge(arg1: any, arg2: any) {
return arg1 + arg2
}
merge("1", "3"); //13,返回值类型为string
merge(1, 3) //4 ,返回值类型为number
//
function sum(arg1: number, arg2: number): number
function sum(arg1: any, arg2: any): any {
return arg1 + arg2
}
let func = merge
// func = sum //报错,func有两个函数重载,而sum只有一个函数重载,所以两个函数是不兼容的,不能做赋值
// 三、讨论枚举类型兼容性
// 数字枚举类型只与数字类型兼容,在不同枚举值之间是不兼容的
enum Status {
Off,
On
}
enum Animal {
Dog,
Cat
}
let s = Status.On
s = 3 //不会报错,number类型成员枚举值类型就是number类型
// s = Animal.Dog //会报错,因为这两个变量时不兼容的,数字枚举类型只与数字类型兼容,在不同枚举值之间是不兼容的
// 四. 比较class类的类型兼容性
// 比较两个不同类的类型值的兼容性只比较实例的成员,类的静态成员和构造函数不进行比较。
class animal1 {
public static age: number
// 使用public修饰符给这个实例上添加一个name属性
constructor(public name: string) {
}
}
class animal2 {
public static age: string
constructor(public name: string) {
}
}
class animal3 {
constructor(public name: number) {
}
}
let a1: animal1
let a2: animal2
let a3: animal3
a1 = a2 //不会报错,因为实例的成员是相同的,虽然他们的静态成员age类型不同,但是类的类型兼容性不考虑静态成员和构造函数,只考虑实例成员
// a1 = a3 //会报错,实例成员中的name属性类型不同
// private私有成员和protected受保护成员会影响类型兼容,只有子类和父类之间可以相互进行值的赋值
class ParentClass {
private age: number
constructor() {
}
}
class ChildClass extends ParentClass {
constructor() {
super()
}
}
class OtherClass {
private age: number
constructor() {
}
}
let children: ParentClass = new ChildClass() //子类可以赋值给父类的值的
// let other: ParentClass = new OtherClass() //报错,不能将类型“OtherClass”分配给类型“ParentClass”。类型具有私有属性“age”的单独声明。
// 五. 泛型兼容性
// 泛型包含类型参数,这个参数可以是任何类型,在使用时,类型参数可以被指定为一个特定的类型,而这个类型只影响使用类型参数的部分
interface Data<T> {
data: T
}
let data1: Data<number>
let data2: Data<string>
// data1 = data2 //会报错,因为两个对象data1和data2中的data属性的类型不同
ts高级类型可以满足更多场景的需求。
交叉类型就是使用&符号定义,就类似于与运算
// 一.交叉类型
// 交叉类型就是使用&符号定义,就类似于与运算
// 这里此函数返回的结果是一种交叉类型,是包含了两种类型的对象
let mergeFunc = <T, U>(arg1: T, arg2: U): T & U => {
let res = {}
// Object.assign方法是返回传入的对象的合并结果
res = Object.assign(arg1, arg2)
// 要使用类型断言指定r对象es的类型
return <T & U>res
// return res as T & U //此句代码等同于上一行代码,只是这句代码是esLint的推荐写法
}
console.log(mergeFunc({ a: "a" }, { b: "b" })); //{a: 'a', b: 'b'}
使用“|”符号定义,比如:string | number表示变量可是string类型也可以是number类型
// 二.联合类型
// 使用“|”符号定义,string | number表示变量可是string类型也可以是number类型
let getLengthFunc = (content: string | number): number => {
if (typeof content === "string") {
return content.length
} else if (typeof content === "number") {
return content.toString().length
}
}
console.log(getLengthFunc("waww")); //4
console.log(getLengthFunc(2323)); //4
类型保护也就是在编译运行前我们不确定一个变量的数据类型,所以我们在编写代码时,就需要考虑到这一点,对这个变量做类型保护,也就是先判断这个变量的类型,然后根据类型的不同执行不同的逻辑。
// 三.类型保护
// 也就是在编译运行前我们不确定一个变量的数据类型,所以我们在编写代码时,就需要考虑到这一点,对这个变量做类型保护,也就是先判断这个变量的类型,然后根据类型的不同执行不同的逻辑
let value = [12.3, "es"]
let getRandomValue = () => {
let number = Math.random() * 10 // 0 - 10的数
if (number < 5) {
return value[0]
} else {
return value[1]
}
}
console.log(getRandomValue()); //打印出来的值是随机的,我们不知道他是什么类型,只有在运行编译结束之后才能判断它的类型
let item = getRandomValue()
// 定义一个类型保护,
// 次函数返回值类型为value is string,其中的is关键字是用来指定返回值类型是value is string,表示这个值是否是string类型的值,如果是返回true,否则返回false
let isString = (value: number | string): value is string => {
return typeof value === "string"
}
// 如果item是字符串,但这里在ts中会报错(虽然在js中不会出现错误),主要是因为item可能是number类型,而number类型没有length属性而报错,所以在ts中可以采用类型保护来解决此问题,也可以采用类型断言解决item as string
// 使用isString函数来进行类型保护
if (isString(item)) {
console.log(item.length);
} else {
// item是数值number类型
console.log(item.toFixed());
}
// 使用typeof关键字来进行类型保护,在ts中只能用来判断简单数据类型string、number、boolean、Symbol这四种数据类型,其他的类型不能判断,否则报错
if (typeof item === "string") {
console.log(item.length);
} else {
// item是数值number类型
console.log(item.toFixed());
}
// 也可以使用instanceof关键字来进行类型保护,它用来判断一个实例是否是某个构造函数来创建的
class CreatedByClass1 {
public name = 'wh'
constructor() {
}
}
class CreatedByClass2 {
public age = 23
constructor() {
}
}
function getRandomItem() {
return Math.random() > 0.5 ? new CreatedByClass1() : new CreatedByClass2()
}
let item1 = getRandomItem()
// 用instanceof来进行类型保护
if (item1 instanceof CreatedByClass1) {
console.log(item1.name);
} else {
console.log(item1.age);
}
// 四.null和undefined
// null和undefined是任何类型的子类型,比如let num1: number = null
// undefined也用于可选参数可可选属性,也就是用?符号来表示
// 此时y的类型其实是number|undefined类型
let sumFunc = (x: number, y?: number): number => {
return x + (y || 0)
}
// 五. 类型保护和类型断言
let getLengthFunc1 = (value: string | null): number => {
if (value === null) {
return 0
} else {
return value.length
}
}
function getSpliceStr(num: number | null): string {
function getRes(prefix: string) { //prefix为前缀
// 在变量后面加上!符号表示类型断言,表示num变量不为空null
return prefix + num!.toFixed().length.toString()
}
num = num || 0.1
return getRes('lison-')
}
console.log(getSpliceStr(null)); //lison-1
为一个类型重新去取一个名字,用type关键字来定义
// 六.类型别名
// 为一个类型重新去取一个名字,用type关键字来定义
type MyString = string
let str: MyString = "wh"
// 类型别名也可以使用泛型
type PositionType<T> = { x: T, y: T }
let position: PositionType<number> = {
x: 1,
y: -1
}
// 使用类型别名时也可以在属性中引用自己
type Childs<T> = {
current: T,
child?: Childs<T>
}
let child: Childs<string> = {
current: "wh",
// 可选属性child,也是Childs类型
child: {
current: "ww"
}
}
// 当为一个接口起别名后,不能用这个别名用作继承extends和执行implements。因为他们只是一个名字而不是真正的接口
//接口和类型别名有的时候是可以起同样作用的
type Alias = {
num: number
}
interface Interface {
num: number
}
// 类型是兼容的,既可以是Alias也可以是Interface
let _alias: Alias = {
num: 123
}
let _interface: Interface = {
num: 123
}
_alias = _interface //不会报错,说明类型是兼容的
字面量类型包括数字字面量和字符串字面量两种。即自定义一种类型(使用type定义)且类型是一种常量数字或字符串字面量
// 七. 字面量类型
// 字面量类型包括数字字面量和字符串字面量两种
// 1.字符串字面量类型
// Name为"wh"这个字符串字面量类型
type Name = "wh"
let name: Name = 'wh'
// 字符串字面量的联合类型
type Direction = "north" | "sorth" | "west" | "east"
// 此时形参direction的类型就是字符串字面量的联合类型
function getDirectionFistLetter(direction: Direction): string {
return direction.slice(0, 1)
}
console.log(getDirectionFistLetter("east")); //e
// 2.数字字面量类型
// Age就是一个数字字面量类型
type Age = 18
interface InfoInterface {
name: string
age: Age
}
let _info: InfoInterface = {
name: "wh",
age: 18 //age只能为18,因为age的类型为18数字字面量类型
}
// 八. 枚举成员类型
// 能够作为类型的枚举要符合三个条件:条件1:不带初始值的枚举成员,条件2:枚举值为字符串字面量,条件3:枚举值为数字字面量
// 九. 可辨识联合
// 即把单例类型(多指枚举成员类型和字面量类型)、联合类型、类型保护和类型别名这几种类型进行合并来创建一个叫做可辨识联合的高级类型,也可以称作标签联合或者代数数据类型
/**
* 可辨识联合需要有两个要素:
* 1. 具有普通的单例类型属性
* 2. 一个类型别名包含了哪些类型的联合,即把几个类型封装为联合类型并起别名
*/
interface Square {
kind: "square"
size: number
}
interface Rectangle {
kind: "rectangle"
height: number
width: number
}
interface Circle {
kind: "circle"
radius: number
}
// 定义一个类型,它是三个接口的联合,并为其起个别名为Shape
type Shape = Square | Rectangle | Circle
function assertNever(value: never): never {
throw new Error(`Unexpected object: ${value}`)
}
// 求面积函数
function getArea(s: Shape): number {
switch (s.kind) {
case "square": {
// 因为这是square分支的逻辑,所以他会自动判断s就是squre接口的类型,会自动把可以访问的属性列出来,这就是可以辨识的联合即可辨识联合
return s.size * s.size
}
case "rectangle": {
return s.height * s.width
}
// 如果缺少,circle分支判断,将会报错,函数缺少结束 return 语句,返回类型不包括 "undefined"。
case "circle": {
return Math.PI * s.radius ** 2
}
// 完整性检查
default: {
return assertNever(s)
}
}
}
let area = getArea({
kind: "rectangle",
width: 3,
height: 4
})
console.log(area); //12
在js中,this可以用来获取对全局对象、类实例对象、构建函数实例的引用,在ts中,this也是一种类型。
// 一. this类型
// 在js中,this可以用来获取对全局对象、类实例对象、构建函数实例的引用,在ts中,this也是一种类型
class Counter {
constructor(public count: number) { }
// 加法
add(value: number) {
this.count += value
// 重新return this的原因就是我们可以连续的做加减法,即可以做链式调用操作
return this
}
// 减法
substract(value: number) {
this.count -= value
return this
}
}
let counter = new Counter(10)
console.log(counter.add(3).substract(3)); //10
class PowCounter extends Counter {
constructor(public count: number = 0) {
super(count)
}
// 求幂运算
pow(value: number) {
// es7幂运算符**
this.count = this.count ** value
return this
}
}
let powCounter = new PowCounter(2)
console.log(powCounter.pow(3).add(2)); //10
索引类型主要包含两种内容,即索引类型查询和索引访问这两个操作符
索引类型查询操作符keyof,其连接一个类型,返回一个由这个类型所有属性名组成的联合类型
// 二. 索引类型
// 索引类型主要包含两种内容,即索引类型查询和索引访问这两个操作符
// 1.索引类型查询操作符keyof,其连接一个类型,返回一个由这个类型所有属性名组成的联合类型
interface info {
name: string
age: number
}
let infoProp: keyof info // 现在info和这个变量的属性就是"name"|"age"字面量类型的联合类型
infoProp = "name"
infoProp = "age"
// infoprop = "ww" //会报错,因为infoProp只能为"name"或者"age"字面量类型
// 通过与泛型结合使用,ts就可以检查使用了动态属性名的代码
// 即泛型变量K就是使用了T类型里面所有属性构成的联合类型
function getValue<T, K extends keyof T>(obj: T, names: K[]): T[K][] { //T[K][]推荐写成Array
return names.map(key => obj[key])
}
let obj = {
name: "wh",
age: 23
}
let infos: Array<number | string> = getValue(obj, ["name"])
console.log(infos); //["wh"] , 即属性值构成的数组
索引访问操作符:就是方括号[],也就是访问对象的某个属性值是类似的,只不过在ts中他是可以访问某个属性的类型。
// 2.索引访问操作符
// 索引访问操作符:就是方括号[],也就是访问对象的某个属性值是类似的,只不过在ts中他是可以访问某个属性的类型
// 举个栗子:
type NameType = info["name"] //索引访问操作符[]来访问info接口的name属性类型
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
return o[name]
}
interface Obj<T> {
// 接口的属性写法,不固定属性名称,只需要保证属性名是string类型即可,属性在写的时候也可以是number类型,如果是number类型,他会先编译转换成string类型
[key: string]: T
}
let objs: Obj<number> = {
age: 23
}
let keys: Obj<number>["name"] //keys为number类型
interface Type {
a: never
b: never
c: string
d: number
e: undefined
f: null
g: object
}
// keyof索引查询操作符不会返回never类型的属性名
type Test = Type[keyof Type] //等同于type Test = string | number | object | null | undefined
ts提供了借助旧类型创建一个新类型的方式,也就是映射类型,他可以以相同的形式去转换旧类型中每一个属性。
// 三. 映射类型
// ts提供了借助旧类型创建一个新类型的方式,也就是映射类型,他可以以相同的形式去转换旧类型中每一个属性
interface Info {
age: number
name: string
sex: string
}
// 将age修饰为readonly只读属性,可以重新创建一个接口,但是比较麻烦,所以ts提供了映射类型来简化这个过程
interface ReadonlyType {
readonly age: number
}
// 使用映射根据旧类型创建一个新类型,
type ReadonlyType1<T> = {
// 其中in类似于for in做一个循环,P可以理解为是keyof T循环的每一项,并为其每一项添加readonly修饰符
readonly [P in keyof T]?: T[P]
}
type ReadonlyInfo1 = ReadonlyType1<Info>
let info1: ReadonlyInfo1 = {
name: "wh",
age: 23,
sex: "man"
}
// info1.name = "ww" //报错,name属性为只读属性,不可修改
// ts内置了只读属性和可选属性,叫做Readonly和Partial,比如:
type InfoReadonly = Readonly<Info>
type InfoPartial = Partial<Info>
// ts也内置了Pick、Record等类型,直接看例子用法:
// Pick使用
interface Info2 {
name: string
age: number
address: string
}
let info2: Info2 = {
name: "wh",
age: 23,
address: "heNan"
}
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
let res: any = {}
keys.map(key => {
res[key] = obj[key]
})
return res
}
let nameAndAddress = pick(info2, ["name", "address"])
console.log(nameAndAddress); //{name: 'wh', address: 'heNan'}
// Record使用
function mapObject<K extends string | number, T, U>(obj: Record<K, T>, f: (x: T) => U) {
let res: any = {}
for (const key in obj) {
res[key] = f(obj[key])
}
return res
}
let names = {
0: "hello",
1: "world",
2: "by3"
}
let length = mapObject(names, (s) => s.length)
console.log(length);
// 3.2. 由映射类型进行推断
// 我们学习了使用映射类型包装一个属性之后,也可以使用逆向操作进行拆包
// 例子:
type Proxy<T> = {
get(): T
set(value: T): void
}
type Proxify<T> = {
[P in keyof T]: Proxy<T[P]>
}
function proxify<T>(obj: T): Proxify<T> {
let res = {} as Proxify<T>
for (let key in obj) {
res[key] = {
get: () => obj[key],
set: (value) => obj[key] = value
}
}
return res
}
let props = {
name: "wh",
age: 23
}
let proxyProps = proxify(props)
proxyProps.name.set("ww")
console.log(proxyProps.name.get()); //ww
// 进行拆包
function unProxify<T>(t: Proxify<T>): T {
// 使用类型断言强制使res为T泛型变量类型
let res = {} as T
for (const k in t) {
res[k] = t[k].get()
}
return res
}
let originalProps = unProxify(proxyProps)
console.log(originalProps);
// 3.3增加或移除特定修饰符
// 使用+ -这两个符号作为前缀来删除修饰符
type RemoveReadonly<T> = {
// 代表移除T中的readonly属性和可选属性?
-readonly [P in keyof T]-?: T[P]
}
type infoWithoutReadonly = RemoveReadonly<ReadonlyInfo1> //此时infoWithoutReadonly的属性就没有readonly属性了
// 3.4 keyof操作符映射类型在ts2.9版本的升级
// ts的2.9版本keyof和映射类型是支持用number和Symbol命名属性的
// keyof:
// 用作计算属性只能用const关键字来声明,因为[]内只能是Symbol类型和文本,声明Symbol类型要用const关键字来定义
const stringIndex = "a"
const numberIndex = 1
const symbolIndex = Symbol()
type Objs = {
[stringIndex]: string
[numberIndex]: number
[symbolIndex]: symbol
}
type objs2 = keyof Objs
type ReadonlyTypes<T> = {
readonly [P in keyof T]: T[P]
}
let objs3: ReadonlyTypes<Objs> = {
a: "aa",
1: 11,
[symbolIndex]: Symbol()
}
// objs3.a = "ww" //不能修改,因为a为只读属性
// 3.5 元组和数组上的映射类型
// 元组和数组上的映射类型会生成新的元组和数组,并不会创建新的类型
// 定义一个映射类型
type MapToPromise<T> = {
[K in keyof T]: Promise<T[K]>
}
// 定义一个元组类型
type Tuple = [number, string, boolean]
type PromiseTuple = MapToPromise<Tuple>
let tuple1: PromiseTuple = [
new Promise((resolve, reject) => resolve(1)),
new Promise((resolve, reject) => resolve("a")),
new Promise((resolve, reject) => resolve(false)),
]
简单来说,unknown 是一种更加安全的 any 的副本。所有东西都可以被标记成是 unknown 类型,但是 unkonwn 必须在进行类型判断和条件控制之后才可以被分配成其他类型,并且在类型判断和条件控制之前也不能进行任何操作
//四.顶级类型unknown
// 简单来说,unknown 是一种更加安全的 any 的副本。任何变量都可以被标记成是 unknown 类型,但是 unkonwn 必须在进行类型判断和条件控制之后才可以被分配成其他类型,并且在类型判断和条件控制之前也不能进行任何操作。
// 4.1 任何类型都可以赋值给unknown类型
let value1: unknown
value1 = 2
value1 = "wh"
// 4.2 如果没有类型断言或基于控制流的类型细化时,unknown不可以赋值给其他类型,此时他只能赋值给unknown和any类型
let value2: unknown
// let value3: string = value2 //报错,因为unknown类型的变量不能赋值给其他类型
value1 = value2 //不报错
// 4.3 如果没有类型断言或基于控制流的类型细化时,不能在他上面进行任何操作
let value4: unknown
// value4 += 1 //报错
// 4.4. unknown与任何类型组成的交叉类型,最后都等于其他类型
type type1 = string & unknown //string类型
type type2 = number & unknown //number类型
type type3 = unknown & unknown //unknown类型
//4.5. unknown与其他类型(除了any)组成的联合类型,都等于unknown类型
type type4 = unknown | string //unknown类型
// 4.6. never类型是unknown的子类型
type type5 = never extends unknown ? true : false //true
// 4.7. keyof unknown 等于类型never
type type6 = keyof unknown //type type6 = never
// 4.8. 只能对unknown进行等或不登操作,不能践行其他操作
value1 == value2
// value1 += value2 //报错
// 4.9 unknown类型的值不能访问他的属性、不能作为函数调用和作为类创建实例
let value5: unknown
// 10. 使用映射类型如果遍历的是unknow类型,则不会映射任何属性
type Types1<T> = {
[P in keyof T]: number
}
type type7 = Types1<any> // {[x: string]: number;}
type type8 = Types1<unknown> //空字段,没有任何属性 {}
// 5.1. 条件类型类似于三元运算符,比如: 1 > 2 ? true: false
type Types2<T> = T extends string ? string : number
let index: Types2<"a"> //其中“a”为字符串字面量类型,其继承于string类型。string类型
// 5.2. 分布式条件类型
// 当待检测类型是联合类型的时候,该条件类型就被称为分布式条件类型,在实例化时ts会自动的分化为联合类型
type TypeName<T> = T extends any ? T : never
type Type3 = TypeName<string | number> //string | number
// 官方文档例子,不断层级判断
type TypeName1<T> =
T extends string ? string :
T extends number ? number :
T extends boolean ? boolean :
T extends undefined ? undefined :
T extends Function ? Function :
object
type Type4 = TypeName1<boolean> //boolean
// 分布式条件类型实际应用:
type Diff<T, U> = T extends U ? never : T
type Test1 = Diff<string | number | boolean, undefined | number> //string | boolean
// 条件类型和映射类型结合的例子
type Type5<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T]
interface Part {
id: number
name: string
subparts: Part[]
undatePart(newName: string): void
}
type Test2 = Type5<Part> //type Test2 = "undatePart"
// 5.3 条件类型的类型推断 使用infer关键字,来推断类型
// 不使用infer来推断类型
type Type6<T> = T extends any[] ? T[number] : T
type Test3 = Type6<string[]> //string
type Test4 = Type6<string> // string
// 使用infer来实现以上代码功能
// 使用infer来修泛型变量U
type Type7<T> = T extends Array<infer U> ? U : T
type Test5 = Type7<string[]> //string[]属于Array,并使用infer推断数组中的元素属性是string,即泛型变量U为string类型
type Test6 = Type7<string> //string
// 5.4 ts提供的预定义条件类型
// Exclude 排除,在前面类型T中选出不是U类型的类型
type Type8 = Exclude<"a" | "b" | "c", "a"> //type Type8 = "b" | "c"
// Extract
type Type9 = Extract<"a" | "b" | "c", "a"> //type Type9 = "a"
// NonNullabe,返回不为undefined和null剩余的类型
type Type10 = NonNullable<string | number | null | undefined> //type Type10 = string | number
// ReturnType 获取函数类型的返回的值类型
type Type11 = ReturnType<() => string> //type Type11 = string
// instanceType 返回构造函数类型的实例类型
class Aclass {
constructor() { }
}
type T1 = InstanceType<typeof Aclass> //type T1 = Aclass
type T2 = InstanceType<any> //type T2 = any
type T3 = InstanceType<never> //type T3 = never
export { }
ts模块学习之前需掌握ES6中的模块化。
nodejs模块是遵循common.js规范的。使用require()导入,module.exports和exports导出。
使用:exports.name = “wh” //相当于ES6模块中的export
module.exports = “wh” //默认导出,相当于ES6模块中的export default
从ts1.5版本开始,内部模块称为命名空间,外部模块称为模块。
ts模块除了遵循ES6的模块语法外还有些特定语法。如下:
export除了可导出变量、类、函数外,还可以导出接口。
//a.ts
export interface FuncInterface {
name: string
(arg: number): string
}
export class ClassA {
constructor() { }
}
class CLassB {
constructor() { }
}
// 导出
export {
// 重命名
CLassB as B
}
// 导入bt中的变量后再导出,
export * from "./b" //相当于这两句:import { name } from ".b" export name
export { age } from "./b"
export { name as n } from "./b"
//b.ts
export const name = "wh"
export const age = 23
//c.ts
let c = 22
// 默认导出,且一个模块中只能有一个默认导出
export default c
//index.ts
// 引入b中的部分内容
import { name } from "./b"
// 引入全部内容
import * as info from "./b"
console.log(info); //{name: 'wh', age: 23, __esModule: true}
import * as A from "./a"
// export default导出的变量引入:
import c from "./c"
console.log(A);
因为ES6的export default 和commonjs规范中的module.exports默认导出是不兼容的,所以ts为了兼容这两种写法,ts增加了export = *和import ** = require()这两个语句。
// 使用export = 语句进行导出
export = name1
// 对于使用export =语句导出的变量,导入时:
import name1 = require("./c")
命名空间又称为内部模块。
当想要设计一个程序内部防止全局污染时把相关的内容放在一起时使用命名空间。
当我们封装了一个工具要适应与模块系统中引入时适合使用模块。
// 引用命名空间,写法:///,注意是3个斜线
///
在编译时要使用tsc --outFile命令:tsc --outFile .\src\index.js .\src\ts-modules\index.ts进行编译并输出到src下的index.js文件中
如:
// 引用命名空间,写法:///,注意是3个斜线
///
let isLetter = Validation.checkLetter("abc")
console.log(isLetter);
当使用tsc对这个命名空间进行编译后,生成的js脚本:
// 设计一个命名空间,把用于内容验证的方法放在一起,这时就需要用到命名空间了。
// 命名空间写法:
var Validation;
(function (Validation) {
var isLetterReg = /^[A-Za-z]+$/; //+:匹配前面的子表达式一次或多次(大于等于1次)。
Validation.isNumber = /^[0-9]+$/;
Validation.checkLetter = function (text) {
return isLetterReg.test(text);
};
})(Validation || (Validation = {}));
// 引用命名空间,写法:///,注意是3个斜线
///
var isLetter = Validation.checkLetter("abc");
console.log(isLetter);
使用import取别名,可以解决深层次嵌套的命名空间问题。如:
namespace Shapes {
export namespace Polygons {
export class Triangle { }
export class Square { }
}
}
// 使用import为其取别名
import polygons = Shapes.Polygons
let triangle = new polygons.Triangle()
相对路径导入有三种方式: /表示根目录 ./表示当前目录 …/表示上一级目录
其余的方式都属于非相对模块导入。
ts中由两种解析策略可以选择,即node和classic。在tsconfig.json文件中的moduleResolution选择配置。
baseUrl": “./”:配置导出文件的目录文件夹 /* Specify the base directory to resolve non-relative module names. */
“paths”: {}, :一般指你下载的第三方文件的存储目录文件夹配置,比如:
"baseUrl": "./", //其中"./"代表是当前目录文件夹下
"paths": {
"*": [ //*代表匹配的任意下载的第三方模块,比如vue、jquery等等,当开起来paths选项后就必须开启baseurl选项
"node_modules/@types",
"src/typings"
]
},
声明合并是指ts编译器会将名字相同的多个声明合并为一个声明,合并后的声明会同时有多个声明的特性。
在js中,使用var关键在定义变量时,定义相同名字的变量,会使后面的变量的值覆盖前面变量值;而使用const和let不能重复定义相同名字的变量,否则会报错。
// 类型合并
// 接口类型合并
// 例子:
interface InfoInter {
name: string
}
interface InfoInter { //因为接口名字相同,合并了了上面的InfoInter接口,也就是说现在InfoInter接口已具有name和age属性
age: number
}
let infoInter: InfoInter
infoInter = { //因为此时此变量是需要使用{age:number , name: string}接口来定义
name: "wh",
age: 23
}
定义:装饰器是一种新的声明,它可以作用于类的声明方法访问服务属性和参数上,使用@+一个名字,这个名字必须是一个函数或者这个函数执行完求完值之后返回的是一个函数。
// 装饰器
//定义:装饰器是一种新的声明,它可以作用于类的声明方法访问服务属性和参数上,使用@+一个名字,这个名字必须是一个函数或者这个函数执行完求完值之后返回的是一个函数
function setProp(target) {
// ....
return function (target) { }
}
// @setProp()
// 举个装饰器栗子:
// 定义一个装饰器工厂
function setName() {
console.log("get setName");
// 需要返回装饰器
return function (target) {
console.log("setName");
}
}
// 再定义一个setAge装饰器工厂
function setAge() {
console.log("get setAge");
return function (target) {
console.log("setAge");
}
}
// 运用,把它作为类装饰器运用在类的声明上
@setName()
@setAge()
// 然后定义类之前来修饰它
class ClassDec { } //执行顺序:get setName; get setAge; setAge ; setName
configrable可配置、writable可重写、enumerable可枚举遍历。
interface objWithAnyKeys {
[key: string]: any
}
let obj: objWithAnyKeys = {}
Object.defineProperty(obj, "name", {
value: "wh1", //默认初始值为wh
// 此属性不可重写
writable: true, //可以重新赋值
configurable: true, //可配置,可以对obj的name属性重新进行配置
enumerable: true, //可枚举,for of时可以遍历出
// set() { }, //重新赋值触发set方法
// get() { } //获取值时触发get方法
})
console.log(obj.name);
混入就是把两个对象或者类的内容混合在一起,从而实现一些功能的复用
// 对象的混入:使用Object.assign(obj1, obj2)进行混入
interface ObjectA {
a: string
}
interface ObjectB {
b: string
}
let a: ObjectA = {
a: "a"
}
let b: ObjectB = {
b: "b"
}
// 现在我们将这两个对象进行和并混入,使用Object.assign方法
let ab: ObjectA & ObjectB = Object.assign(a, b)
console.log(ab); //{a: 'a', b: 'b'}
// 2.类的混入
class A {
public isA: boolean
public funcA() { }
}
class B {
public isB: boolean
public funcB() { }
}
// 对类A和类B进行合并混入,这里是使用implements执行接口
class AB implements A, B {
constructor() { }
public isA: boolean = true;
public isB: boolean = true;
public funcA: () => void
public funcB: () => void
}
class C { }
function mixins(base: any, from: any[]) {
from.forEach(fromItem => {
// 来获取每一个类的原型对象,然后使用getOwnPropertyNames获取类的原型对象上的属性构成一数组
Object.getOwnPropertyNames(fromItem.prototype).forEach(key => {
// console.log(key);
base.prototype[key] = fromItem.prototype[key]
})
})
}
mixins(AB, [A, B])
let ab1 = new AB()
console.log(ab1)
// mixins(C, [A, B])
// // console.log(C.isA);
// // C["funcA"] = function A() { }
// // C.prototype["name1"] = "wh"
// console.log(C.prototype);
// 1.promise、async相关
interface Res {
data: {
[key: string]: any
}
}
// 定义一个命名空间/内部模块
namespace axios {
export function post(url: string, config: object): Promise<Res> {
return new Promise((resolve, reject) => {
setTimeout(() => {
let res: Res = { data: {} }
if (url === "/login") {
res.data.user_id = 111
} else {
res.data.role = "admin"
}
console.log(2);
resolve(res)
}, 1000)
})
}
}
interface Info {
user_name: string
password: string
}
async function loginReq({ user_name, password }: Info) { //结构赋值
try {
console.log(1);
let res = await axios.post("/login", {
data: {
user_name,
password
}
})
console.log(3);
return res
} catch (error) {
throw new Error(error)
}
}
async function getRoleReq(user_id: string) { //结构赋值
try {
console.log(1);
let res = await axios.post("/user_roles", {
data: {
user_id
}
})
console.log(4);
return res
} catch (error) {
throw new Error(error)
}
}
// 注意,若一个函数使用了async关键字来修饰,那么此函数就会返回一个Promise对象
loginReq({ user_name: "wh", password: "123" }).then((res) => {
let { data: { user_id } } = res //进行解构赋值
console.log(user_id);//111
getRoleReq(user_id).then((res) => {
let { data: { role } } = res
console.log(role); //"admin"
})
})
//2.动态导入表达式
async function getTime(format: string) {
//动态导入模块,专门用来格式化时间的模块moment模块
let moment = await import("moment") //注意:import其实返回的是一个promise对象,所以可以用await关键字来修饰,使其为同步代码块
return moment.default().format(format)
}
getTime("cc").then((res) => {
console.log(res);
})
// 3.弱类型探测,任何只包含可选属性的类型都会被当做弱类型
interface ObjIn { //因为ObjIn接口中的所有属性都是可选属性,所以ObjIn就是弱类型
name?: string
age?: number
}
let obj = {
sex: "man"
}
function printInfo(info: ObjIn) {
console.log(info);
}
// printInfo(obj) //会报错。类型“{ sex: string; }”与类型“ObjIn”不具有相同的属性。
printInfo(obj as ObjIn) //使用类型断言就不会报错了
// ...拓展运算符,支持泛型
function mergeOption<T, U extends string>(op1: T, op2: U) {
return { ...op1, op2 } //返回的结果是将op1与op2对象属性合并的结果
}
let mergeobj = mergeOption<object, string>({ age: 23 }, "name")
console.log(mergeobj); //{age: 23, op2: 'name'}
js库一般分为全局库(仅支持前端页面 script标签的src导入)、模块化库(一般用作在后台import导入export导出这些)、umd库(既支持前端script标签导入也支持服务器端import导入)。
// 自己编写的全局库
function setTitle(title) {
document && (document.title = title) // 如果存在document,才会执行后面的语句
}
function getTitle() {
return document ? document.title : ""
}
let documentTitle = getTitle()
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documew1nttitle>
head>
<script src="../module//handle-title.js">script>
<body>
<div>22div>
body>
<script>
console.log(222);
script>
html>
但是这样还是会报错不能引入,这是因为使用相对路径…/在编译之后是不能够被引入的,我们可以使用copy-webpack-plugin插件来拷贝此文件到输出目录dist目录。
// 自己编写的全局库,handle-title.js
function setTitle(title) {
document && (document.title = title) // 如果存在document,才会执行后面的语句
}
function getTitle() {
return document ? document.title : ""
}
let documentTitle = getTitle()
console.log("wwww");
然后在src下设置全局声明文件global.d.ts文件(.d.ts就是一种全局声明文件),在文件中使用declare关键字进行声明,只有声明了之后,才能在全局中使用自己写的全局库handle-title.js。
// 全局声明文件, 是用来为全局库中的一些函数或者变量先进行声明,然后才能在全局中使用这些定义的函数或者变量
// 因为是全局的,所以要是用declare来修饰
declare function setTitle(title: string | number): void
declare function getTitle(): string
declare let documentTitle: string
模块化库代代码中一般会出现import、export导入导出语句。
umd库将全局库和模块化库的功能进行结合,塔湖UI首先判断环境是否有模块加载器一些特定方法比如define,typeof define == “function” 或者typeof module == “object”&&module.export说明有模块加载系统要执行模块化库的逻辑,否则就执行全局库的逻辑;判断是否有window对象来判断是否执行全局库逻辑。
// 如果要引入全局库,那么就使用///reference来引入
///
//如果我们要引入模块库,那么我们就用import导入
import * as moment from "moment"
//如果要引入umd库,也可以使用来引入
第一步:首先创建一个根项目,然后在这个项目下npm init初始化包管理文件。然后再tsc --init初始化tsconfig.json文件。
第二步:编写要封装的文件,这里我要封装的是数组的一个map映射方法;
const arrMap = (array: any[], callback: (item: any, i?: number, arr?: any[]) => any): any[] => {
let resArray: any[] = []
for (let i = 0; i < array.length; i++) {
resArray.push(callback(array[i], i, array))
}
return resArray
}
export = arrMap //使用export = 语句导出
第三步:终端执行tsc命令,进行编译。编译后会在dist目录下产生.d.ts声明文件以及输出的js文件。
第四步:使用npm login进行登录npm账号
第五步:使用npm publish进行发包
cnpm i express-generator -g
这时就可以使用express命令来创建一个项目了
express --view=jade server //其中使用jade视图来创建项目模板,并为项目起名为server
cnpm i typescript
cnpm i @types/express @types/node -D //开发依赖
tsc --init
npm i mysql
npm i @types/mysql -D
。。。。。。。。。。。。。
vue ui来配置
创建项目:
持续学习更新中…