一介*书生
愿你熬过苦难,依旧努力生活。

1.枚举enum

1.1、数字枚举:

使用枚举我们可以定义一些带名字的常量,使用枚举可以清晰地表达意图或创建一组有区别的用例,程序中能灵活的使用枚举(enum),会让程序有更好的可读性

enum Status {
    text1= 1,
    text2,
    text3,
}
这里能打印出枚举的值(也有叫下标的),那如果我们知道下标后,也可以通过反差的方法,得到枚举的值,此时text1就为1,text2为2,text3为3
如果text1没有赋值,那么排序就直接从0开始排,如果从中间位置开始, 数字枚举成员还具有了 反向映射,从枚举值到枚举名字。

1.2、字符串枚举:

enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}

由于字符串枚举没有自增长的行为,字符串枚举可以很好的序列化,还有其他很多枚举类型,但是不常用,用到时,才去研究吧,比如异型枚举

enum BooleanLikeHeterogeneousEnum {
  No = 0,
  Yes = "YES",
}

1.3、const枚举:

大多数情况下,枚举是十分有效的方案。 然而在某些情况下需求很严格。 为了避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问,我们可以使用 const枚举。

常量枚举通过在枚举上使用 const修饰符来定义。

const enum Enum {
  A = 1,
  B = A * 2
}

1.4、外部枚举:

外部枚举用来描述已经存在的枚举类型的形状
declare enum Enum {
  A = 1,
  B,
  C = 2
}
外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里,没有初始化方法的成员被当成常数成员。 对于非常数的外部枚举而言,没有初始化方法时被当做需要经过计算的。
最后:

枚举是不是看起来有点类似js中镜像翻转后的数组,又有点像序列化之后的对象是吧?

我开始也是这么认为的,那么这个枚举类型到底有什么用处,或者说和已有类型(对象和数组)有什么使用上的区别呢?

在百度了一些资料后,我得出的结论是:

ts中的枚举类型和普通的js对象本质上没有区别,只是对于开发者来说,相较于直接使用值类型去做判断,枚举类型更易读,能够提升代码的可读性和易维护性。

 

2、泛型(难点)

泛型的定义使用<>(尖角号)进行定义的,里面写类型参数,一般可以用T来表示,简单来说,泛型中的 T就像一个占位符,或者说一个变量,在使用的时候可以把

定义的类型像参数一样传入,它可以原封不动地输出。(泛型就是动态灵活的去控制类型)

泛型在造轮子的时候经常使用,因为造轮子很多东西都需要灵活性。泛型给了我们很好的灵活性。需要注意的是,如果函数定义了多个泛型,使用时要对应的定义出具体的类型

function join<T, P>(first: T, second: P) {
    return `${first}${second}`;
  }
join < number, string > (1, "2");
虽然泛型也可以自己进行类型推断,但是不建议,最好在使用时指明其类型,这会让你的代码简单易读写。
function join1<T, P>(first: T, second: P) {
    return `${first}${second}`;
  }
join1(1, "2");
 比如:我们写一个类,实现对我们实例化传入的值,进行一个输出,可能会像下面写:
class SelectGirl {
    constructor(private girls: string[]) {}
    getGirl(index: number): string {
      return this.girls[index];
    }
  }
const selectGirl = new SelectGirl(["大脚", "刘英", "晓红"]);
console.log(selectGirl.getGirl(1));
 但是如果我们把实例化时传入的参数改成[12,122,33],我们在进行取值,这样返回的类型就不再说固定的一个类型了,此时就需要使用泛型了
class SelectGirl2<T> {
    constructor(private girls: T[]) {}
    getGirl(index: number): T {
      return this.girls[index];
    }
  }
  const selectGirl2 = new SelectGirl2<string>(["大脚", "刘英", "晓红"]);
  console.log(selectGirl2.getGirl(1));

1、泛型的默认参数

interface Print<T = string> {
    name: T
  }
const myPrint:Print = {
    name:'2323'
}
function print<T>(arg: T):T{
    return arg
}
print<string>('1212')

 2、泛型约束

