栈数据结构基本操作(数栈技术分享前端篇)

数栈是—站式大数据开发平台,我们在github和gitee上有一个有趣的开源项目:FlinkX,FlinkX是一个基于Flink的批流统一的数据同步工具,既可以采集静态的数据,也可以采集实时变化的数据,是全域、异构、批流一体的数据同步引擎。大家喜欢的话请给我们点个star!star!star!

github开源项目:https://github.com/DTStack/flinkx

gitee开源项目:https://gitee.com/dtstack_dev_0/flinkx

有兴趣的话,欢迎大家加入我们的交流社群:30537511(钉钉群)

写在前面

本文难度偏中下,涉及到的点大多为如何在项目中合理应用TS,小部分会涉及一些原理,受众面较广,有无TS基础均可放心食用

阅读完本文,您可能会收获到:

1、若您还不熟悉 TS,那本文可帮助您完成 TS 应用部分的学习,伴随众多 Demo 例来引导业务应用。

2、若您比较熟悉 TS,那本文可当作复习文,带您回顾知识,希望能在某些点引发您新发现和思考。

3、针对于 class 组件的 IState 和 IProps,类比 Hook 组件的部分写法和思考。

TIPS:超好用的在线 TS 编辑器(诸多配置项可手动配置)

传送门:https://www.TypeScriptlang.org/

什么是 TS

不扯晦涩的概念,通俗来说 TypeScript 就是 JavaScript 的超集,它具有可选的类型,并可以编译为纯 JavaScript 运行。(笔者一直就把 typeScript 看作 JavaScript 的 Lint)

那么问题来了,为什么 TS 一定要设计成静态的?或者换句话说,我们为什么需要向 JavaScript 添加类型规范呢 ?

经典自问自答环节——因为它可以解决一些 JS 尚未解决的痛点:1、JS 是动态类型的语言,这也意味着在实例化之前我们都不知道变量的类型,但是使用 TS 可以在运行前就避免经典低级错误。

例:Uncaught TypeError:'xxx' is not a function⚠️ 典中典级别的错误 :

栈数据结构基本操作(数栈技术分享前端篇)(1)

栈数据结构基本操作(数栈技术分享前端篇)(2)

栈数据结构基本操作(数栈技术分享前端篇)(3)

栈数据结构基本操作(数栈技术分享前端篇)(4)

栈数据结构基本操作(数栈技术分享前端篇)(5)

栈数据结构基本操作(数栈技术分享前端篇)(6)

JS 就是这样,只有在运行时发生了错误才告诉我有错,但是当 TS 介入后:

栈数据结构基本操作(数栈技术分享前端篇)(7)

栈数据结构基本操作(数栈技术分享前端篇)(8)

栈数据结构基本操作(数栈技术分享前端篇)(9)

栈数据结构基本操作(数栈技术分享前端篇)(10)

好家伙!直接把问题在编辑器阶段抛出,nice!

2、懒人狂欢! 规范方便,又不容易出错,对于 VS Code,它能做的最多只是标示出有没有这个属性,但并不能精确的表明这个属性是什么类型,但 TS 可以通过类型推导/反推导(说白话:如果您未明确编写类型,则将使用类型推断来推断您正在使用的类型),从而完美优化了代码补全这一项:

栈数据结构基本操作(数栈技术分享前端篇)(11)

栈数据结构基本操作(数栈技术分享前端篇)(12)

栈数据结构基本操作(数栈技术分享前端篇)(13)

栈数据结构基本操作(数栈技术分享前端篇)(14)

栈数据结构基本操作(数栈技术分享前端篇)(15)

