网站开发 一般用什么语言合肥网站建设博客

当前位置: 首页 > news >正文

网站开发 一般用什么语言,合肥网站建设博客,阿里云做网站预装环境,网站维护一般要几天TS 与 JS 的区别 TypeScript[4] 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集#xff0c;而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。– 官方文档 说人话就是 TS 拓展了 JS 的一些功能#xff0c;解决了 JS 的一些缺点#…TS 与 JS 的区别 TypeScript[4] 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。– 官方文档 说人话就是 TS 拓展了 JS 的一些功能解决了 JS 的一些缺点可以总结在下面的表格里 TypeScriptJavaScriptJavaScript 的超集用于解决大型项目的代码复杂性一种脚本语言用于创建动态网页。强类型支持静态和动态类型动态弱类型语言可以在编译期间发现并纠正错误只能在运行时发现错误不允许改变变量的数据类型变量可以被赋予不同类型的值 关于强类型、弱类型、静态类型和动态类型语言可以看我的这篇文章[5]。 用一张图来描述一下 TS 和 JS 的关系 image.png JS 有的 TS 都有 JS 没有的 TS 也有毕竟 TS 是 JS 的超集嘛。 TS 的缺点 不能被浏览器理解需要被编译成 JS有学习成本写习惯了 JS 的我们要上手需要花时间去理解而且 TS 中有一些概念还是有点难比如泛型。 TS 基础 这一部分的内容是比较简单的有 JS 基础的同学把例子写一遍就理解了。 基础类型 boolean、number 和 string 类型 boolean let isHandsome: boolean true
复制代码赋值与定义的不一致会报错静态类型语言的优势就体现出来了可以帮助我们提前发现代码中的错误。number let age: number 18 复制代码string let realName: string lin let fullName: string A \({realName} // 支持模板字符串 复制代码undefined 和 null 类型 let u:undefined undefined // undefined 类型let n:null null // null 类型复制代码默认情况下 null 和 undefined 是所有类型的子类型。就是说你可以把 null 和 undefined 赋值给 number 类型的变量。 let age: number nulllet realName: string undefined复制代码但是如果指定了 --strictNullChecks 标记null 和 undefined 只能赋值给 void 和它们各自不然会报错。 image.png any、unknown 和 void 类型 any 不清楚用什么类型可以使用 any 类型。这些值可能来自于动态的内容比如来自用户输入或第三方代码库 let notSure: any 4 notSure maybe a string // 可以是 string 类型 notSure false // 也可以是 boolean 类型 notSure.name // 可以随便调用属性和方法 notSure.getName() 不建议使用 any不然就丧失了 TS 的意义。 unknown 类型 不建议使用 any当我不知道一个类型具体是什么时该怎么办 可以使用 unknown 类型 unknown 类型代表任何类型它的定义和 any 定义很像但是它是一个安全类型使用 unknown 做任何事情都是不合法的。 比如这样一个 divide 函数 function divide(param: any) {return param / 2; } 把 param 定义为 any 类型TS 就能编译通过没有把潜在的风险暴露出来万一传的不是 number 类型不就没有达到预期了吗。 把 param 定义为 unknown 类型 TS 编译器就能拦住潜在风险如下图 function divide(param: unknown) {return param / 2; } image.png 因为不知道 param 的类型使用运算符 /导致报错。 再配合类型断言即可解决这个问题 function divide(param: unknown) {return param as number / 2; }void void类型与 any 类型相反它表示没有任何类型。 比如函数没有明确返回值默认返回 Void 类型 function welcome(): void {console.log(hello) }never 类型 never类型表示的是那些永不存在的值的类型。 有些情况下值会永不存在比如 如果一个函数执行时抛出了异常那么这个函数永远不存在返回值因为抛出异常会直接中断程序运行。函数中执行无限循环的代码使得程序永远无法运行到函数返回值那一步。 // 异常function fn(msg: string): never { throw new Error(msg)}// 死循环 千万别这么写会内存溢出 function fn(): never { while (true) {}} never 类型是任何类型的子类型也可以赋值给任何类型。 没有类型是 never 的子类型没有类型可以赋值给 never 类型除了 never 本身之外。即使 any也不可以赋值给 never 。 let test1: never;test1 lin // 报错Type string is not assignable to type neverlet test1: never;let test2: any;test1 test2 // 报错Type any is not assignable to type never 数组类型 let list: number[] [1, 2, 3]list.push(4) // 可以调用数组上的方法 数组里的项写错类型会报错 image.png push 时类型对不上会报错 image.png 如果数组想每一项放入不同数据怎么办用元组类型 元组类型 元组类型允许表示一个已知元素数量和类型的数组各元素的类型不必相同。 let tuple: [number, string] [18, lin] 写错类型会报错 image.png 越界会报错 image.png 可以对元组使用数组的方法比如使用 push 时不会有越界报错 let tuple: [number, string] [18, lin]tuple.push(100) // 但是只能 push 定义的 number 或者 string 类型push 一个没有定义的类型报错 函数类型 TS 定义函数类型需要定义输入参数类型和输出类型。 输出类型也可以忽略因为 TS 能够根据返回语句自动推断出返回值类型。 function add(x:number, y:number):number {return x y } add(1,2) 函数没有明确返回值默认返回 Void 类型 function welcome(): void {console.log(hello); }函数表达式写法 let add2 (x: number, y: number): number {return x y }可选参数 参数后加个问号代表这个参数是可选的 function add(x:number, y:number, z?:number):number {return x y } add(1,2,3) add(1,2)注意可选参数要放在函数入参的最后面不然会导致编译错误。 image.png 默认参数 function add(x:number, y:number 100):number {return x y } add(100) // 200 跟 JS 的写法一样在入参里定义初始值。 和可选参数不同的是默认参数可以不放在函数入参的最后面 function add(x:number 100, y:number):number {return x y } add(100) 看上面的代码add 函数只传了一个参数如果理所当然地觉得 x 有默认值只传一个就传的是 y 的话就会报错 image.png 编译器会判定你只传了 x没传 y。 如果带默认值的参数不是最后一个参数用户必须明确的传入 undefined值来获得默认值。 add(undefined,100) // 200函数赋值 JS 中变量随便赋值没问题 image.png 但在 TS 中函数不能随便赋值会报错的 image.png 也可以用下面这种方式定义一个函数 add3把 add2 赋值给 add3 let add2 (x: number, y: number): number {return x y } const add3:(x: number, y: number) number add2有点像 es6 中的箭头函数但不是箭头函数TS 遇到 : 就知道后面的代码是写类型用的。 当然不用定义 add3 类型直接赋值也可以TS 会在变量赋值的过程中自动推断类型如下图 image.png interface 基本概念 interface(接口) 是 TS 设计出来用于定义对象类型的可以对对象的形状进行描述。 定义 interface 一般首字母大写代码如下 interface Person {name: stringage: number } const p1: Person {name: lin,age: 18 }属性必须和类型定义的时候完全一致。 少写了属性报错 image.png 多写了属性报错 image.png 类型提示显著提升开发效率 image.png 注意interface 不是 JS 中的关键字所以 TS 编译成 JS 之后这些 interface 是不会被转换过去的都会被删除掉interface 只是在 TS 中用来做静态检查。 可选属性 跟函数的可选参数是类似的在属性上加个 ?这个属性就是可选的比如下面的 age 属性 interface Person {name: stringage?: number } const p1: Person {name: lin, } 只读属性 如果希望某个属性不被改变可以这么写 interface Person {readonly id: numbername: stringage: number} 改变这个只读属性时会报错。 image.png interface 描述函数类型 interface 也可以用来描述函数类型代码如下 interface ISum {(x:number,y:number):number}const add:ISum (num1, num2) {return num1 num2 } 自定义属性可索引的类型 上文中属性必须和类型定义的时候完全一致如果一个对象上有多个不确定的属性怎么办 可以这么写。 interface RandomKey {[propName: string]: string} const obj: RandomKey {a: hello,b: lin,c: welcome, } 如果把属性名定义为 number 类型就是一个类数组了看上去和数组一模一样。 interface LikeArray {[propName: number]: string } const arr: LikeArray [hello, lin] arr[0] // 可以使用下标来访问值 当然不是真的数组数组上的方法它是没有的。 image.png duck typing(鸭子类型[6]) 看到这里你会发现interface 的写法非常灵活它不是教条主义。 用 interface 可以创造一系列自定义的类型。 事实上 interface 还有一个响亮的名称duck typing(鸭子类型)。 当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子那么这只鸟就可以被称为鸭子。 – James Whitcomb Riley[7] 这句话完美地诠释了 interface 的含义只要数据满足了 interface 定义的类型TS 就可以编译通过。 举个例子 interface FunctionWithProps {(x: number): numberfnName: string} FunctionWithProps 接口描述了一个函数类型还向这个函数类型添加了 name 属性这看上去完全是四不像但是这个定义是完全可以工作的。 const fn: FunctionWithProps (x) {return x }fn.fnName hello world 这就是 duck typing 和 interface非常的灵活。 类 我们知道 JS 是靠原型和原型链来实现面向对象编程的es6 新增了语法糖 class。 TS 通过 public、private、protected 三个修饰符来增强了 JS 中的类。 在 TS 中写法和 JS 差不多只是要定义一些类型而已我们通过下面几个例子来复习一下类的封装、继承和多态。 基本写法 定义一个 Person 类有属性 name 和 方法 speak class Person {name: stringconstructor(name: string) {this.name name}speak() {console.log(\){this.name} is speaking)} } const p1 new Person(lin) // 新建实例
p1.name // 访问属性和方法 p1.speak()继承 使用 extends 关键字实现继承定义一个 Student 类继承自 Person 类。 class Student extends Person {study() {console.log(\({this.name} needs study)} } const s1 new Student(lin) s1.study()继承之后Student 类上的实例可以访问 Person 类上的属性和方法。 image.png super关键字 注意上例中 Student 类没有定义自己的属性可以不写 super 但是如果 Student 类有自己的属性就要用到 super 关键字来把父类的属性继承过来。 比如Student 类新增一个 grade(成绩) 属性就要这么写 class Student extends Person {grade: numberconstructor(name: string,grade:number) {super(name)this.grade grade} } const s1 new Student(lin, 100)不写 super 会报错。 image.png 多态 子类对父类的方法进行了重写子类和父类调同一个方法时会不一样。 class Student extends Person {speak() {return Student \){super.speak()}}} TS 中一般对抽象方法实现多态详细见后文抽象类。 public public公有的一个类里默认所有的方法和属性都是 public。 比如上文中定义的 Person 类其实是这样的 class Person {public name: stringpublic constructor(name: string) {this.name name}public speak() {console.log(\({this.name} is speaking)} }复制代码public 可写可不写不写默认也是 public。 private private私有的只属于这个类自己它的实例和继承它的子类都访问不到。 将 Person 类的 name 属性改为 private。 class Person {private name: stringpublic constructor(name: string) {this.name name}public speak() {console.log(\){this.name} is speaking)} } 实例访问 name 属性会报错 image.png 继承它的子类 访问 name 属性会报错 image.png protected protected 受保护的继承它的子类可以访问实例不能访问。 将 Person 类的 name 属性改为 protected。 class Person {protected name: stringpublic constructor(name: string) {this.name name}public speak() {console.log(\({this.name} is speaking)} }实例访问 name 属性会报错 image.png 子类可以访问。 class Studeng extends Person {study() {console.log(\){this.name} needs study)} } static static 是静态属性可以理解为是类上的一些常量实例不能访问。 比如一个 Circle 类圆周率是 3.14可以直接定义一个静态属性。 class Circle {static pi 3.14public radius: numberpublic constructor(radius: number) {this.radius radius}public calcLength() {return Circle.pi * this.radius * 2 // 计算周长直接访问 Circle.pi} } 实例访问会报错 image.png 抽象类 抽象类听名字似乎是非常难理解的概念但其实非常简单。 TS 通过 public、private、protected 三个修饰符来增强了 JS 中的类。 其实 TS 还对 JS 扩展了一个新概念——抽象类。 所谓抽象类是指只能被继承但不能被实例化的类就这么简单。 抽象类有两个特点 抽象类不允许被实例化抽象类中的抽象方法必须被子类实现 抽象类用一个 abstract 关键字来定义我们通过两个例子来感受一下抽象类的两个特点。 抽象类不允许被实例化 abstract class Animal {} const a new Animal()定义一个抽象类 Animal初始化一个 Animal 的实例直接报错 image.png 抽象类中的抽象方法必须被子类实现 abstract class Animal {constructor(name:string) {this.name name}public name: stringpublic abstract sayHi():void }class Dog extends Animal {constructor(name:string) {super(name)} }复制代码定义一个 Dog 类继承自 Animal 类但是却没有实现 Animal 类上的抽象方法 sayHi报错 image.png 正确的用法如下 abstract class Animal {constructor(name:string) {this.name name}public name: stringpublic abstract sayHi():void}class Dog extends Animal {constructor(name:string) {super(name)}public sayHi() {console.log(wang)} }复制代码为什么叫抽象类 很显然抽象类是一个广泛和抽象的概念不是一个实体就比如上文的例子动物这个概念是很广泛的猫、狗、狮子都是动物但动物却不好是一个实例实例只能是猫、狗或者狮子。 官方一点的说法是在面向对象的概念中所有的对象都是通过类来描绘的但是反过来并不是所有的类都是用来描绘对象的如果一个类中没有包含足够的信息来描绘一个具体的对象这样的类就是抽象类。 比如 Animal 类只是具有动物都有的一些属性和方法但不会具体到包含猫或者狗的属性和方法。 所以抽象类的用法是用来定义一个基类声明共有属性和方法拿去被继承。 抽象类的好处是可以抽离出事物的共性有利于代码的复用。 抽象方法和多态 多态是面向对象的三大基本特征之一。 多态指的是父类定义一个抽象方法在多个子类中有不同的实现运行的时候不同的子类就对应不同的操作比如 abstract class Animal {constructor(name:string) {this.name name}public name: stringpublic abstract sayHi():void}class Dog extends Animal {constructor(name:string) {super(name)}public sayHi() {console.log(wang)}}class Cat extends Animal {constructor(name:string) {super(name)}public sayHi() {console.log(miao)}}复制代码Dog 类和 Cat 类都继承自 Animal 类Dog 类和 Cat 类都不同的实现了 sayHi 这个方法。 interface 和 class 的关系 上文中我们说过interface 是 TS 设计出来用于定义对象类型的可以对对象的形状进行描述。 interface 同样可以用来约束 class要实现约束需要用到 implements 关键字。 implements implements 是实现的意思class 实现 interface。 比如手机有播放音乐的功能可以这么写 interface MusicInterface {playMusic(): void}class Cellphone implements MusicInterface {playMusic() {}}复制代码定义了约束后class 必须要满足接口上的所有条件。 如果 Cellphone 类上不写 playMusic 方法会报错。 image.png 处理公共的属性和方法 不同的类有一些共同的属性和方法使用继承很难完成。 比如汽车Car 类也有播放音乐的功能你可以这么做 用 Car 类继承 Cellphone 类找一个 Car 类和 Cellphone 类的父类父类有播放音乐的方法他们俩继承这个父类 很显然这两种方法都不合常理。 实际上使用 implements问题就会迎刃而解。 interface MusicInterface {playMusic(): void}class Car implements MusicInterface {playMusic() {}}class Cellphone implements MusicInterface {playMusic() {}}复制代码这样 Car 类和 Cellphone 类都约束了播放音乐的功能。 再比如手机还有打电话的功能就可以这么做Cellphone 类 implements 两个 interface。 interface MusicInterface {playMusic(): void}interface CallInterface {makePhoneCall(): void}class Cellphone implements MusicInterface, CallInterface {playMusic() {}makePhoneCall() {}}复制代码这个 CallInterface 也可以用于 iPad 类、手表类上面毕竟他们也能打电话。 interface 来约束 class只要 class 实现了 interface 规定的属性或方法就行了没有继承那么多条条框框非常灵活。 约束构造函数和静态属性 使用 implements 只能约束类实例上的属性和方法要约束构造函数和静态属性需要这么写。 以我们上文提过的 Circl 类为例 interface CircleStatic {new (radius: number): voidpi: number}const Circle:CircleStatic class Circle {static pi: 3.14public radius: numberpublic constructor(radius: number) {this.radius radius}}复制代码未定义静态属性 pi会报错 image.png constructor 入参类型不对会报错 image.png 枚举 在任何项目开发中我们都会遇到定义常量的情况常量就是指不会被改变的值。 TS 中我们使用 const 来声明常量但是有些取值是在一定范围内的一系列常量比如一周有七天比如方向分为上下左右四个方向。 这时就可以使用枚举Enum来定义。 基本使用 enum Direction {Up,Down,Left,Right}复制代码这样就定义了一个数字枚举他有两个特点 数字递增反向映射 枚举成员会被赋值为从 0 开始递增的数字 console.log(Direction.Up) // 0console.log(Direction.Down) // 1console.log(Direction.Left) // 2console.log(Direction.Right) // 3复制代码枚举会对枚举值到枚举名进行反向映射 console.log(Direction[0]) // Upconsole.log(Direction[1]) // Downconsole.log(Direction[2]) // Leftconsole.log(Direction[3]) // Right复制代码如果枚举第一个元素赋有初始值就会从初始值开始递增 enum Direction {Up 6,Down,Left,Right}console.log(Direction.Up) // 6console.log(Direction.Down) // 7console.log(Direction.Left) // 8console.log(Direction.Right) // 9复制代码反向映射的原理 枚举是如何做到反向映射的呢我们不妨来看一下被编译后的代码 var Direction;(function (Direction) {Direction[Direction[Up] 6] Up;Direction[Direction[Down] 7] Down;Direction[Direction[Left] 8] Left;Direction[Direction[Right] 9] Right;})(Direction || (Direction {}));复制代码主体代码是被包裹在一个自执行函数里封装了自己独特的作用域。 Direction[Up] 6复制代码会将 Direction 这个对象的 Up 属性赋值为 6JS 的赋值运算符返回的值是被赋予的值。 Direction[Up] 6 返回 6复制代码 执行 Direction[Direction[Up] 6] Up;相当于执行Direction[Up] 6Direction[6] Up复制代码这样就实现了枚举的反向映射。 手动赋值 定义一个枚举来管理外卖状态分别有已下单配送中已接收三个状态。 可以这么写 enum ItemStatus {Buy 1,Send,Receive}console.log(ItemStatus[Buy]) // 1console.log(ItemStatus[Send]) // 2console.log(ItemStatus[Receive]) // 3复制代码但有时候后端给你返回的数据状态是乱的就需要我们手动赋值。 比如后端说 Buy 是 100Send 是 20Receive 是 1就可以这么写 enum ItemStatus {Buy 100,Send 20,Receive 1}console.log(ItemStatus[Buy]) // 100console.log(ItemStatus[Send]) // 20console.log(ItemStatus[Receive]) // 1复制代码别问为什么实际开发中经常会有这种情况发生。 计算成员 枚举中的成员可以被计算比如经典的使用位运算合并权限可以这么写 enum FileAccess {Read 1 1,Write 1 2,ReadWrite Read | Write,}console.log(FileAccess.Read) // 2 - 010console.log(FileAccess.Write) // 4 - 100console.log(FileAccess.ReadWrite) // 6 - 110复制代码看个实例吧Vue3 源码中的 patchFlags用于标识节点更新的属性。 // packages/shared/src/patchFlags.tsexport const enum PatchFlags {TEXT 1, // 动态文本节点CLASS 1 1, // 动态 classSTYLE 1 2, // 动态 stylePROPS 1 3, // 动态属性FULL_PROPS 1 4, // 具有动态 key 属性当 key 改变时需要进行完整的 diff 比较HYDRATE_EVENTS 1 5, // 具有监听事件的节点STABLE_FRAGMENT 1 6, // 子节点顺序不会被改变的 fragmentKEYED_FRAGMENT 1 7, // 带有 key 属或部分子节点有 key 的 fragmentUNKEYED_FRAGMENT 1 8, // 子节点没有 key 的 fragmentNEED_PATCH 1 9, // 非 props 的比较比如 ref 或指令DYNAMIC_SLOTS 1 10, // 动态插槽DEV_ROOT_FRAGMENT 1 11, // 仅供开发时使用表示将注释放在模板根级别的片段HOISTED -1, // 静态节点BAIL -2 // diff 算法要退出优化模式}复制代码字符串枚举 字符串枚举的意义在于提供有具体语义的字符串可以更容易地理解代码和调试。 enum Direction {Up UP,Down DOWN,Left LEFT,Right RIGHT,}const value UPif (value Direction.Up) {// do something}复制代码常量枚举 上文的例子使用 const 来定义一个常量枚举 const enum Direction {Up UP,Down DOWN,Left LEFT,Right RIGHT,}const value UPif (value Direction.Up) {// do something}复制代码编译出来的 JS 代码会简洁很多提高了性能。 const value UP;if (value UP /* Up /) {// do something}复制代码不写 const 编译出来是这样的 var Direction;(function (Direction) {Direction[Up] UP;Direction[Down] DOWN;Direction[Left] LEFT;Direction[Right] RIGHT;})(Direction || (Direction {}));const value UP;if (value Direction.Up) {// do something}复制代码这一堆定义枚举的逻辑会在编译阶段会被删除常量枚举成员在使用的地方被内联进去。 很显然常量枚举不允许包含计算成员不然怎么叫常量呢 const enum Test {A lin.length}复制代码这么写直接报错 image.png 总结一下常量枚举可以避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问。 小结 枚举的意义在于可以定义一些带名字的常量集合清晰地表达意图和语义更容易地理解代码和调试。 常用于和后端联调时区分后端返回的一些代表状态语义的数字或字符串降低阅读代码时的心智负担。 类型推论 TypeScript里在有些没有明确指出类型的地方类型推论会帮助提供类型。 这种推断发生在初始化变量和成员设置默认参数值和决定函数返回值时。 定义时不赋值 let aa 18a lin复制代码定义时不赋值就会被 TS 自动推导成 any 类型之后随便怎么赋值都不会报错。 image.png 初始化变量 例如 let userName lin复制代码image.png 因为赋值的时候赋的是一个字符串类型所以 TS 自动推导出 userName 是 string 类型。 这个时候再更改 userName 时就必须是 string 类型是其他类型就报错比如 image.png 设置默认参数值 函数设置默认参数时也会有自动推导 比如定义一个打印年龄的函数默认值是 18 function printAge(num 18) {console.log(num)return num}复制代码那么 TS 会自动推导出 printAge 的入参类型传错了类型会报错。 image.png 决定函数返回值 决定函数返回值时 TS 也会自动推导出返回值类型。 比如一个函数不写返回值 function welcome() {console.log(hello)}复制代码TS 自动推导出返回值是 void 类型 再比如上文的 printAge 函数TS 会自动推导出返回值是 number 类型。 如果我们给 printAge 函数的返回值定义为 string 类型看看会发生什么。 function printAge(num 18) {console.log(num)return num}interface PrintAge {(num: number): string}const printAge1: PrintAge printAge复制代码很显然定义的类型和 TS 自动推导出的类型冲突报错 image.png 最佳通用类型 当需要从几个表达式中推断类型时候会使用这些表达式的类型来推断出一个最合适的通用类型。比如 let arr [0, 1, null, lin];复制代码image.png 又比如 let pets [new Dog(), new Cat()]复制代码image.png 虽然 TS 可以推导出最合适的类型但最好还是在写的时候就定义好类型上文的例子我们可以这么写 type arrItem number | string | nulllet arr: arrItem[] [0, 1, null, lin];let pets: Pets[] [new Dog(), new Cat()]复制代码小结 类型推论虽然能为我们提供帮助但既然写了 TS除非是函数默认返回类型为 void 这种大家都知道的其他的最好每个地方都定义好类型。 内置类型 JavaScript 中有很多内置对象[8]它们可以直接在 TypeScript 中当做定义好了的类型。 内置对象是指根据标准在全局作用域 global 上存在的对象这里的标准指的是 ECMAcript 和其他环境比如DOM的标准。 JS 八种内置类型 let name: string lin;let age: number 18;let isHandsome: boolean true;let u: undefined undefined;let n: null null;let obj: object {name: lin, age: 18};let big: bigint 100n;let sym: symbol Symbol(lin); 复制代码ECMAScript 的内置对象 比如Array、Date、Error 等 const nums: Arraynumber [1,2,3]const date: Date new Date()const err: Error new Error(Error!);const reg: RegExp /abc/;Math.pow(2, 9)复制代码以 Array 为例按住 comand/ctrl再鼠标左键点击一下就能跳转到类型声明的地方。 image.png 可以看到Array 这个类型是用 interface 定义的有多个不同版本的 .d.ts 文件声明了这个类型。 在 TS 中重复声明一个 interface会把所有的声明全部合并这里所有的 .d.ts 文件合并出来的 Array 接口就组合成了 Array 内置类型的全部属性和功能。 再举个例子 DOM 和 BOM 比如 HTMLElement、NodeList、MouseEvent 等 let body: HTMLElement document.bodylet allDiv: NodeList document.querySelectorAll(div);document.addEventListener(click, (e: MouseEvent) {e.preventDefault()// Do something});复制代码TS 核心库的定义文件 TypeScript 核心库的定义文件[9]中定义了所有浏览器环境需要用到的类型并且是预置在 TypeScript 中的。 比如 Math.pow 的类型定义如下 interface Math {/** Returns the value of a base expression taken to a specified power.* param x The base value of the expression.* param y The exponent value of the expression.*/pow(x: number, y: number): number;}复制代码又比如addEventListener 的类型定义如下 interface Document extends Node, GlobalEventHandlers, NodeSelector, DocumentEvent {addEventListener(type: string, listener: (ev: MouseEvent) any, useCapture?: boolean): void;}复制代码浅尝辄止知道在哪里定义就行真要去分析一些Web Api 的类型实现是很费精力的。 TS 进阶 这一部分的内容就需要费点脑细胞了毕竟学习一门语言还是没那么容易的最好把基础的内容都理解透彻之后再来学进阶。 高级类型 高级类型分一和二两部分一的部分不需要理解泛型也能理解二的部分需要理解泛型之后才能理解所以二被拆分到后面去了。 联合类型 如果希望一个变量可以支持多种类型就可以用联合类型union types来定义。 例如一个变量既支持 number 类型又支持 string 类型就可以这么写 let num: number | string num 8 num eight 联合类型大大提高了类型的可扩展性但当 TS 不确定一个联合类型的变量到底是哪个类型的时候只能访问他们共有的属性和方法。 比如这里就只能访问 number 类型和 string 类型共有的方法如下图 image.png 如果直接访问 length 属性string 类型上有number 类型上没有就报错了 image.png 交叉类型 如果要对对象形状进行扩展可以使用交叉类型 。 比如 Person 有 name 和 age 的属性而 Student 在 name 和 age 的基础上还有 grade 属性就可以这么写 interface Person {name: stringage: number }type Student Person { grade: number } 这和类的继承是一模一样的这样 Student 就继承了 Person 上的属性 image.png 联合类型 | 是指可以取几种类型中的任意一种而交叉类型 是指把几种类型合并起来。 交叉类型和 interface 的 extends 非常类似都是为了实现对象形状的组合和扩展。 类型别名type 类型别名type aliase听名字就很好理解就是给类型起个别名。 就像 NBA 球员 扬尼斯-阿德托昆博名字太长难记我们叫他字母哥。 就像我们项目中配置 alias不用写相对路径就能很方便地引入文件 import componentA from ../../../../components/componentA/index.vueimport componentA from /components/componentA/index.vue 类型别名用 type 关键字来书写有了类型别名我们书写 TS 的时候可以更加方便简洁。 比如下面这个例子getName 这个函数接收的参数可能是字符串可能是函数就可以这么写。 type Name string type NameResolver () string type NameOrResolver Name | NameResolver // 联合类型 function getName(n: NameOrResolver): Name {if (typeof n string) {return n}else {return n()} } 这样调用时传字符串和函数都可以。 getName(lin) getName(() lin)如果传的格式有问题就会提示。 image.png image.png 类型别名会给一个类型起个新名字。类型别名有时和接口很像但是可以作用于原始值联合类型元组以及其它任何你需要手写的类型。– TS 文档 类型别名的用法如下 type Name string // 基本类型type arrItem number | string // 联合类型 const arr: arrItem[] [1,2, 3] type Person { name: Name } type Student Person { grade: number } // 交叉类型type Teacher Person { major: string } type StudentAndTeacherList [Student, Teacher] // 元组类型 const list:StudentAndTeacherList [{ name: lin, grade: 100 }, { name: liu, major: Chinese } ] type 和 interface 的区别 比如下面这个例子可以用 type也可以用 interface。 interface Person {name: stringage: number}const person: Person {name: lin,age: 18}复制代码 type Person {name: stringage: number}const person: Person {name: lin,age: 18}复制代码那 type 和 interface 难道都可以随便用总得有个区别吧。 两者相同点 都可以定义一个对象或函数都允许继承 都可以定义一个对象或函数 定义对象上文已经说了我们来看一下如何定义函数。 type addType (num1:number,num2:number) numberinterface addType {(num1:number,num2:number):number}// 这两种写法都可以定义函数类型复制代码 const add:addType (num1, num2) {return num1 num2}复制代码都允许继承 我们定义一个 Person 类型和 Student 类型Student 继承自 Person可以有下面四种方式 // interface 继承 interfaceinterface Person { name: string }interface Student extends Person { grade: number }复制代码 const person:Student {name: lin,grade: 100}复制代码 // type 继承 typetype Person { name: string }type Student Person { grade: number } // 用交叉类型复制代码 // interface 继承 typetype Person { name: string }interface Student extends Person { grade: number }复制代码 // type 继承 interfaceinterface Person { name: string }type Student Person { grade: number } // 用交叉类型复制代码interface 使用 extends 实现继承 type 使用交叉类型实现继承 两者不同点 interface接口 是 TS 设计出来用于定义对象类型的可以对对象的形状进行描述。type 是类型别名用于给各种类型定义别名让 TS 写起来更简洁、清晰。type 可以声明基本类型、联合类型、交叉类型、元组interface 不行interface可以合并重复声明type 不行 合并重复声明 interface Person {name: string}interface Person { // 重复声明 interface就合并了age: number}const person: Person {name: lin,age: 18}复制代码重复声明 type 就报错了 type Person {name: string}type Person { // Duplicate identifier Personage: number}const person: Person {name: lin,age: 18}复制代码image.png 这两者的区别说了这么多其实本不该把这两个东西拿来做对比他们俩是完全不同的概念。 interface 是接口用于描述一个对象。 type 是类型别名用于给各种类型定义别名让 TS 写起来更简洁、清晰。 只是有时候两者都能实现同样的功能才会经常被混淆 平时开发中一般使用组合或者交叉类型的时候用 type。 一般要用类的 extends 或 implements 时用 interface。 其他情况比如定义一个对象或者函数就看你心情了。 类型保护 如果有一个 getLength 函数入参是联合类型 number | string返回入参的 length function getLength(arg: number | string): number {return arg.length}复制代码从上文可知这么写会报错因为 number 类型上没有 length 属性。 image.png 这个时候类型保护Type Guards出现了可以使用 typeof 关键字判断变量的类型。 我们把 getLength 方法改造一下就可以精准地获取到 string 类型的 length 属性了 function getLength(arg: number | string): number {if(typeof arg string) {return arg.length} else {return arg.toString().length}}复制代码之所以叫类型保护就是为了能够在不同的分支条件中缩小范围这样我们代码出错的几率就大大降低了。 类型断言 上文的例子也可以使用类型断言来解决。 类型断言语法 值 as 类型复制代码使用类型断言来告诉 TS我开发者比你编译器更清楚这个参数是什么类型你就别给我报错了 function getLength(arg: number | string): number {const str arg as stringif (str.length) {return str.length} else {const number arg as numberreturn number.toString().length}}复制代码注意类型断言不是类型转换把一个类型断言成联合类型中不存在的类型会报错。 比如 function getLength(arg: number | string): number {return (arg as number[]).length}复制代码image.png 字面量类型 有时候我们需要定义一些常量就需要用到字面量类型比如 type ButtonSize mini | small | normal | largetype Sex 男 | 女复制代码这样就只能从这些定义的常量中取值乱取值会报错 image.png 泛型 泛型是 TS 比较难理解的部分拿下了泛型对 TS 的理解就又上了一个台阶对后续深入学习帮助很大。 为什么需要泛型 如果你看过 TS 文档一定看过这样两段话 软件工程中我们不仅要创建一致的定义良好的 API同时也要考虑可重用性。组件不仅能够支持当前的数据类型同时也能支持未来的数据类型这在创建大型系统时为你提供了十分灵活的功能。 在像 C# 和 Java 这样的语言中可以使用泛型来创建可重用的组件一个组件可以支持多种类型的数据。这样用户就可以以自己的数据类型来使用组件。 简直说的就不是人话你确定初学者看得懂 我觉得初学者应该要先明白为什么需要泛型这个东西它解决了什么问题而不是看这种拗口的定义。 我们还是先来看这样一个例子体会一下泛型解决的问题吧。 定义一个 print 函数这个函数的功能是把传入的参数打印出来再返回这个参数传入参数的类型是 string函数返回类型为 string。 function print(arg:string):string {console.log(arg)return arg}复制代码现在需求变了我还需要打印 number 类型怎么办 可以使用联合类型来改造 function print(arg:string | number):string | number {console.log(arg)return arg}复制代码现在需求又变了我还需要打印 string 数组、number 数组甚至任何类型怎么办 有个笨方法支持多少类型就写多少联合类型。 或者把参数类型改成 any。 function print(arg:any):any {console.log(arg)return arg}复制代码且不说写 any 类型不好毕竟在 TS 中尽量不要写 any。 而且这也不是我们想要的结果只能说传入的值是 any 类型输出的值是 any 类型传入和返回并不是统一的。 这么写甚至还会出现bug const res:string print(123) 复制代码定义 string 类型来接收 print 函数的返回值返回的是个 number 类型TS 并不会报错提示我们。 这个时候泛型就出现了它可以轻松解决输入输出要一致的问题。 注意泛型不是为了解决这一个问题设计出来的泛型还解决了很多其他问题这里是通过这个例子来引出泛型。 泛型基本使用 处理函数参数 我们使用泛型来解决上文的问题。 泛型的语法是 里写类型参数一般可以用 T 来表示。 function printT(arg:T):T {console.log(arg)return arg}复制代码这样我们就做到了输入和输出的类型统一且可以输入输出任何类型。 如果类型不统一就会报错 image.png 泛型中的 T 就像一个占位符、或者说一个变量在使用的时候可以把定义的类型像参数一样传入它可以原封不动地输出。 泛型的写法对前端工程师来说是有些古怪比如 T 但记住就好只要一看到 就知道这是泛型。 我们在使用的时候可以有两种方式指定类型。 定义要使用的类型TS 类型推断自动推导出类型 printstring(hello) // 定义 T 为 stringprint(hello) // TS 类型推断自动推导类型为 string复制代码我们知道type 和 interface 都可以定义函数类型也用泛型来写一下type 这么写 type Print T(arg: T) Tconst printFn:Print function print(arg) {console.log(arg)return arg}复制代码interface 这么写 interface IprintT {(arg: T): T}function printT(arg:T) {console.log(arg)return arg}const myPrint: Iprintnumber print复制代码默认参数 如果要给泛型加默认参数可以这么写 interface IprintT number {(arg: T): T}function printT(arg:T) {console.log(arg)return arg}const myPrint: Iprint print复制代码这样默认就是 number 类型了怎么样是不是感觉 T 就如同函数参数一样呢 处理多个函数参数 现在有这么一个函数传入一个只有两项的元组交换元组的第 0 项和第 1 项返回这个元组。 function swap(tuple) {return [tuple[1], tuple[0]]}复制代码这么写我们就丧失了类型用泛型来改造一下。 我们用 T 代表第 0 项的类型用 U 代表第 1 项的类型。 function swapT, U(tuple: [T, U]): [U, T]{return [tuple[1], tuple[0]]}复制代码这样就可以实现了元组第 0 项和第 1 项类型的控制。 image.png 传入的参数里第 0 项为 string 类型第 1 项为 number 类型。 在交换函数的返回值里第 0 项为 number 类型第 1 项为 string 类型。 第 0 项上全是 number 的方法。 image.png 第 1 项上全是 string 的方法。 image.png 函数副作用操作 泛型不仅可以很方便地约束函数的参数类型还可以用在函数执行副作用操作的时候。 比如我们有一个通用的异步请求方法想根据不同的 url 请求返回不同类型的数据。 function request(url:string) {return fetch(url).then(res res.json())}复制代码调一个获取用户信息的接口 request(user/info).then(res {console.log(res)})复制代码这时候的返回结果 res 就是一个 any 类型非常讨厌。 image.png 我们希望调用 API 都清晰的知道返回类型是什么数据结构就可以这么做 interface UserInfo {name: stringage: number}function requestT(url:string): PromiseT {return fetch(url).then(res res.json())}requestUserInfo(user/info).then(res {console.log(res)})复制代码这样就能很舒服地拿到接口返回的数据类型开发效率大大提高 image.png 约束泛型 假设现在有这么一个函数打印传入参数的长度我们这么写 function printLengthT(arg: T): T {console.log(arg.length)return arg}复制代码因为不确定 T 是否有 length 属性会报错 image.png 那么现在我想约束这个泛型一定要有 length 属性怎么办 可以和 interface 结合来约束类型。 interface ILength {length: number}function printLengthT extends ILength(arg: T): T {console.log(arg.length)return arg}复制代码这其中的关键就是 T extends ILength让这个泛型继承接口 ILength这样就能约束泛型。 我们定义的变量一定要有 length 属性比如下面的 str、arr 和 obj才可以通过 TS 编译。 const str printLength(lin)const arr printLength([1,2,3])const obj printLength({ length: 10 })复制代码这个例子也再次印证了 interface 的 duck typing。 只要你有 length 属性都符合约束那就不管你是 strarr 还是obj都没问题。 当然我们定义一个不包含 length 属性的变量比如数字就会报错 image.png 泛型的一些应用 使用泛型可以在定义函数、接口或类的时候不预先指定具体类型而是在使用的时候再指定类型。 泛型约束类 定义一个栈有入栈和出栈两个方法如果想入栈和出栈的元素类型统一就可以这么写 class StackT {private data: T[] []push(item:T) {return this.data.push(item)}pop():T | undefined {return this.data.pop()}}复制代码在定义实例的时候写类型比如入栈和出栈都要是 number 类型就这么写 const s1 new Stacknumber()复制代码这样入栈一个字符串就会报错 image.png 这是非常灵活的如果需求变了入栈和出栈都要是 string 类型在定义实例的时候改一下就好了 const s1 new Stackstring()复制代码这样入栈一个数字就会报错 image.png 特别注意的是泛型无法约束类的静态成员。 给 pop 方法定义 static 关键字就报错了 image.png 泛型约束接口 使用泛型也可以对 interface 进行改造让 interface 更灵活。 interface IKeyValueT, U {key: Tvalue: U}const k1:IKeyValuenumber, string { key: 18, value: lin}const k2:IKeyValuestring, number { key: lin, value: 18}复制代码泛型定义数组 定义一个数组我们之前是这么写的 const arr: number[] [1,2,3]复制代码现在这么写也可以 const arr: Arraynumber [1,2,3]复制代码数组项写错类型报错 image.png 小结 泛型Generics从字面上理解泛型就是一般的广泛的。 泛型是指在定义函数、接口或类的时候不预先指定具体类型而是在使用的时候再指定类型。 泛型中的 T 就像一个占位符、或者说一个变量在使用的时候可以把定义的类型像参数一样传入它可以原封不动地输出。 泛型在成员之间提供有意义的约束这些成员可以是函数参数、函数返回值、类的实例成员、类的方法等。 用一张图来总结一下泛型的好处 image.png TS 实战 语法学得差不多了不实战一下怎么行。 Vue3 todoList 通过编写一个 Vue3 todoList熟悉日常工作中用得最多的增删改查的写法熟悉最基本的 Vue3 语法体会比较简单的 TS 结合 Vue3 的写法 涉及知识 Vue3 script setuprefcomputed条件渲染和列表渲染数据绑定和 v-model事件 TS 基础类型接口泛型TS 结合 Vue3
要实现的功能 新增待办事项删除待办事项全选和取消全选功能清理已做事项 todolist.gif 项目安装 使用 pnpm 新建一个 vite 项目 pnpm create vite my-v3-app – –template vue然后进入项目安装依赖启动 cd my-v3-app pnpm i pnpm devimage.png 真的太快了全程不到一分钟就可以开始开发 todoList 了。 代码实现 templateinput typetext v-modeltodoMsgbutton clickadd添加/buttonbutton clickclearHasDone清理/buttondiv v-iflists.lengthdiv v-for(item, index) in lists :keyitem.msginput typecheckbox v-modelitem.donespan :class{done: item.done}{{item.msg}}/spanspan clickdeleteItem(index)❎/span/divdivspan全选/spaninput typecheckbox v-modelisAllDonespan{{hasDown}} / {{lists.length}}/span/div/divdiv v-else暂无数据/div /template script langts setup import { ref, computed } from vue; interface TodoItem { // 定义 todo 事项类型msg: string // 要做的事done: boolean // 是否完成 }const todoMsg refstring() // 用一个ref来定义 todo 事项并双向绑定 const lists refTodoItem const hasDown computed(() lists.value.filter(item item.done).length) // 已经做了的事项列表 const isAllDone computedboolean({ // 所有的事项是否完成双向绑定到全选按钮get() { // isAllDone 的获取方法用于双向绑定数据return hasDown.value lists.value.length},set(value:boolean) { // isAllDone 的更改方法用于实现全选 和 取消全选功能lists.value.forEach(item {item.done value})} }) const add () { // 新增事项if (todoMsg.value) { // 有值才新增lists.value.push({msg: todoMsg.value,done: false})todoMsg.value // 新增完了输入框的值清空} } const deleteItem (index:number) { // 删除事项lists.value.splice(index, 1) } const clearHasDone () { // 清理所有已完成的事项lists.value lists.value.filter(item !item.done) } /script style.done {text-decoration: line-through; // 删除线样式color: gray; } /style 小结 别看 todoList 的实现很简单但日常开发中我们写的就是这些东西俗称搬砖用到的 TS 知识也不多所以说啊如果只是为了应付日常开发真不需要学很深。