假设现在有一个函数,打印传入参数的长度,我们是这么写的
function printLength<T>(arg: T): T {
    console.log(arg.length)
}
因为不确定T是否有length属性,会报错,现在想要约束这个泛型,就一定要有length属性,我们就可以结合interface来约束
interface ILength{
    length: number
}
//泛型继承接口
function printLength<T extends ILength>(arg: T): T {
    console.log(arg.length)
    return arg
}
printLength({length: 4, name: 'ceshi'})
如果调用时传入的参数里面没有length属性,那么ts就会报错,提示必须包含次属性,这其中的关键是<T extends ILength>,让这个泛型继承接口,这样就会约束泛型啦
处理多个函数参数:
现在有这么一个函数,传入一个只有两项的元组,交换元组的第 0 项和第 1 项,返回这个元组。
function swap(tuple) {
    return [tuple[1], tuple[0]]
}
这么写,我们就丧失了类型,用泛型来改造一下
们用 T 代表第 0 项的类型,用 U 代表第 1 项的类型
function swap1<T, U>(tuple: [T, U]): [U, T]{
    return [tuple[1], tuple[0]]
}
swap1<number, string>([12, 'hhhh'])
这样就可以实现了元组第 0 项和第 1 项类型的控制
第 0 项上全是 number 的方法,如下:

 

 第1项上就全是string的方法了,如下:

 

函数副作用操作:

如我们在项目中写api接口的时候,想知道函数返回的接口数据结构,那么我们就可以像下面那种方式写:

interface UserInfo {
    name: string
    age: number
}
function request<T>(url:string): Promise<T> {
    return fetch(url).then(res => res.json())
}
request<UserInfo>('user/info').then(res =>{
    console.log(res)
})
样就能很舒服地拿到接口返回的数据类型,开发效率大大提高

 

 

3、泛型约束类

定义一个栈,有入栈和出栈两个方法,如果想入栈和出栈的元素类型统一,就可以这么写:
class Stack<T> {
    public data: T[] = []
    push(item:T) {
        return this.data.push(item)
    }
    pop():T | undefined {
        return this.data.pop()
    }
}
const s1 = new Stack<number>()
s1.push(12)
console.log(s1.data)
这样,入栈一个字符串就会报错

 

 

特别注意的是,泛型无法约束类的静态成员

 

 4、泛型约束接口

使用泛型,也可以对 interface 进行改造,让 interface 更灵活

interface IKeyValue<T, U> {
    key: T
    value: U
}
const k1:IKeyValue<number, string> = { key: 18, value: 'lin'}
const k2:IKeyValue<string, number> = { key: 'lin', value: 18}

5、泛型定义数组

 定义一个数组,我们之前是这么写的:const arr: number[] = [1,2,3]
现在这么写也可以:const arr: Array<number> = [1,2,3]
实战:泛型约束后端接口参数类型
interface API {
    '/book/detail': {
        id: number,
    },
    '/book/comment': {
        id: number
        comment: string
    }
}
function request<T extends keyof API>(url: T, obj: API[T]) {
    return axios.post(url, obj)
}
request('/book/comment', {
    id: 1,
    comment: '非常棒!'
})
 如果路径写错了,就会提醒:

 

 泛型的用处还有很多地方,就不一样举例

下面用一张图来概括泛型吧:

 

 3.命名空间

命名空间简介:

命名空间(在早期版本的 TypeScript 中称为“内部模块”)是一种用于组织和分类代码的 TypeScript 特定方式,使你能够将相关代码组合在一起。 命名空间允许将与业务规则相关的变量、函数、接口或类分组到一个命名空间,将安全性分组到另一个命名空间。

命名空间内的代码将从全局范围拉入到命名空间范围。 这种布局有助于避免全局命名空间中组件之间的命名冲突,并且在与可能使用类似组件名称的分布式开发团队合作时也会有好处。

命名空间特点

  • 减少全局范围内的代码量,限制“全局范围污染”。
  • 为名称提供上下文,有助于减少命名冲突。
  • 提高可重用性。

TypeScript 中命名空间使用 namespace 来定义,例如:

