标签:系统 成员修饰符 src 好的 mic 继承 之一 完全 each
JavaScript是一门动态弱类型语言 对变量的类型非常宽容 而且不会在这些变量和它们的调用者之间建立结构化的契约。
试想有这么几个场景:
1: 你调用一个别人写的函数,但是这个人没有写注释,为了搞清楚参数类型,只能去看里面的逻辑
2: 为了保证代码的健壮性,你需要对一个函数的输入参数进行各种假设判断
3: 让你维护一个重要的底层类库,你不小心更换了一个参数类型,但是不知道有多少处的引用
4: 明明定义好的接口,可一连调就报错了,TypeError:Cannot read property ‘length’of undefined,于是你就找后端理论,这个接口定义不是数组么?
以上场景归根结底就是因为 javascript 是动态 弱类型语言,长时间在没有类型约束的环境下开发,就会造成“类型思维”的缺失,养成不良的编程习惯,这也是做前端开发的短板之一
在强类型语言中,当一个对象从调用函数传递到被调用函数时,其类型必须与被调用函数中声明的类型兼容 ——Liskov, Zilles 19
上图解释:有两个方法,方法A和方法B。方法A调用方法B的时候,x类型必须要和y类型兼容。兼容就意味着,y可以赋值给x,而且程序还可以正常运行 这是比较宽泛的定义,没有产生具体的规则,现在我们对强类型语言的定义会更精确一些 。
强类型语言:不允许改变变量的数据类型, 除非进行强制类型转换
以Java语言为例:
(左图)报错:提示类型不兼容,不能将布尔类型转换为整型
(右图)打印 x = 97。java将字符a进行了强制类型转换,将a的ACSLL码 强制给了x,这样x类型依然是整型
弱类型语言: 在弱类型语言中, 变量可以被赋予不同的数据类型
以js语言为例。图解:把z赋值给x 7、打印 x 为 字符串‘a‘ 。从这里就可以看出JS是一门弱类型语言
静态类型语言:在编译阶段确定所有变量的类型
动态类型语言:在执行阶段确定所有变量的类型
左图解 JS代码 :当编译器在看add方法这行代码的时候无法知道变量a、b的数据类型,只有在程序执行的时候,才能根据实际传递进来的参数再确定
右图解 C++代码: C++ 和JS不同的是,它在编译的时候就能够确定变量的数据类型,而且a、b类型一定是整型
1、 从对比来看,静态类型语言的优势要大于动态类型语言。如果是一门动态弱类型语言,就更会被打入鄙视链的底端。
2、 动态类型语言的支持者也有自己的理由
其实,这种争论一直存在,这说明任何语言的特点都具有两面性,也是一个发展和进化的过程,不能一概而论,要看具体的场景和性价比。
比如js就是一门动态弱类型语言,但是它目前还是被广泛应用。这也引申出一道面试题:大家也可以一起思考一下: TS会替代JS么?
我认为TypeScript更多地是对JavaScript的补充,而不是替代品,而且两者的普及都在增长。
横轴代表 从动到静。纵轴代表 从弱到强
这个图可以帮助我们理解 动态类型、静态类型、强类型和弱类型语言有哪些。
值得庆幸的是,开源社区也一直努力的解决这个问题,早在2014年 Facebook 推出了 Flow,微软也在同一年发布了 TypeScript 1.0 版本,它们都是致力于为JS提供静态类型检查,如今过去6年了。TS发展好像是更好一些,多个团队,比如 Angular和Vue,开始全面使用TS重构代码,甚至连Facebook自家的产品 比如 Jest和Yarn,都在从Flow向TS转移。 可以说在ECMScript标准推出静态类型检查之前,TS是当下解决此问题的最佳方案。
Typescript是拥有类型系统的JavaScript的超集。
可以编译成纯JavaScript。需要注意三点:
1. 类型检查。typescript会在编译代码时进行严格的静态类型检查,这就意味着,你可以在编写代码的阶段,发现可能存在的隐患,而不是上完线才发现
2. 语言拓展。typescript会包括来自ES6和未来提案中的特性,比如异步操作和装饰器,还有就是,也会从java语言借鉴某些特性,比如接口和抽象类。
3. 工具属性。typescript可以编译成标准的javascript,可以在任何浏览器操作系统上运行,无需任何运行时的额外开销,从这个角度上讲,typescript更像是一个工具,而不是一门独立的语言
ES6的数据类型:• Boolean • Number • String • Symbol • undefined • null • Array • Function • Object
Typescript的数据类型在ES6的基础上新增了几种: • void • any • never • 元组 • 枚举 • 高级类型
// 原始数据类型 let bool: boolean = true let num: number = 123 let str: string = ‘abc‘ // 数组 let arr: number[] = [1, 2, 3] let arr: Array<number|string> = [1, 2, 3, ‘4‘] // 元组(特殊的数组,限定了数组元素的类型和个数) let tuple2: [number, string] = [0, ‘1‘] // 元组越界问题: tuple2.push(2) console.log(tuple2) tuple[2] // 报错,不能进行越界访问 // 函数 let add = (x, y) => x + y; // 报错。需要为参数加上类型注解 let add = (x: number, y: number): number => x + y let add = (x: number, y: number) => x + y add11 = add22 // 类型推断可以省略返回值number let compute: (x: number, y: number) => number compute = (a, b) => a + b // 对象 let obj: object = { x: 1, y: 2 } obj.x = 3 // 报错。 let obj: { x: number, y: number } = { x: 1, y: 2 } obj.x = 3 // symbol let s1: symbol = Symbol() let s2 = Symbol() console.log(s1 === s2) // false // undefined, null let un: undefined = undefined // 被声明undefined,就不能赋值任何其他数据类型,只能赋值它本身 let un: undefined = ‘1‘ // 报错 let nu: null = null num = undefined // 报错。ts官方文档中,undefined和null是任何类型的子类型,说明可以赋值给其他类型,可以设置strictNullChecks:false num = null // void 可以确保任何表达式返回值一定是undefined console.log(void 0) // undefined。 // 原因:undefined在js中不是保留字,也可以自定义undefined变量覆盖全局undefined // 如下:控制台打印输出 (function (){ var undefined = 0; console.log(undefined) })() let noReturn = () => {} // any let x x = 1 x = [[]] x = () => {} // never let error = () => { throw new Error(‘error‘) } let endless = () => { while(true) {} }
作用:相当于强类型语言中的类型声明
语法:(变量/函数):类型名称
场景:如下图伪代码为例:权限操作判断函数,不同权限对应不同UI界面,用户登陆系统,一般会做初始化的工。
这种代码存在两个问题: 1) 可读性差:很难记住数字的含义。 2) 可维护差:硬编码,牵一发动全身
解决方案:TS的枚举类型。枚举可以理解成手机里的通讯录,拨打电话的时候只需要记住人名,不需要记住电话号码,而且,号码是可变性的,人名是不可变的
// 数字枚举,有反向映射
// 字符串枚举,没有反向映射 enum Message { Success = ‘恭喜你,成功了‘, Fail = ‘抱歉,失败了‘ } // 异构枚举(字符串/数字枚举混用)不建议使用 enum Answer { N, Y = ‘Yes‘ } // 枚举成员的分类 enum Char { // const member 常量枚举,会在编译的时候计算结果,以常量的形式出现在运行时环境 a, // 1.无初始值 b = Char.a, // 2.对已有成员的引用 c = 1 + 3, // 3.常量表达式 // computed member 计算枚举,非常量表达式,不会在编译阶段计算,而是会保留到程序的执行阶段 d = Math.random(), e = ‘123‘.length, } // 常量枚举。会在编译阶段被移除 // 使用场景:不需要对象,只是需要对象值时 const enum Month { Jan, Feb, Mar, Apr = Month.Mar + 1 } let month = [Month.Jan, Month.Feb, Month.Mar] // console.log(month, ‘month==‘) // 枚举类型 enum E { a, b } enum F { a = 0, b = 1 } enum G { a = ‘apple‘, b = ‘banana‘ } let e: E = 3 let f: F = 3 console.log(e === f) // 报错 不同类型的枚举不可比较 let e1: E.a = 3 let e2: E.b = 3 let e3: E.a = 3 console.log(e1 === e2) // 报错console.log(e1 === e3) // 正确 // 字符串枚举取值只能是枚举成员的类型 let g1: G = G.a let g2: G.a = G.a
// 定义一个List接口
interface List { readonly id: number; name: string; [x: string]: any; // 字符串索引签名,不确定接口返回个数 age?: number; // 可索引接口 } interface Result { data: List[] } function render(result: Result) { result.data.forEach((value) => { console.log(value) if (value.age) { console.log(value.age, ‘---‘) } }) } let result = { data: [ {id: 1, name: ‘A‘, sex: ‘male‘}, {id: 2, name: ‘B‘} ] } render(result)
interface StringArray {
[index: number]: string
}
let chars: StringArray = [‘a‘, ‘b‘]
interface Names { [x: string]: string; // [x: string]: any; // y: number; [z: number]: string; // 数字索引签名的返回值必须要是字符串索引签名返回值的子类型,因为javascript会进行类型转换,将number转换成string,这样就能保证类型的兼容性 // [z: number]: number; }
使用接口的好处:可以思考变量的类型,也可以思考接口的边界问题,这个过程非常有利于培养‘类型思维’
// function
function add1(x: number, y: number) { return x + y }
// 变量 let add2: (x: number, y: number) => number
// 类型别名 type add3 = (x: number, y: number) => number
// 接口 interface add4 { (x: number, y: number): number } add1(1, 2, 3) // 报错。 ts中形参和实参必须一一对应
function add5(x: number, y?: number) { return y ? x + y : x } add5(1)
function add6(x: number, y = 0, z: number, q = 1) { return x + y + z + q } add6(1, undefined, 3) console.log(add6(1, undefined, 3))
function add7(x: number, ...rest: number[]) { return x + rest.reduce((pre, cur) => pre + cur); } add7(1, 2, 3, 4, 5) console.log(add7(1, 2, 3, 4, 5))
function add8(...rest: number[]): number; function add8(...rest: string[]): string; function add8(...rest: any[]) { let first = rest[0]; if (typeof first === ‘number‘) { return rest.reduce((pre, cur) => pre + cur); } if (typeof first === ‘string‘) { return rest.join(‘‘); } } console.log(add8(1, 2)) console.log(add8(‘a‘, ‘b‘, ‘c‘))
我们知道es6引入了class关键字,我们也终于可以像传统的面向对象语言那样去创建一个类。 总体上来说,ts的类覆盖了es6的类,同时也引入了其他的特性, 那么两者之间有什么不同呢?
注意1: 类成员属性都是实例属性,不是原型属性 注意2: 类成员方法都是实例方法,不是原型方法
class Dog { constructor(name: string) { // 自动配对为类的本身 Dog this.name = name } public name: string = ‘dog‘ run() {} } // console.log(Dog.prototype) // 报错。类成员属性都是实例属性,不是原型属性let dog = new Dog(‘Dog‘)console.log(dog)
类的继承,extends关键字
class Dog { constructor(name: string) { this.name = name } public name: string = ‘dog‘ run() {} } let dog = new Dog(‘Dog‘) console.log(dog) class Husky extends Dog { constructor(name: string, color: string) { super(name) this.color = color } color: string }
成员修饰符:public、private、protected、readonly、static
// 父类 class Dog1 { constructor(name: string) { this.name = name this.pri() } name: string = ‘dog1‘ // 类的所有属性默认是public run() {} private pri() {} // 私有成员,只能被类的本身调用不能被类的实例和子类调用 protected pro() {} // 受保护成员,只能在类或者子类中访问,不能在类的实例中访问 readonly legs: number = 4 // 只读属性 static food: string = ‘bones‘ // 静态成员。 sleep() { // console.log(‘Dog sleep‘) } } let dog1 = new Dog1(‘Dog1‘) console.log(dog1.pri) // 报错 console.log(dog1.pro) // 报错 console.log(Dog1.food) console.log(dog1.food) // 报错。静态成员只能通过类名调用,不能通过子类来调用 // 子类调用 class Husky extends Dog1 { // 构造函数参数也可以添加修饰符,变成实例的属性 constructor(name: string, public color: string) { super(name) this.color = color // this.pri() // 报错 this.pro() } // color: string } console.log(Husky.food) // 类的静态成员也可以被继承
抽象类:abstract 关键字。ES中没有引入抽象类的概念,这是ts对es的又一次扩展。所谓抽象类就是只能被继承,而不能被实例化的类。
优点:1. 方法的复用
2. 可以实现多态
abstract class Animal { eat() { // console.log(‘eat‘) } abstract sleep(): void // 抽象方法。好处:明确知道子类可以有其他的实现,不必在父类中实现 } let animal = new Animal(); // 报错。只能被继承,而不能被实例化 class Dog2 extends Animal { constructor(name: string) { super() this.name = name } name: string run() {} sleep() { console.log(‘Dog2 sleep‘) } } let dog2 = new Dog2(‘Dog2‘) console.log(dog2.eat()) // ‘eat‘。子类可以调用抽象类里的方法 console.log(dog2.sleep()) // ‘Dog2 sleep‘
多态:在父类中定义一个抽象方法,在多个子类中对这个方法有不同的实现,在程序运行的时候,会根据多个对象,执行不同操作。
// 父类
abstract class Animal {
eat() {
console.log(‘eat‘)
}
abstract sleep(): void
}
// 子类中对这个抽象方法有不同的实现 class Cat extends Animal { sleep() { console.log(‘Cat sleep‘) } } let cat = new Cat() console.log(cat.sleep()) // ‘Cat sleep‘
定义:类的成员方法会返回一个this,这样就可以方便实现链式调用。
class Workflow { step1() { console.log(this, ‘step1‘) return this } step2() { console.log(this, ‘step2‘) return this } } new Workflow().step1().step2() // 继承的时候,this也可以表现为多态。this既可以是父类型,也可以是子类型 // 作用:保证了父类调用和子类调用的连贯性 class MyFlow extends Workflow { next() { console.log(this, ‘next‘) return this } } new MyFlow().next().step1().next().step2() // 父类和子类之间调用的连贯性
接口可以像类一样,相互继承,一个接口可以继承多个接口。 但是,接口继承类时需注意,接口在抽离类的成员的时候,不仅抽离了公共成员,而且抽离了私有成员和受保护成员。
图解:
1、首先接口之间是可以相互继承的,这样可以实现接口的复用
2、类之间也可以相互继承,可以实现方法和属性的复用
3、接口可以通过类来实现,但是接口只能约束类的共有成员,不能约束类的私有成员
4、接口也可以抽离出类的成员。抽离的时候会包括共有成员,私有成员,受保护成员
一个类通过关键字implements声明自己使用一个或者多个接口。
interface Human { // new (name: string): void; // 2.报错。类型接口也不能约束类的构造函数 name: string; eat(): void; } // implements 可以实现多个接口,用逗号分开就行,接口的方法一般为空的, 必须重写才能使用 class Asian implements Human { constructor(name: string) { this.name = name; } name: string // private name: string // 3.报错 类接口只能声明共有成员 eat() {} // 可以定义自己的属性 // age: number = 0 // sleep() {} }
接口继承接口。接口的继承可以抽离出可重用的接口。
interface Man extends Human { run(): void } interface Child { cry(): void } interface Boy extends Man, Child {} let boy: Boy = { name: ‘‘, eat() {}, run() {}, cry() {} }
接口继承类。接口把类的成员都抽象了出来,有类的成员结构,没有具体的实现
class Auto { state = 1 // private state1 = 0 // 报错。注意:接口在抽离类的成员的时候,不仅抽离了公共成员,而且抽离了私有成员和受保护成员 } interface AutoInterface extends Auto { } class C implements AutoInterface { state = 1 }
很多时候,我们希望一个函数或者一个类能支持多种数据类型,有很大的灵活性,如下图:
定义一个log函数,接收一个string,最后打印出这个字符串。但是,我希望这个字符串能接收一个字符串数组,根据前面介绍的,应该怎么实现呢?
1、可以通过函数重载实现
先定义一个接收字符串的函数,再定义一个接收字符串数组的函数,最后在一个比较宽泛的版本里打印实现
2、也可以通过联合类型实现
看起来比函数重载简便一些。
3、但是,现在我希望这个函数能接收任何类型的参数,实际上从前面的函数重载也可以看出,可以使用any类型。
看样子这个函数好像是满足了我们的需求。但是,产生了另外的一个问题,any类型会丢失一些信息,就是类型之间的约束关系,忽略了输入参数的类型和函数返回值的类型必须是一致的这个问题。
当一个调用者看见这个log函数的时候,完全无法获知这种约束关系,这个时候就需要用到“泛型”了!
那么什么是泛型呢???
泛型定义:不预先确定的数据类型, 具体的类型在使用的时候才能确定。
泛型从字面上理解就是“一般的,广泛的,不需要预先定义的的数据类型” 下面我们用泛型改造一下上面的log函数。
function log<T>(value: T): T { // console.log(value); return value; } log<string[]>([‘a‘, ‘,b‘, ‘c‘]) log([‘a‘, ‘,b‘, ‘c‘]) //ts的类型推断,可以省略类型参数,直接传入数组
除了上述的参数可以用泛型表示,函数、接口、类可不可以更灵活的去用泛型呢?答案是,必须能!
type Log = <T>(value: T) => T
let myLog: Log = log
看样子是不是很简单。
interface Log<T> { // 约束接口的所有成员 (value: T): T } // let myLog: Log = log; // 报错。注意:当泛型约束了所有的接口之后,使用的时候必须指定一个类型 let myLog: Log<number> = log myLog(1)
class Log<T> { // static run(value: T) { // 报错。注意:泛型不能运用于类的静态成员 // console.log(value) // return value // } run(value: T) { // console.log(value) return value } } let log1 = new Log<number>() log1.run(1) let log2 = new Log<string>() log2.run(‘1‘) let log3 = new Log(); // 不指定类型参数,value可以是任意类型的值 log3.run({ a: 1 })
interface Length {
length: number
}
function log<T extends Length>(value: T): T { // 继承Length接口,受到Length接口的约束 // console.log(value, value.length); return value; } log([1]) log(‘123‘) log({ length: 3 }) log(123) // 报错:number没有length属性
1) 增强程序的可扩展性:函数或类可以很轻松地支持多种数据类型
2) 增强代码的可读性:不必写多条函数重载,或者冗长的联合类型声明
3) 灵活地控制类型之间的约束
有了泛型,类型就像穿上了变色龙的外衣,可以很友好的融入各种环境,代码的灵活性就大大增强了。
下一章介绍TS的类型检查机制,尽情期待!
标签:系统 成员修饰符 src 好的 mic 继承 之一 完全 each
原文地址:https://www.cnblogs.com/xdfapp/p/13644171.html