1)第一个 Q&A——思考 :提问:那么我们还能想到在业务开发中 TS 解决了哪些 JS 的痛点呢?回答,总结,补充:

  • 对函数参数的类型限制;
  • 对数组和对象的类型限制,避免定义出错 例如数据解构复杂或较多时,可能会出现数组定义错误 a = { }, if (a.length){ // xxxxx }
  • let functionA = 'jiawen' // 实际上 let functionA: string = 'jiawen'

3、使我们的应用代码更易阅读和维护,如果定义完善,可以通过类型大致明白参数的作用。相信通过上述简单的bug-demo,各位已对TS有了一个初步的重新认识 接下来的章节便正式介绍我们在业务开发过程中如何用好TS。

怎么用 TS

在业务中如何用TS/如何用好TS?这个问题其实和 " 在业务中怎么用好一个API " 是一样的。首先要知道这个东西在干嘛,参数是什么,规则是什么,能够接受有哪些扩展......等等。简而言之,撸它!哪些扩展......等等。 简而言之,撸它!

栈数据结构基本操作(数栈技术分享前端篇)(16)

栈数据结构基本操作(数栈技术分享前端篇)(17)

栈数据结构基本操作(数栈技术分享前端篇)(18)

栈数据结构基本操作(数栈技术分享前端篇)(19)

1、TS 常用类型归纳

通过对业务中常见的 TS 错误做出的一个综合性总结归纳,希望 Demos 会对您有收获

1)元语(primitives)之 string number boolean

笔者把基本类型拆开的原因是: 不管是中文还是英文文档,primitives/元语/元组 这几个名词都频繁出镜,笔者理解的白话:希望在类型约束定义时,使用的是字面量而不是内置对象类型,官方文档:

栈数据结构基本操作(数栈技术分享前端篇)(20)

栈数据结构基本操作(数栈技术分享前端篇)(21)

let a: string = 'jiawen';let flag: boolean = false;let num: number = 150interface IState: { flag: boolean; name: string; num: number;}

栈数据结构基本操作(数栈技术分享前端篇)(22)

栈数据结构基本操作(数栈技术分享前端篇)(23)

2)元组

// 元组类型表示已知元素数量和类型的数组,各元素的类型不必相同,但是对应位置的类型需要相同。 let x: [string, number]; x = ['jiawen', 18]; // ok x = [18, 'jiawen']; // Erro console.log(x[0]); // jiawen

栈数据结构基本操作(数栈技术分享前端篇)(24)

3)undefined null

let special: string = undefined // 值得一提的是 undefined/null 是所有基本类型的子类, // 所以它们可以任意赋值给其他已定义的类型,这也是为什么上述代码不报错的原因

栈数据结构基本操作(数栈技术分享前端篇)(25)

4)object 和 { }

// object 表示的是常规的 Javascript对象类型,非基础数据类型 const offDuty = (value: object) => { console.log("value is ", value); } offDuty({ prop: 0}) // ok offDuty(null) offDuty(undefined) // Error offDuty(18) offDuty('offDuty') offDuty(false) // Error // {} 表示的是 非null / 非undefined 的任意类型 const offDuty = (value: {}) => { console.log("value is ", value); } offDuty({ prop: 0}) // ok offDuty(null) offDuty(undefined) // Error offDuty(18) offDuty('offDuty') offDuty(false) // ok offDuty({ toString(){ return 333 } }) // ok // {} 和Object几乎一致,区别是Object会对Object内置的 toString/hasOwnPreperty 进行校验 const offDuty = (value: Object) => { console.log("value is ", value); } offDuty({ prop: 0}) // ok offDuty(null) offDuty(undefined) // Error offDuty(18) offDuty('offDuty') offDuty(false) // ok offDuty({ toString(){ return 333 } }) // Error 如果需要一个对象类型,但对属性没有要求,建议使用 object {} 和 Object 表示的范围太大,建议尽量不要使用

栈数据结构基本操作(数栈技术分享前端篇)(26)

5)object of params

// 我们通常在业务中可多采用点状对象函数(规定参数对象类型) const offDuty = (value: { x: number; y: string }) => { console.log("x is ", value.x); console.log("y is ", value.y); } // 业务中一定会涉及到"可选属性";先简单介绍下方便快捷的“可选属性” const offDuty = (value: { x: number; y?: string }) => { console.log("必选属性x ", value.x); console.log("可选属性y ", value.y); console.log("可选属性y的方法 ", value.y.toLocaleLowerCase()); } offDuty({ x: 123, y: 'jiawen' }) offDuty({ x: 123 }) // 提问:上述代码有问题吗? 答案: // offDuty({ x: 123 }) 会导致结果报错value.y.toLocaleLowerCase() // Cannot read property 'toLocaleLowerCase' of undefined 方案1: 手动类型检查 const offDuty = (value: { x: number; y?: string }) => { if (value.y !== undefined) { console.log("可能不存在的 ", value.y.toUpperCase()); } } 方案2:使用可选属性 (推荐) const offDuty = (value: { x: number; y?: string }) => { console.log("可能不存在的 ", value.y?.toLocaleLowerCase()); }

