一、为了获得更好的开发体验,解决js中一些难以解决的问题
TypeScript是什么?我们为什么要使用TypeScript?
光说不写太抽象,我还是举个例子吧,看以下这段代码
const origin = [{id:101, name:'ts'}]
const findUserNameById = (arr, id) => {
let result = arr.filters(item => {
return id === item.id
})
return result;
}
let obj = findUserNmaeById(origin, '101')
console.log(obj[0].name + 'is very good !!!')
以上这段代码会输出什么?有的小伙伴可能发现了问题,但是你又能发现几个呢?
- Uncaught ReferenceError: findUserNmaeById is not defined
- arr.filters is not a function
- Uncaught TypeError: Cannot read property 'name' of undefined
- 更深的雷:id === item.id,由于调用的时候传入的是 string 类型的,源数据是number类型的,这样你将永远得不到数据;('101' === 101 -> false)
写代码时候是不是都会遇到这样的问题? 我相信有很多小伙伴和我一样会遇到这些......嗯低级的错误。(拥有了ts,真香,我真的笑出了zhu声~~~)
二、javascript 存在的问题
- 使用了不存在的变量函数或者成员;(函数名字写错等等)
- 函数返回类型不准确,无法预知的类型;(把不确定的类型,当成确定的类型)
- 在使用null或者undefined成员;
- 代码的 可阅读性 和 可维护性,没法控制;
三、javascript 的原罪
- js语言本身的特性,决定了该语言无法适应大型复杂项目;
- 弱类型,某个变量随时更换类型,在哪里重新赋值改变了类型你无法预测;
- 解释性语言,解释一行,运行一行。错误发生的时间点是运行时;
- 前端开发中,大部分时间都在排错。(很多时候,都在花费时间排雷!!!)
什么是TypeScript ?
一、TypeScript (简称TS)
- TS是JS的超集,是一个可选的,静态的类型系统。
- 类型系统,对所有的标识符(变量、参数、函数、返回值)进行类型检查
- 类型检查是在编译时候,运行之前(运行的是编译后的JS代码)
- 需要tsc index.ts 转换才能执行,TS不参与任何运行时候的检查。
二、TypeScript 的常识
- 2012年微软发布
- anders负责开发TS项目,后来开源了
- 定位类型检查系统,缩短项目排错时间
- 有了类型检查之后,无形中增强了面向对象的开发
- JS可以面向对象开发,但是会遇到很多问题的。(暂不举具例)
- 使用TS后,可以编写出完善的面向对象代码
三、TS 需要注意的一些项目细节
- 假设当前的执行环境是浏览器的环境
- 如果代码中没有使用模块化语句(import、export),便认为该代码是全局执行。
- 一个文件中 let n:number = 1 ,tsc 编译之后形成的js文件中的变量是全局的,所有TS文件的全局变量报错。全局冲突嘛。
- 编译的目标代码,是ES3,可配置。
- 使用tsc命令行编译的时候,加上选项参数
- 使用TS配置文件,来更改编译选项(重点学, tsc --init命令会直接生成tsconfig.json文件)
- 使用了配置文件之后,tsc编译不能再跟上文件名了,如果跟上会忽略配置文件
- @types/node, @types是官方的第三方库,其中包含了很多对js代码类型的描述。(比如JQ,可以安装@types/jquery, 为jquery库添加类型描述)
TypeScript 正式入门
正式开始学习typescript之前呢,给小伙伴们安利一个网站,可以在线编译,把TS代码直接对比编译成JS代码,对我们入门学习很有帮助。
www.typescriptlang.org/play/index.…
- TS是可选的,TS类型是静态类型,暂时做不到运行时的类型检查,最终运行还是JS代码
一、基本类型
- number
- string
- boolean
- 数组
let numArr: number[] = [1, 2];
let numArr: Array<number> = [1, 2] //这种也叫泛型
- 对象
let u:object; // 约束力很弱,赋值函数也不会报错,用得不多。
- null / undefined
- null 和 undefined 是所有其它类型的子类型,它们可以赋值给其它类型
- "strictNullChecks": true, 可以更加严格的空类型检查,不让null 和 undefined 随便赋值,只能赋值给自身
- 随着版本更新,2020-2-20发布的3.8版本有更多新特性、基本类型
二、其它常用类型
- 联合类型(多种类型,任选其一),比如字面量联合类型,类型联合类型,后面还会学到 & 与操作
- let name:string | undefined = undefined // 如果没有具体赋值,还是可以类型推导。
- 类型保护,如果某个变量进行判断之后,就可以确定是什么类型了。(后面讲怎么触发类型保护,只能触发原始类型)
- void类型,通常用于约束函数的返回值
- never类型,通常用于约束函数的返回值,表示该函数永远不可能结束,不会返回值或者函数内死循环。
function throwError(msg :string):never{
throw new Error(msg); //到这里就停了
console.log('aaaa'); //这里以至于下面都不可能执行了
}
- 字面量类型
let gender:'男' | '女'; //从此只能赋值男女
let arr:[] //永远只能取值为一个空数组
let user: { name:string, age:number }
user = { name:'hrt', age:23 } //强力约束了
- 元祖类型(Tuple),一个固定长度的数组,并且数组中每一项的类型确定
let tu:[string, number]; //之后赋值必须赋值两项,类型确定
- any 类型
- any类型:any类型,可以绕过类型检查,因此,any类型的数据,可以赋值给任意类型
- let data:any;
- let num:number = data
- 会有隐患,不要随意用,如果没有必要,不要使用any类型
- 基本类型 和其它 常用类型 统称为TS类型
- 类型别名, 对已知的类型定义别名
- 这个react会经常用到
type User = {
name:string
age:number
}
//不仅变量可以这样方便的使用,函数返回值也可以方便点,简洁代码
let user: User - 后面还可以通过一些操作符,像&之类的,高级联合类型
type Gender = '男' | '女'
type User = {
name:string,
age:number
gender:Gender //这样可以很方便的维护代码,代码重用
}
三、函数相关的类型约束
- 实用的函数重载
- 函数重载:在函数实现之前,对调用的多种情况进行声明
- 举例:
function combine(a:number, b:number):number;
function combine(a:string, b:string):string;
function combine(a:number | string, b:number | string):number | string{
if(typeof a === 'number' && typeof b === 'number'){
return a * b
}else if(typeof a === 'string' && typeof b === 'string'){
return a + b
}
throw new Error('a和b 类型必须相同')
}
const result = combine("2", '2') //使用的时候,result 就可以具体的推导出类型
- 可选参数和默认值
- 可选参数,但是要注意放在必须参数后面
- 当然,也可以有默认值 a:number = 1
function combine(a:number = 1, b?:number):number {
return a + 0
}
四、枚举类型
- 定义语法:enum 枚举名{ 枚举字段 }
- 举例使用:
enum Gender {
male = '男', female = '女'
}
let gender:Gender;
gender = Gender.male
gender = Gender.female
有的小伙伴会想,既然都有类型别名了,我们还需要枚举类型干什么?我们先来一个小案例
需求:某个函数中要通过性别过滤对象,返回所有的特定性别,我们要定义一个特定性别类型
- 用字面量类型;字面量类型会有问题,出现重复的代码,如 let gender: '男' | '女';在调用函数的时候,参数也必须写字面量,会出现硬编码,不好维护;
- 用类型别名可以解决硬编码,type Gender = '男' | '女' ;但是,如果一旦别名类型一改,全部的代码都要改。我们先来看段代码
type Gender = '男' | '女'
let gender:Gender;
gender = '男' //这里只能选字面量的值
//我们假设后面还有 N 多个 变量/函数 使用类型别名 Gender
//如果Gender改了,改成type Gender = 'boy' | 'girl'; gender后面甚至更多的都要改
//根源是真实的值和逻辑含义产生了混淆,没有分开。真实值一改,全要改了。
//字面量类型不会进入到编译结果
- 用枚举类型,自定义类型,扩展类型
- 枚举类型可以很好的解决上面硬编码的问题
- 举例使用:
enum Gender {
male = '男',
female = '女'
}
let gender:Gender;
gender = Gender.male
gender = Gender.female
- 因此,使用的时候是使用逻辑名称,赋值的时候还是字面量
- 如果逻辑名称改了,f2重构就可以了,一改全改,编码硬编码
- 枚举参与编译,会出现在编译结果中,编译成一个对象。以前的类型别名,不会参数到编译中的
- 枚举细节规则
- 枚举的字段值,只能使用字符串或者数字
- 枚举的字段名称,要符合变量的命名规则,比如不能以数字开头
- 数字枚举的时候,值会自动自增,比如第一个写了1,后面不写,后面就自增1,如果全都不写,第一个就是0,后面自增
enum Level {
level1 = 1,
level2,
level3,
}
注意细节:
- 被数字枚举约束的变量,可以直接赋值为数子,如:let lev:Level = 1;原因是因为位枚举。
- 数据枚举的编译结果,和字符串编译结果有差异,将来枚举数字的时候千万要注意
最佳事件:
- 尽量不要在一个枚举中,即出现字符串字段值,又出现了数字字段值;
- 使用枚举时,尽量使用枚举的名字字段,不要使用真实值,避免硬编码;
- 能使用枚举,就不要使用类型别名,因为会出现上面我们分析的一改全改的情况;
实战扩展:
针对数字枚举,在一些权限控制下非常的优雅,可扩展性也很强,直接看代码
enum Permission {
Read = 1, //0001
Write = 2, //0010
Create = 4,//0100
Delete = 8 //1000
}
// 1.组合权限,使用或运算
// 0001 | 0010 -> 0011
let p:Permission = Permission.Read | Permission.Write
// 2.如何判断是否拥有某个权限, 用与运算
function hasPermission(target:Permission, per:Permission):boolean {
return (target & per) === Permission.Read
}
// 判断是不是有读权限
hasPermission(p, Permission.Read);
// 3.如何删除某个权限
// 亦或,相同取0,不同1
// 0011 ^ 0010 -> 0001 这样就可以删除写权限了;
p = p ^ Permission.Write
五、接口
什么是接口呢?
interface,接口。TS中的接口,用于约束类,对象,函数的契约(标准)。
举个例子,电源接口,如果两个充电器的电源接口满足相同的标准,就可以互相拿来充电。
只不过,我们以前前端开发的契约标准就是文档形式的。API文档,就是一个契约文档。但是文档作为契约,作为标准的话,是一个弱标准。没有强制按照要求来使用;写代码的时候调用接口时候会写错,完全没有提示。在代码层次的接口约束提醒,就是强约束;常见的代码层次强约束语言,如 java、c# 等;
这样解释还是过于干燥,我们来举一些实例吧;
- 接口约束对象
interface IUser {
name:string,
age:number,
sayHello?:() => void, //千万不要在这里写实现,这里是定义。
}
let u:IUser = { //强力约束u这个字面量对象必须按照接口标准实现
name:'hrt',
age:18,
}
//那和type User 有什么区别呢?目前区别不大,在约束类中区别就大了。
//在绝大部分场景下,约束对象尽量用接口约束,而尽量不要使用type。
//接口和类型别名一样,不出现在编译结果中。
- 接口约束函数
//接口约束
interface ICallBack {
(n:number): boolean
}
//类型别名可以实现同样的效果
type ICallBack = (n:number) => boolean
//定界符号,具体的约束内容
type ICallBack = {
(n:number): boolean
}
function sum(numbers:number[], callBack:ICallBack):number {
let s:number = 0
numbers.forEach((d) => {
if(callBack(d)){
s += d
}
})
return s
}
let addSum:number = sum([1,2,3,4,5,6], (n) => n % 2 !== 0)
console.log(addSum)
既然类型别名和接口都能实现同样的效果,那类型别名和接口有什么区别呢?
接口是可以继承的, 可以通过继承接口来组合约束。类型别名不行了。
interface A {
T1:string,
}
interface B {
T2:string,
}
interface C extends A, B {
T3:boolean
}
// 类型别名实现同样的效果:需要通过 & 符号实现,是交叉类型。
type A = {
T1:string,
}
type B = {
T2:string,
}
type C = {
T3:boolean
} & A & B //C是交叉类型,交叉A, B
但是两者是可以混用的,同时,在书写的时候,类型别名是不能重复定义的,而接口是可以自动聚合的;
interface A {
T1:string,
}
interface B {
T2:string,
}
interface C extends A, B {
T3:boolean
}
// 类型别名和接口混用
type D = {} type test1 = D & C
// 接口和类型别名混用
// 接口只能扩展对象类型 或 对象类型与静态已知成员的交集。
// 就是说 type D = '1' 是一个字面量这种非可继承的属性是会报错的
interface test2 extends D { }
总结一下接口和类型别名的区别
- 接口可以继承,类型别名不可以,类型别名可以通过 & 交叉类型实现同样的效果
- 类型别名和接口是可以混用的,但是要注意接口只能继承扩展对象类型
- 在OOP面向对象编程中,接口还可以被对象所实现 implements,类型别名不行
- 在接口中,子接口不能覆盖父接口的约束成员。
- 类型别名交叉的时候,相同的成员会交叉约束类型。两个成员的约束类型都会有,不覆盖。
六、TS的类型兼容
什么是类型兼容呢?
比如两个类型,如果能完成赋值,A = B,则 B和A兼容;
TS的类型兼容用的是子结构辩型法。(也叫鸭子辩型法)
我们来具体了解下TS对类型的判断
- 基本类型,要求完全匹配
- 对象类型,鸭子辩型法
interface Duck {
sound:"嘎嘎嘎"
swin:() => void
}
let person = {
name:'伪装成鸭子的人',
age:18,
// 类型断言,sound:"嘎嘎嘎" 这样的写的话TS会自动推断出sound的类型为string。
// 用了类型断言就是"嘎嘎嘎"类型的"嘎嘎嘎"值了;其实就是更换类型。
// 类型断言也可以在前面加<Type>来使用, <"嘎嘎嘎">"嘎嘎嘎"
sound:"嘎嘎嘎" as "嘎嘎嘎",
swin:() => {
console.log(this.name)
}
}
// 这就是鸭子辨型法,可以完成赋值
let duck:Duck = person
// 但是,不能直接把person的字面量对象直接写过来。
// 如果直接写过来,就更加严格了,只能是个鸭子
let duck2:Duck = {
sound:"嘎嘎嘎",
swin:() => {
console.log(this.name)
},
age:18, //报错,不能将 age 分配给 Duck
name:'伪装成鸭子的人', //报错,不能将 name 分配给 Duck
}
- 函数类型,一切无比的自然,但是返回值如果在约束的时候要求返回就一定要返回
//函数参数的鸭子辨型法
interface ICallBack {
(n:number, i:number): boolean
}
function sum(numbers:number[], callBack:ICallBack):number{
let s:number = 0
numbers.forEach((d, i) => {
// 注意这里接口约束要传两个,不能少
if(callBack(d, i)){
s += d
}
})
return s
}
//注意这里实际调用的时候根本没传完喔!
let addSum:number = sum([1,2,3,4,5,6], (n) => n % 2 !== 0)
// js的高阶函数forEach, map, filter都是这样的自然
七、readonly修饰符,修饰的目标是只读的
- 普通修饰,可以用在接口、类型别名、类等
type A = {
readonly T1:string,
}
interface C1 extends A1, B1 {
readonly T3:boolean
}
- 比较需要注意的修饰细节
let arr: readonly number[] = [1,2,3]
// 注意上面的修饰符号只是修饰类型。通过
arr = [4,5,6]
// 凡是涉及改变数组的相关函数/成员的提示就没了,只读; 比如 push 加入一项
arr.push() // 报错,不存在属性
arr[0] = 3 // 这样都不行了。只读
// 如果 const arr: readonly number[] = [1,2,3]
// 相当于:cosnt arr: ReadonlyArray<number> = [1,2,3]
// 注意,如果是修饰成员,写在成员的前面,相当于const。
八、类
这里我想分两部分讲,因为TS增加可很多类型,语法,对面向对象的编程支持更加的完善,所以将以基础部分和面向对象来讨论一下类。先以类的基础部分讨论
- tsconfig.json 属性的初始化检查,"strictPropertyInitialization": true 。更加严格的属性检查,检查初始化;检查构造函数中有没有赋值,或者属性列表有没有初始化默认值
- 属性可以修饰为可选的,用问号?
- 有些属性初始化之后就不能改变了, 用 readonly 修饰
- 有些属性是不希望外部能读取的,使用访问修饰符。可以控制类中的某个成员的访问权限;可以在构造函数的形参增加一个访问修饰符,可以这样简写。达到传参赋初值效果(不做任何操作的初始化赋值才可以)
// "strictPropertyInitialization": true 更加严格的属性检查,检查属性初始化
// public 默认是这个访问权限,公开的,所有都可以访问
// private 私有的,只有在类中使用
// protected 受保护的,暂时不考虑
class name {
// 属性列表
readonly id:number; //只读, 相当于const
gender:'男' | '女' = '男'
public pid?: string
private publishNumber:number = 3 //每天一共可以发多少篇文章
private curNumber:number = 0 //当前可以发布的文章数量
// 可以在形参中加修饰符达到初始化的效果。简化繁琐代码
constructor(public name:string, public age:number) {
this.id = Math.random()
}
public publish(title:string){
if(this.publishNumber > this.curNumber){
console.log('发布了一篇文章', title)
this.curNumber++
}else{
console.log('不能发布文章了')
}
}
}
const user = new name('hrt', 18)
user.publish('文章1')
user.publish('文章2')
user.publish('文章3')
- 访问器,用于控制属性的读取和赋值
class name {
// 可以在形参中加修饰符达到初始化的效果。简化繁琐代码
constructor(public name:string, private _age:number) {
this.id = Math.random()
}
// 利用private控制,getAge() 是java的做法
// C# 是这样做的 get age,用的时候就像普通对象的获取
// ES6 的时候,也是这样玩的,其实这样也相当于是readonly
// C# 规范,写私有属性的时候尽量前面加下划线
get age(){
return this._age
}
set age(age: number){
// 类的属性,要受一定的限制控制
if(age >= 18 ){
return;
}
this._age = age
}
}
const user = new name('hrt', 18)
user.publish('文章1')
user.publish('文章2')
user.publish('文章3')
user.age = 10
OOP 部分后面再讲,还需要讲泛型,泛型是比较绕,和难理解的;
九、泛型
泛型是一个比较重要,实际项目运用也比较频繁的一个类型,需要掌握其使用;
什么是泛型?为什么需要泛型?
为什么需要泛型:如果,实际项目编写中,我们需要一些丢失的信息,就需要泛型了;
第二点你可能很难以理解,实际上它相当于灵活需要一些类型信息,当时这些类型信息要在运行的时候才知道;举个例子。
// T表示泛型,依附于这个函数,泛型就相当于类型变量
function take<T>(arr:T[], n:number): T[]{
if(n >= arr.length){
return arr
}
const res: T[] = []
for (let i = 0; i < n; i++) {
const element = arr[i];
res.push(element)
}
return res
}
// 调用函数的时候才知道什么类型
// 这样就能把丢失的信息(类型)找回来了
take<number>([1, '2', 3], 2)
//指定类型的话,里面有字符串类型,就会报错;
- 在函数名之后写上两个尖括号加泛型名称
- 泛型就相当于类型变量,一般用T表示,定义的时候不知道什么类型,运行的时候才能确定是什么类型
- 执行语句如果不使用指定泛型的类型,take([1, '2', 3], 2),就不会报错了,返回结果推导为 string | number; 因为TS可以类型推导,很多时候,TS可以智能的推导出类型,前提是使用了泛型。
- 如果无法完成推导,并且有没有传递具体的类型,默认为空对象(相当于any)
- 定义时候泛型可以使用默认值,不指定就是默认值。如:take<T = number>
- 泛型是一个地方确定类型了,就可以推导确定出具体的类型了,很灵活强大;
泛型实际运用中的一些注意事项:
- 泛型约束:函数使用泛型,参数是泛型,返回值也是泛型<T>, 对T进行一些约束。<T extends XXX>, 这样子一约束之后,到时候传过来的类型,必须满足XXX,鸭子辨型法。千万要注意,这里的 extends 不是继承的意思,是约束。
- 泛型对于前端同学来说确实是一个比较抽象的学习成本,特别是没有接触过强类型语言的;这里我比较推荐大家多查多练多看;这样你才能掌握泛型;有基础了直接看TS内置类型,antd 4.0 UI 库,antd 这个库使用的泛型相当多,也比较经典使用。
十、模块化
- TS中如何书写模块化语句
- TS中,导入和导出,统一使用es6的模块化标准;
- 尽量不要使用默认 export default 导出,因为没有智能提示 ;应该用export。
- 导入的时候不要添加后缀名ts
- 编译结果中的模块化
- 如果编译标准是ES6,没有区别的;如果编译结果的模块化标准是 commonJS,导出的声明会变成exports的属性
总结前面的知识点
- 基本类型:boolean number string object Array void never null undefined
- 字面量类型:具体的对象、字符等,或者元组
- 扩展类型:类型别名,枚举,接口,类。(实际上还有很多高级的联合类型)
- 类型别名和接口不产生编译结果,枚举和类产生编译结果。(枚举产生的就是类,类没啥区别)
- TS类:访问修饰符,readonly, 一些访问修饰符(public等)
- 泛型:解决某个功能和类型的耦合。(就是抽出一个通用的方法,方便代码重用、灵活)
- 类型兼容性:鸭子辨型法,子结构辨型法。(A如果想赋值给B,A必须满足B的结构,A的属性可以多不可以少)
- 类型兼容性,在函数类型兼容的时候,参数是可以少的,但是不可以多。要求返回必须返回,不要求你随缘
- 类型断言:开发者非常清楚某个类型,但是TS分辨不出来,可以用类型断言 as
- TS有很多的内置关键类型,像ReturnType<T>之类的,用到时候可以查文档;这个一个初学者的难点,多练多查多看;
- TS 还有 keyof、is 、in、typeof 之类的关键字,用到时候也可以多查阅文档,后面再介绍
十一、深入理解类和接口
这里我想再以面向对象的方式去深入一点理解接口和类,什么是面向对象?
面向对象:以类为切入点进行编程,提取出对象的抽象模版
这样说可能还是有很多小伙伴会懵圈了,特别是没有接触过强类型语言 java/c# 的一些面向对象编程方式的小伙伴;面向对象(Oriented Object Programing)只是一个编程的思维方法方式。
举个实例:人
- 特征(属性):眼睛、鼻子、嘴巴、四肢、性别等
- 动作(方法):说话、运动、制造工具
以人这个对象为出发点,封装一个class对象模版,他拥有一些属性和方法;这就是面向对象开发方式;感兴趣的小伙伴也可以去了解下面向对象三大基础。封装、继承、多态;
react:类组件,可以产生对象的模版
如何学习OOP思想?我觉得最好的学习方式就是开发小游戏,很锻炼思维;
1. 里氏替换原则
类型匹配:TS 用的是鸭子辨型法。子类的对象,始终可以赋值给父类;在面向对象中,这种现象叫做里氏替换原则。如果要判断具体的子类型,用 instanceof 即可;
2. 继承的作用
继承可以描述类与类之间的关系,如A和B
- B是父类,A是子类
- B派生A,A继承自B
- instanceof 判断具体是哪个子类型
- B是A的基类,A是B的派生类
- A继承自B,会拥有B的所有方法和属性(类访问控制符会影响)
- 子类的对象,始终可以赋值给父类。(鸭子辨型法)
- 实例:
- let a:B = new A()
- a变量中的 this 指向还是正常的,还是执行A的实例
- 此时a只能使用子类和父类共有的方法,因为你声明的是B父类;
- 就是A子类有多余的都不能使用,一般不会这么奇葩写
3. 成员的重写(override)
- 子类中覆盖父类的成员,这就叫重写
- 重写不能改变父类成员的类型(属性)
- 重写方法的时候,子类参数要和父类一样,返回值可以不一样,但差别太大就没必要使用同一个方法
- 重写方法的时候,要注意this指向
- super:在子类的方法中,可以使用super读取父类的成员。super 和 this 是有区别的。
4. 再谈类访问修饰符
- 编译结果没有访问修饰符
- readonly:只读修饰符
- protected:受保护成员,只能在自身或者子类使用
- private:私有的,只能自己使用
- public:公共的,默认的
- 单根性:每个类最多只能拥有一个父类,即extends 只能继承一个父类
- 传递性:如果A(爷爷)是B(爸爸)的父类(有血缘关系),并且B(爸爸)也是C(儿子)的父类(有血缘关系),则A(爷爷)也是C(孙子)的父类(有血缘关系)。
6. 抽象类
语法:abstract class Chess { }
- 有时某个类只是表示抽象的概念,用于提取子类的公有成员,而不能直接创建它的实例对象。
- 该类可以作为抽象类,主要是为了解决代码重复的问题。不用兵、马、炮都写一遍类模版
- 抽象类只能用于继承,不能 new 创建实例。抽象类是一个强约束,用于对类的强约束。
- 如果抽象类中某个成员必须要子类实现,也可以使用abstract来进行强约束,要求子类一定要实现。
- 抽象成员必须出现在抽象类中。
- 抽象类可以再次继承抽象类,抽象类中可以先实现抽象成员,也可以到具体的子类再实现。
- 这样的强约束,对于维护和团队合作是非常好的。
7. 静态成员 static
- 属于某个构造函数的成员, 不附属在实例上,就要加上static关键字
- 静态方法中的this, 执向的是当前类
8. 再谈接口
- 接口用于约束类,对象、函数,是一个类型契约。
- 在类的实例中,方法没有强约束力。容易将类型和方法耦合在一起。例如:A, B 继承自C。A,B有各自的方法,在某处要一起调用,就需要instance判断类型了。
- 系统缺失对能力(方法)的定义 - 解决办法实现接口(implements 接口)
- 面向对象领域中的接口的语义:表达了某个类是否拥有某种能力(方法)
- 某个类具有某种能力,其实,就是实现了某种接口。implements interface(可以实现多个接口)
- 如果implements实现了某种能力,必须要实现,不实现会报错。
interface IDown{
down():void
}
class Children extends Parent implements IDown{
down(): void {
throw new Error("Method not implemented.");
}
}
9. 类型保护函数,来看个有趣的例子,判断某个变量是不是接口
// 相当于 a instanceof IDown, java中可以这么用TS不行。
function isHasIDown(chi: object): chi is IDown {
if((chi as unknown as IDown).方法){
return true
}else{
return false
}
}
10. 接口和类型别名的区别
- 接口可以被类实现,类型别名不行
- 接口可以继承类,表示该类的所有成员都在接口中
class A { name: string = 'A'; } class B { age: number = 1; } interface C extends A, B{ } let c:C = { name:'C', age: 10 }
十二、索引器
class A {
[prop:string]:string
name:string = '1'
}
let c:A = new A()
c.name = "ss"
c.x = '2222' //动态增加成员
十三、类this指向问题
- TS类中其实是使用了严格模式的,下面这样用会导致 this 为 undefined
class A {
name:string = 'hrt'
say(){
console.log(this.name)
}
}
let c:A = new A()
let s = c.say s()
- 而如果使用字面量的形式,this是any类型的
let c = {
name: 'hrt',
say(){
console.log(this.name)
}
}
let s = c.say
s() //this是any类型的
- TS允许在书写函数的时候,手动声明该函数的this指向,将this作为第一个参数,只用于约束this,不是真正的参数,也不会出现在编译结果中。接口强约束了this指向,防止this乱指向,压根就犯不了错误。
interface IUser {
name:string,
age:number,
sayHello(this:IUser):void //强行约束this
}
let c:IUser = {
name: 'hrt',
age:18,
sayHello(){
console.log(this.name)
}
}
let s = c.sayHello
s() //报错, this不匹配
十四、三个关键字
- typeof,js中是得到一个数据的类型,ts中的 typeof 表示获取类型
// js中的 typeof const a = '111' typeof a -> string
// ts 中的typeof,表示获取到的数据类型
// b 的类型和 a 的类型保持一致,就用typeof
let b:typeof a = "aaa"
- 当 typeof 作用于类的时候,得到的是该类的构造函数
class User {}
//1. 返回值要求的是 new User(),不是User
function careteUser(cls:User): User {
//2. 如果传new User(),这里必须是一个构造函数
return new cls()
}
//传类,参数不匹配,一定要满足这个奇葩需求怎么办呢?
const u = careteUser(User)
//那怎么办才能满足这奇葩需求呢?
//1. 构造函数约束
function careteUser(cls:new() => User)
//2. 或者这么干, typeof 作用于类的时候,得到的是该类的构造函数(是个类型)
function careteUser(cls:typeof User)
- keyof 作用于类、接口、类型别名,用于获取其它类型中的所有成员名,组成的联合类型;很强大实用的一个关键字
interface IUser {
pw:string,
age:number,
id:string
}
// 怎么约束prop呢? 它应该是某个具体类型的一个属性
// keyof User 相当于 type prop = pw | age | id
function printUserProperty(obj:User, prop:keyof User){
//obj[prop]
}
printUserProperty(user, "age")
- in 该关键字往往和 keyof 联用,限制某个索引类型的取值范围
type Obj = {
//属性和值是字符串是string就满足条件
//怎么进一步约束取值范围呢?太广阔了
// [p:string]: string,
// in 操作符,in 一个联合类型就行了
[p in "pw" | "age" | "id"]: string
}
const u:Obj = {
pw:' ',
age:'',
id:'',
}
//Obj 不存在 abc 属性
u.abc = "sss"
// 最佳写法 in keyof xxx
// 得到一个新类型Obj,将IUser所有属性值类型变成了字符串
type Obj = {
[p in keyof IUser]: string,
}
- in 关键字还有很多玩法
// 完全重新塑造一个类型
type newObj = {
[p in keyof IUser]: IUser[p],
}
// 还可以这么玩,全设成只读的
type newObj = {
readonly [p in keyof IUser]: IUser[p],
}
// 还可以这么玩,全设成可选的
type newObj = {
[p in keyof IUser]?: IUser[p],
}
十五、类型演算
TS中预设了很多实用的类型演算,其实这些内置类型都是根据上面介绍的三个关键字来得出的
- Partial<T>,将类型变成可选
type Partial<T> = {
[p in keyof T]?:T[p]
}
- ReadyOnly<T>, 只读
type ReadyOnly<T> = {
readonly [p in keyof T]:T[p]
}
- Required<T> 全部必选
type Required<T> = {
// -? 就是把问号去掉
[p in keyof T]-?: string,
}
- Pick<T, K> 选择,在T中选择K,K是T的子集
// K extends keyof T 这里是限制类型的意思,不是继承。
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
}
还有很多内置的类型就不一一列举了,感兴趣的小伙伴可自行去读TS预设类型的源码,同时可以很快的提高你对泛型和高级类型的认识;
总结
其实ts说难也不是很难,因为ts是一个静态可选的,学习成本曲线也不是很陡峭,除了类型系统,完全可以和 JS 一模一样;并且 TS 还增强了一些 OOP 特性;给JS套上一个拥有更多可能的壳;
ts的东西远远不止这些,但是我们去如何学习掌握呢?我认为最好的方式就是动手去练;第一阶段先了解个大概语法,类型,一些关键字;跟着在线编译网站练;第二阶段就是直接去做写demo,小项目;这个阶段对你的提升绝对是很大的,中途你可能磕磕绊绊会遇到很多不懂的懵比的,但是只需要静下心来慢慢理解,查阅资料,很快你就可以熟练使用ts了;