namespace namespaceA{
	export interface interfaceA{}
	export class classA{}
	function getUserInfo() {
		let name = getName()
		return console.log(name + ',I am 26 years old')
	}
	function getName() {
		return 'my name is Yj'
	}
}
interfaceA  // false,没有添加命名空间名称前缀namespaceA
namespaceA.interfaceA    // true
namespaceA.classA  // true
namespaceA.getName  // false,没有export关键字
namespaceA.getUserInfo  // 'my name is Yj, I am 26 years old'
以上定义了一个命名空间 namespaceA,如果我们需要在外部可以调用 namespaceA中的类和接口,则需要在类和接口添加 export 关键字。
命名空间中定义的所有组件的作用域都限定为命名空间,并从全局范围中删除
使用嵌套命名空间组织代码:在命名空间中嵌套命名空间,从而提供更多的选项来组织代码,个人理解类似于在一个对象里面找对象属性的属性。
namespace namespaceA{
	export namespaceB{
		export functionA() {
			namespaceC.functionB
		}
	}
	export namespaceC{
		export functionB() {
			return console.log('这是命名空间c里面的函数b!')
		}
		export functionC() {
			return console.log('这是命名空间C里面的函数C!')
		}
	}
}
namespaceA.namespaceB.functionA  // 这是命名空间c里面的函数b!
namespaceA.namespaceC.functionC  // 这是命名空间C里面的函数C!
定义命名空间别名:
TypeScript 创建一个易于导航的嵌套命名空间层次结构。 但是,随着嵌套命名空间变得越来越复杂,你可能需要创建一个别名来缩短和简化代码。 为此,请使用 import 关键字。
//namespaceA 和 namespaceB来自上面的代码
import nameA = namespaceA.namespaceB;
nameA.functionA();  // '这是命名空间c里面的函数b!'
编译单一文件命名空间:

编译单一文件命名空间的方式与编译任何其他 TypeScript 文件的方式相同。 因为命名空间是一个只包含 TypeScript 的构造,所以会从生成的 JavaScript 代码中删除它们,并将其转换为必要时嵌套的变量,以形成类似命名空间的对象。

如果一个命名空间在一个单独的 TypeScript 文件中,则应使用三斜杠 /// 引用它,语法格式如下:

/// <reference path = "SomeFileName.ts" />

使用多文件命名空间来组织代码:

可以通过跨多个 TypeScript 文件共享命名空间来扩展它们。 如果在多个文件中具有彼此相关的命名空间,则必须添加 reference 标记,使 TypeScript 编译器知道文件之间的关系。 例如,假定你有三个 Typescript 文件:

  • interfaces.ts,它声明包含某些接口定义的命名空间。
  • functions.ts,该文件使用在 interfaces.ts 中实现接口的函数声明命名空间。
  • main.ts,它调用 functions.ts 中的函数,并表示应用程序的主代码。

若要告知 TypeScript interfaces.ts 与 functions.ts 之间的关系,请在 functions.ts 顶部使用三斜杠 (///) 语法向 interfaces.ts 添加 reference。 然后,在与 interfaces.ts 和 functions.ts 关联的 main.ts 中,将 reference 添加到这两个文件。

 

 

  • 如果对多个文件进行引用,请从最高级别的命名空间开始,然后逐渐向下。 在编译文件时,TypeScript 将使用此顺序。
  • 如果几个ts文件里的命名空间名称一样, 尽管是不同的文件,它们仍是同一个命名空间,并且在使用的时候就如同它们在一个文件中定义的一样。
  • 因为不同文件之间存在依赖关系,所以我们还是要采用上面方式加入了引用标签来告诉编译器文件之间的关联,只是使用的时候可以直接用命名空间里面的方法,不用在加上命名空间去点取。

 最后小结一下:

命名空间本质是一个闭包,用来隔离作用域

TS命名空间是对全局变量时代的兼容

在一个完全模块化的系统中,不必使用命名空间

 

 

posted on 2023-02-19 18:45  一介-_-书生  阅读(200)  评论(0编辑  收藏  举报