栈数据结构基本操作(数栈技术分享前端篇)(27)

6)unknown 与 any

// unknown 可以表示任意类型,但它同时也告诉TS, 开发者对类型也是无法确定,做任何操作时需要慎重 let Jiaven: unknown Jiaven.toFixed(1) // Error if (typeof Jiaven=== 'number') { Jiaven.toFixed(1) // OK } 当我们使用any类型的时候,any会逃离类型检查,并且any类型的变量可以执行任意操作,编译时不会报错 anyscript === javascript 注意:any 会增加了运行时出错的风险,不到万不得已不要使用; 如果遇到想要表示【不知道什么类型】的场景,推荐优先考虑 unknown

栈数据结构基本操作(数栈技术分享前端篇)(28)

7)union 联合类型

union也叫联合类型,由两个或多个其他类型组成,表示可能为任何一个的值,类型之间用 ' | '隔开 type dayOff = string | number | boolean 联合类型的隐式推导可能会导致错误,遇到相关问题请参考语雀 code and tips —— 《TS的隐式推导》 .值得注意的是,如果访问不共有的属性的时候,会报错,访问共有属性时不会.上个最直观的demo function dayOff (value: string | number): number { return value.length; } // number并不具备length,会报错,解决方法:typeof value === 'string' function dayOff (value: string | number): number { return value.toString(); } // number和string都具备toString(),不会报错

栈数据结构基本操作(数栈技术分享前端篇)(29)

8)never

// never是其它类型(包括 null 和 undefined)的子类型,代表从不会出现的值。 // 那never在实际开发中到底有什么作用?这里笔者原汁原味照搬尤雨溪的经典解释来做第一个例子 第一个例子,当你有一个 union type: interface Foo { type: 'foo' } interface Bar { type: 'bar' } type All = Foo | Bar 在 switch 当中判断 type,TS是可以收窄类型的 (discriminated union): function handleValue(val: All) { switch (val.type) { case 'foo': // 这里 val 被收窄为 Foo break case 'bar': // val 在这里是 Bar break default: // val 在这里是 never const exhaustiveCheck: never = val break } } 注意在 default 里面我们把被收窄为 never 的 val 赋值给一个显式声明为 never 的变量。 如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事改了 All 的类型: type All = Foo | Bar | Baz 然而他忘记了在 handleValue 里面加上针对 Baz 的处理逻辑, 这个时候在 default branch 里面 val 会被收窄为 Baz,导致无法赋值给 never,产生一个编译错误。 所以通过这个办法,你可以确保 handleValue 总是穷尽 (exhaust) 了所有 All 的可能类型

栈数据结构基本操作(数栈技术分享前端篇)(30)

第二个用法 返回值为 never 的函数可以是抛出异常的情况 function error(message: string): never { throw new Error(message); } 第三个用法 返回值为 never 的函数可以是无法被执行到的终止点的情况 function loop(): never { while (true) {} }

栈数据结构基本操作(数栈技术分享前端篇)(31)

9)Void

interface IProps { onOK: () => void } void 和 undefined 功能高度类似,但void表示对函数的返回值并不在意或该方法并无返回值

栈数据结构基本操作(数栈技术分享前端篇)(32)

10)Enum

笔者认为ts中的enum是一个很有趣的枚举类型,它的底层就是number的实现 1.普通枚举 enum Color { Red, Green, Blue }; let c: Color = Color.Blue; console.log(c); // 2 2.字符串枚举 enum Color { Red = 'red', Green = 'not red', }; 3.异构枚举 / 有时也叫混合枚举 enum Color { Red = 'red', Num = 2, };

栈数据结构基本操作(数栈技术分享前端篇)(33)

<第一个坑> enum Color { A, // 0 B, // 1 C = 20, // 20 D, // 21 E = 100, // 100 F, // 101 } 若初始化有部分赋值,那么后续成员的值为上一个成员的值加1

栈数据结构基本操作(数栈技术分享前端篇)(34)

<第二个坑> 这个坑是第一个坑的延展,稍不仔细就会上当! const getValue = () => { return 23 } enum List { A = getValue(), B = 24, // 此处必须要初始化值,不然编译不通过 C } console.log(List.A) // 23 console.log(List.B) // 24 console.log(List.C) // 25 如果某个属性的值是计算出来的,那么它后面一位的成员必须要初始化值。 否则将会 Enum member must have initializer.

栈数据结构基本操作(数栈技术分享前端篇)(35)

11)泛型

笔者理解的泛型很白话:先不指定具体类型,通过传入的参数类型来得到具体类型 我们从下述的 filter-demo 入手,探索一下为什么一定需要泛型

泛型的基础样式

function fun<T>(args: T): T { return args }

栈数据结构基本操作(数栈技术分享前端篇)(36)

如果没接触过,是不是会觉得有点懵?没关系!我们直接从业务角度深入。

1.刚开始的需求:过滤数字类型的数组 declare function filter( array: number[], fn: (item: unknown) => boolean ) : number[]; 2.产品改了需求:还要过滤一些字符串 string[] 彳亍,那就利用函数的重载, 加一个声明, 虽然笨了点,但是很好理解 declare function filter( array: string[], fn: (item: unknown) => boolean ): string[]; declare function filter( array: number[], fn: (item: unknown) => boolean ): number[]; 3.产品又来了! 这次还要过滤 boolean[]、object[] .......... 这个时候如果还是选择重载,将会大大提升工作量,代码也会变得越来越累赘,这个时候泛型就出场了, 它从实现上来说更像是一种方法,通过你的传参来定义类型,改造如下: declare function filter<T>( array: T[], fn: (item: unknown) => boolean ): T[];

栈数据结构基本操作(数栈技术分享前端篇)(37)

当我们把泛型理解为一种方法实现后,那么我们便很自然的联想到:方法有多个参数、默认值,泛型也可以。

type Foo<T, U = string> = { // 多参数、默认值 foo: Array<T> // 可以传递 bar: U } type A = Foo<number> // type A = { foo: number[]; bar: string; } type B = Foo<number, number> // type B = { foo: number[]; bar: number; }

栈数据结构基本操作(数栈技术分享前端篇)(38)

既然是“函数”,那也会有“限制”,下文列举一些稍微常见的约束。

1. extends: 限制 T 必须至少是一个 XXX 的类型 type dayOff<T extends HTMLElement = HTMLElement> = { where: T, name: string }

栈数据结构基本操作(数栈技术分享前端篇)(39)

2. readonly<T>: 构造一个所有属性为readonly,这意味着无法重新分配所构造类型的属性。 interface Eat { food: string; } const todo: Readonly<Eat> = { food: "meat beef milk", }; todo.food = "no food"; // Cannot assign to 'title' because it is a read-only property.

栈数据结构基本操作(数栈技术分享前端篇)(40)

3. Pick<T,K>: 从T中挑选出一些K属性 interface Todo { name: string; job: string; work: boolean; type TodoPreview = Pick<Todo, "name" | "work">; const todo: TodoPreview = { name: "jiawen", work: true, }; todo;

栈数据结构基本操作(数栈技术分享前端篇)(41)

4. Omit<T, K>: 结合了 T 和 K 并忽略对象类型中 K 来构造类型。 interface Todo { name: string; job: string; work: boolean; } type TodoPreview = Omit<Todo, "work">; const todo: TodoPreview = { name: "jiawen", job: 'job', };

栈数据结构基本操作(数栈技术分享前端篇)(42)

5.Record: 约束 定义键类型为 Keys、值类型为 Values 的对象类型。 enum Num { A = 10001, B = 10002, C = 10003 } const NumMap: Record<Num, string> = { [Num.A]: 'this is A', [Num.B]: 'this is B' } // 类型 "{ 10001: string; 10002: string; }" 中缺少属性 "10003", // 但类型 "Record<ErrorCodes, string>" 中需要该属性,所以我们还可以通过Record来做全面性检查 keyof 关键字可以用来获取一个对象类型的所有 key 类型 type User = { id: string; name: string; }; type UserKeys = keyof User; // "id" | "name" 改造如下 type Record<K extends keyof any, T> = { [P in K]: T; }; 此时的 T 为 any;

栈数据结构基本操作(数栈技术分享前端篇)(43)

还有一些不常用,但是很易懂的: 6. Extract<T, U> 从T,U中提取相同的类型 7. Partial<T> 所有属性可选 type User = { id?: string, gender: 'male' | 'female' } type PartialUser = Partial<User> // { id?: string, gender?: 'male' | 'female'} type Partial<T> = { [U in keyof T]?: T[U] } 8. Required<T> 所有属性必须 << === >> 与Partial相反 type User = { id?: string, sex: 'male' | 'female' } type RequiredUser = Required<User> // { readonly id: string, readonly gender: 'male' | 'female'} function showUserProfile (user: RequiredUser) { console.log(user.id) // 这时候就不需要再加?了 console.log(user.sex) } type Required<T> = { [U in keyof T]-?: T[U] }; -? : 代表去掉?

栈数据结构基本操作(数栈技术分享前端篇)(44)

TS的一些须知1、TS 的 type 和 interface

1)interface(接口) 只能声明对象类型,支持声明合并(可扩展)。

interface User { id: string}interface User { name: string}const user = {} as Userconsole.log(user.id);console.log(user.name);

栈数据结构基本操作(数栈技术分享前端篇)(45)

2)type(类型别名)不支持声明合并 -- l类型

type User = { id: string, } if (true) { type User = { name: string, } const user = {} as User; console.log(user.name); console.log(user.id) // 类型“User”上不存在属性“id”。 }

栈数据结构基本操作(数栈技术分享前端篇)(46)

栈数据结构基本操作(数栈技术分享前端篇)(47)

栈数据结构基本操作(数栈技术分享前端篇)(48)

3)type 和 interface 异同点总结:

a、通常来讲 type 更为通用,右侧可以是任意类型,包括表达式运算,以及映射等;

b、凡是可用 interface 来定义的,type 也可;

c、扩展方式也不同,interface 可以用 extends 关键字进行扩展,或用来 implements 实现某个接口;

d、都可以用来描述一个对象或者函数;

e、type 可以声明基本类型别名、联合类型、元组类型,interface 不行;

f、⚠️ 但如果你是在开发一个包,模块,允许别人进行扩展就用 interface,如果需要定义基础数据类型或者需要类型运算,使用 type;

g、interface 可以被多次定义,并会被视作合并声明,而 type 不支持;

h、导出方式不同,interface 支持同时声明并默认导出,而 typetype 必须先声明后导出;r/>

2、TS 的脚本模式和模块模式

Typescript 存在两种模式,区分的逻辑是,文件内容包不包含 import 或者 export 关键字 。

1)脚本模式(Script), 一个文件对应一个 html 的 script 标签 。

2)模块模式(Module),一个文件对应一个 Typescript 的模块。

脚本模式下,所有变量定义,类型声明都是全局的,多个文件定义同一个变量会报错,同名 interface 会进行合并;而模块模式下,所有变量定义,类型声明都是模块内有效的。

两种模式在编写类型声明时也有区别,例如脚本模式下直接 declare var GlobalStore 即可为全局对象编写声明。

例子:

脚本模式下直接 declare var GlobalStore 即可为全局对象编写声明。

GlobalStore.foo = "foo"; GlobalStore.bar = "bar"; // Error declare var GlobalStore: { foo: string; };

栈数据结构基本操作(数栈技术分享前端篇)(49)

模块模式下,要为全局对象编写声明需要 declare global

GlobalStore.foo = "foo"; GlobalStore.bar = "bar"; declare global { var GlobalStore: { foo: string; bar: string; }; } export {}; // export 关键字改变文件的模式

栈数据结构基本操作(数栈技术分享前端篇)(50)

3、TS 的索引签名索引签名可以用来定义对象内的属性、值的类型,例如定义一个 React 组件,允许 Props 可以传任意 key 为 string,value 为 number 的 props

interface Props { [key: string]: number } <Component count={1} /> // OK <Component count={true} /> // Error <Component count={'1'} /> // Error

栈数据结构基本操作(数栈技术分享前端篇)(51)

4、TS 的类型键入Typescript 允许像对象取属性值一样使用类型

type User = { userId: string friendList: { fristName: string lastName: string }[] } type UserIdType = User['userId'] // string type FriendList = User['friendList'] // { fristName: string; lastName: string; }[] type Friend = FriendList[number] // { fristName: string; lastName: string; }

栈数据结构基本操作(数栈技术分享前端篇)(52)

在上面的例子中,我们利用类型键入的功能从 User 类型中计算出了其他的几种类型。FriendList[number]这里的 number 是关键字,用来取数组子项的类型。在元组中也可以使用字面量数字得到数组元素的类型。

type group = [number, string] type First = group[0] // number type Second = group[1] // string

栈数据结构基本操作(数栈技术分享前端篇)(53)

5、TS 的断言1)类型断言不是类型转换,断言成一个联合类型中不存在的类型是不允许的。

function getLength(value: string | number): number { if (value.length) { return value.length; } else { return value.toString().length; } // 这个问题在object of parmas已经提及,不再赘述 修改后: if ((<string>value).length) { return (<string>value).length; } else { return something.toString().length; } }

栈数据结构基本操作(数栈技术分享前端篇)(54)

断言的两种写法 1. <类型>值: <string>value 2. 或者 value as string 特别注意!!!断言成一个联合类型中不存在的类型是不允许的 function toBoolean(something: string | number): boolean { return <boolean>something; }

栈数据结构基本操作(数栈技术分享前端篇)(55)

2)非空断言符

TypeScript 还具有一种特殊的语法,用于从类型中删除 null 和 undefined 不进行任何显式检查。

在任何表达式之后写入实际上是一个类型断言,表明该值不是 null 或 undefined

function liveDangerously(x?: number | undefined | null) { // 推荐写法 console.log(x!.toFixed()); }

栈数据结构基本操作(数栈技术分享前端篇)(56)

如何在 Hook 组件中使用 TS1、usestateuseState 如果初始值不是 null/undefined 的话,是具备类型推导能力的,根据传入的初始值推断出类型;初始值是 null/undefined 的话则需要传递类型定义才能进行约束。一般情况下,还是推荐传入类型(通过 useState 的第一个泛型参数)。

// 这里ts可以推断 value的类型并且能对setValue函数调用进行约束 const [value, setValue] = useState(0); interface MyObject { name: string; age?: number; } // 这里需要传递MyObject才能约束 value, setValue // 所以我们一般情况下推荐传入类型 const [value, setValue] = useState<MyObject>(null);

栈数据结构基本操作(数栈技术分享前端篇)(57)

2)useEffect useLayoutEffect

没有返回值,无需类型传递和约束

3)useMemo useCallback
  • useMemo无需传递类型, 根据函数的返回值就能推断出类型。
  • useCallback无需传递类型,根据函数的返回值就能推断出类型。

但是注意函数的入参需要定义类型,不然将会推断为any!

const value = 10; const result = useMemo(() => value * 2, [value]); // 推断出result是number类型 const multiplier = 2; // 推断出 (value: number) => number // 注意函数入参value需要定义类型 const multiply = useCallback((value: number) => value * multiplier, [multiplier]);

栈数据结构基本操作(数栈技术分享前端篇)(58)

4)useRefuseRef传非空初始值的时候可以推断类型,同样也可以通过传入第一个泛型参数来定义类型,约束ref.current的类型。

1. 如果传值为null const MyInput = () => { const inputRef = useRef<HTMLInputElement>(null); // 这里约束inputRef是一个html元素 return <input ref={inputRef} /> } 2. 如果不为null const myNumberRef = useRef(0); // 自动推断出 myNumberRef.current 是number类型 myNumberRef.current = 1;

栈数据结构基本操作(数栈技术分享前端篇)(59)

5)useContextuseContext一般根据传入的Context的值就可以推断出返回值。一般无需显示传递类型。

type Theme = 'light' | 'dark';// 我们在createContext就传了类型了const ThemeContext = createContext<Theme>('dark');const App = () => ( <ThemeContext.Provider value="dark"> <MyComponent /> </ThemeContext.Provider>)const MyComponent = () => { // useContext根据ThemeContext推断出类型,这里不需要显示传 const theme = useContext(ThemeContext); return <div>The theme is {theme}</div>

栈数据结构基本操作(数栈技术分享前端篇)(60)

一些思考

在本文中笔者对TS的基础应用和Hook中的TS做了一些思考,但关于关于TSC如何把TS代码转换为JS代码的内容,这个部分比较冗长,后续可以单独出一篇文章(2)来专门探索。关于TS泛型的底层实现,这个部分比较复杂,笔者还需沉淀,欢迎各位直接留言或在文章中补充!!!

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页