“类型思维”之Typescript,你掌握了吗?
(一)背景
JavaScript是一门动态弱类型语言 对变量的类型非常宽容 而且不会在这些变量和它们的调用者之间建立结构化的契约。
试想有这么几个场景:
1: 你调用一个别人写的函数,但是这个人没有写注释,为了搞清楚参数类型,只能去看里面的逻辑
2: 为了保证代码的健壮性,你需要对一个函数的输入参数进行各种假设判断
3: 让你维护一个重要的底层类库,你不小心更换了一个参数类型,但是不知道有多少处的引用
4: 明明定义好的接口,可一连调就报错了,TypeError:Cannot read property ‘length’of undefined,于是你就找后端理论,这个接口定义不是数组么?
以上场景归根结底就是因为 javascript 是动态 弱类型语言,长时间在没有类型约束的环境下开发,就会造成“类型思维”的缺失,养成不良的编程习惯,这也是做前端开发的短板之一
1、强类型语言
在强类型语言中,当一个对象从调用函数传递到被调用函数时,其类型必须与被调用函数中声明的类型兼容 ——Liskov, Zilles 19
上图解释:有两个方法,方法A和方法B。方法A调用方法B的时候,x类型必须要和y类型兼容。兼容就意味着,y可以赋值给x,而且程序还可以正常运行 这是比较宽泛的定义,没有产生具体的规则,现在我们对强类型语言的定义会更精确一些 。
强类型语言:不允许改变变量的数据类型, 除非进行强制类型转换
以Java语言为例:
(左图)报错:提示类型不兼容,不能将布尔类型转换为整型
(右图)打印 x = 97。java将字符a进行了强制类型转换,将a的ACSLL码 强制给了x,这样x类型依然是整型
2、弱类型语言
弱类型语言: 在弱类型语言中, 变量可以被赋予不同的数据类型
以js语言为例。图解:把z赋值给x 7、打印 x 为 字符串'a' 。从这里就可以看出JS是一门弱类型语言
3、静态类型语言与动态类型语言
静态类型语言:在编译阶段确定所有变量的类型
动态类型语言:在执行阶段确定所有变量的类型
左图解 JS代码 :当编译器在看add方法这行代码的时候无法知道变量a、b的数据类型,只有在程序执行的时候,才能根据实际传递进来的参数再确定
右图解 C++代码: C++ 和JS不同的是,它在编译的时候就能够确定变量的数据类型,而且a、b类型一定是整型
4、静态类型与动态类型比较
1、 从对比来看,静态类型语言的优势要大于动态类型语言。如果是一门动态弱类型语言,就更会被打入鄙视链的底端。
2、 动态类型语言的支持者也有自己的理由
- 性能是可以改善的(V8引擎),V8在性能方便做得很好,相对损失的性能而言,语言的灵活性更重要
- 隐藏的错误可以通过单元测试发现
- 文档可以通过工具生成
其实,这种争论一直存在,这说明任何语言的特点都具有两面性,也是一个发展和进化的过程,不能一概而论,要看具体的场景和性价比。
比如js就是一门动态弱类型语言,但是它目前还是被广泛应用。这也引申出一道面试题:大家也可以一起思考一下: TS会替代JS么?
我认为TypeScript更多地是对JavaScript的补充,而不是替代品,而且两者的普及都在增长。
5、语言类型象限
横轴代表 从动到静。纵轴代表 从弱到强
这个图可以帮助我们理解 动态类型、静态类型、强类型和弱类型语言有哪些。
值得庆幸的是,开源社区也一直努力的解决这个问题,早在2014年 Facebook 推出了 Flow,微软也在同一年发布了 TypeScript 1.0 版本,它们都是致力于为JS提供静态类型检查,如今过去6年了。TS发展好像是更好一些,多个团队,比如 Angular和Vue,开始全面使用TS重构代码,甚至连Facebook自家的产品 比如 Jest和Yarn,都在从Flow向TS转移。 可以说在ECMScript标准推出静态类型检查之前,TS是当下解决此问题的最佳方案。
(二)Typescript是什么?
Typescript是拥有类型系统的JavaScript的超集。
可以编译成纯JavaScript。需要注意三点:
1. 类型检查。typescript会在编译代码时进行严格的静态类型检查,这就意味着,你可以在编写代码的阶段,发现可能存在的隐患,而不是上完线才发现
2. 语言拓展。typescript会包括来自ES6和未来提案中的特性,比如异步操作和装饰器,还有就是,也会从java语言借鉴某些特性,比如接口和抽象类。
3. 工具属性。typescript可以编译成标准的javascript,可以在任何浏览器操作系统上运行,无需任何运行时的额外开销,从这个角度上讲,typescript更像是一个工具,而不是一门独立的语言
1. 基本类型
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) {}
}
2. 类型注解
作用:相当于强类型语言中的类型声明
语法:(变量/函数):类型名称
3. 枚举
场景:如下图伪代码为例:权限操作判断函数,不同权限对应不同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
4. 接口
4.1 作用:接口可以用来约束对象、函数以及类的结构的类型,是一种代码编写的契约,我们严格遵守,而且不能改变
4.2 语法:interface (变量名称) { }
4.3 分类:
- 对象类型接口
- 函数类型接口
- 混合类型接口
// 定义一个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)
4.4 数字索引接口
interface StringArray {
[index: number]: string
}
let chars: StringArray = ['a', 'b']
4.5 字符串索引接口
interface Names {
[x: string]: string;
// [x: string]: any;
// y: number;
[z: number]: string; // 数字索引签名的返回值必须要是字符串索引签名返回值的子类型,因为javascript会进行类型转换,将number转换成string,这样就能保证类型的兼容性
// [z: number]: number;
}
使用接口的好处:可以思考变量的类型,也可以思考接口的边界问题,这个过程非常有利于培养‘类型思维’
5. 函数
5.1 函数定义几种方式:
- function, function a(x: number, y: number) => number
- 变量, let a:(x: number, y: number) => number
- 类型别名, type a = (x: number, y: number) => number
- 接口, interface a { (x: number, y: 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中形参和实参必须一一对应
5.2 可选参数
function add5(x: number, y?: number) {
return y ? x + y : x
}
add5(1)
5.3 增加默认值
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))
5.4 扩展运算符
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))
5.4 函数重载
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'))
6. 类
我们知道es6引入了class关键字,我们也终于可以像传统的面向对象语言那样去创建一个类。 总体上来说,ts的类覆盖了es6的类,同时也引入了其他的特性, 那么两者之间有什么不同呢?
6.1 类的实现
注意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)
6.2 类的继承
类的继承,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
}
6.3 类的成员修饰符
成员修饰符: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) // 类的静态成员也可以被继承
6.4 类的抽象类
抽象类: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'
6.4 类的多态
多态:在父类中定义一个抽象方法,在多个子类中对这个方法有不同的实现,在程序运行的时候,会根据多个对象,执行不同操作。
// 父类
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'
6.5 类的this类型(特殊类型)
定义:类的成员方法会返回一个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() // 父类和子类之间调用的连贯性
7. 类与接口的关系
接口可以像类一样,相互继承,一个接口可以继承多个接口。 但是,接口继承类时需注意,接口在抽离类的成员的时候,不仅抽离了公共成员,而且抽离了私有成员和受保护成员。
图解:
1、首先接口之间是可以相互继承的,这样可以实现接口的复用
2、类之间也可以相互继承,可以实现方法和属性的复用
3、接口可以通过类来实现,但是接口只能约束类的共有成员,不能约束类的私有成员
4、接口也可以抽离出类的成员。抽离的时候会包括共有成员,私有成员,受保护成员
7.1 类类型接口
一个类通过关键字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() {}
}
7.2 接口继承
接口继承接口。接口的继承可以抽离出可重用的接口。
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
}
8. 泛型
很多时候,我们希望一个函数或者一个类能支持多种数据类型,有很大的灵活性,如下图:
定义一个log函数,接收一个string,最后打印出这个字符串。但是,我希望这个字符串能接收一个字符串数组,根据前面介绍的,应该怎么实现呢?
1、可以通过函数重载实现
先定义一个接收字符串的函数,再定义一个接收字符串数组的函数,最后在一个比较宽泛的版本里打印实现
2、也可以通过联合类型实现
看起来比函数重载简便一些。
3、但是,现在我希望这个函数能接收任何类型的参数,实际上从前面的函数重载也可以看出,可以使用any类型。
看样子这个函数好像是满足了我们的需求。但是,产生了另外的一个问题,any类型会丢失一些信息,就是类型之间的约束关系,忽略了输入参数的类型和函数返回值的类型必须是一致的这个问题。
当一个调用者看见这个log函数的时候,完全无法获知这种约束关系,这个时候就需要用到“泛型”了!
那么什么是泛型呢???
8.1 什么是泛型
泛型定义:不预先确定的数据类型, 具体的类型在使用的时候才能确定。
8.2 泛型参数
泛型从字面上理解就是“一般的,广泛的,不需要预先定义的的数据类型” 下面我们用泛型改造一下上面的log函数。
function log<T>(value: T): T {
// console.log(value);
return value;
}
log<string[]>(['a', ',b', 'c'])
log(['a', ',b', 'c']) //ts的类型推断,可以省略类型参数,直接传入数组
除了上述的参数可以用泛型表示,函数、接口、类可不可以更灵活的去用泛型呢?答案是,必须能!
8.3 泛型函数
type Log = <T>(value: T) => T
let myLog: Log = log
看样子是不是很简单。
8.4 泛型接口
interface Log<T> { // 约束接口的所有成员
(value: T): T
}
// let myLog: Log = log; // 报错。注意:当泛型约束了所有的接口之后,使用的时候必须指定一个类型
let myLog: Log<number> = log
myLog(1)
8.3 泛型类
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 })
8.4 泛型约束
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属性
8.5 泛型好处
1) 增强程序的可扩展性:函数或类可以很轻松地支持多种数据类型
2) 增强代码的可读性:不必写多条函数重载,或者冗长的联合类型声明
3) 灵活地控制类型之间的约束
有了泛型,类型就像穿上了变色龙的外衣,可以很友好的融入各种环境,代码的灵活性就大大增强了。
下一章介绍TS的类型检查机制,尽情期待!
“类型思维”之Typescript,你掌握了吗?的更多相关文章
- 帮你培养类型思维TypeScript(一)
前言:作为一名程序员,相信你已经熟练掌握了JavaScript语言,由于其应用领域非常的广泛,所以算得上是每一个程序员必须要掌握的语言.但是JavaScript自身的缺点,相信每一个程序员也是深有体会 ...
- java编程思想-枚举类型思维导图
- 转载:《TypeScript 中文入门教程》 12、类型推导
版权 文章转载自:https://github.com/zhongsp 建议您直接跳转到上面的网址查看最新版本. 介绍 这节介绍TypeScript里的类型推论.即,类型是在哪里如何被推断的. 基础 ...
- TypeScript Type Innference(类型推断)
在这一节,我们将介绍TypeScript中的类型推断.我们将会讨论类型推断需要在何处用到以及如何推断. 基础 在TypeScript中,在几个没有明确指定类型注释的地方将会使用类型推断来提供类型信息. ...
- 在 Typescript 2.0 中使用 @types 类型定义
在 Typescript 2.0 中使用 @type 类型定义 基于 Typescript 开发的时候,很麻烦的一个问题就是类型定义.导致在编译的时候,经常会看到一连串的找不到类型的提示.解决的方式经 ...
- TypeScript入门-枚举、类型推论
枚举 使用枚举可以定义一些具有名字的数字常量,和在C语言中一样都是使用关键字enum enum Direction { Up = 1, Down = 1<<2, Left, Right } ...
- typescript枚举,类型推论,类型兼容性,高级类型,Symbols(学习笔记非干货)
枚举部分 Enumeration part 使用枚举我们可以定义一些有名字的数字常量. 枚举通过 enum关键字来定义. Using enumerations, we can define some ...
- TypeScript 之 基础类型、高级类型
基础类型:https://m.runoob.com/manual/gitbook/TypeScript/_book/doc/handbook/Basic%20Types.html 高级类型:https ...
- typescript接口的概念 以及属性类型接口
/* 1.vscode配置自动编译 1.第一步 tsc --inti 生成tsconfig.json 改 "outDir": "./js", 2.第二步 任务 ...
随机推荐
- mysql常用sql语法
一.创建主键的三种方式 1. CREATE TABLE user( uid INT PRIMARY KEY, uname VARCHAR(10), address VARCHAR(20) ) 2. C ...
- python调用接口方式
python中调用API的几种方式: - urllib2- requests 一.调用别人的接口 案例1.urllib2 import urllib2, urllib github_url ='htt ...
- GetOverlappedResult 函数
BOOL GetOverlappedResult( HANDLE hFile, LPOVERLAPPED lpOverlapped, LPDWORD lpNumberOfBytesTransferre ...
- 一文说清OpenCL框架
背景 Read the fucking official documents! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: 对不 ...
- jQuery 两个日期时间相减
var sDate='2016-10-31';var eDate='2016-10-10'var sArr = sDate.split("-");var eArr = eDate. ...
- 《SEO实战密码》
一.搜索引擎 1.蜘蛛访问网站时都会先访问网站根目录下的 robots.txt 文件. 2.蜘蛛喜欢访问经常更新的页面. 3.离首页点击距离越近,页面权重越高. 4.使用"" + ...
- ORACLE ORA-00933: SQL 命令未正确结束,
这个错误害我花了一天时间排查,最后原来是因为结束符,这种语句不能是分号,将分号即可执行成功. MERGE INTO MO_TRADE_COUNT_DAY A USING ( SELECT MAX(fl ...
- 内置函数 字符串比较 strcmp 登录密码
1 //内置函数 字符串比较 strcmp 2 // 原理:将两个字符串从首字母开始,按照ASCII码的顺序逐个比较 3 //字符串1 == 字符串2 返回0 4 //字符串1 < 字符串2, ...
- C++面向对象总结——虚指针与虚函数表
最近在逛B站的时候发现有候捷老师的课程,如获至宝.因此,跟随他的讲解又复习了一遍关于C++的内容,收获也非常的大,对于某些模糊的概念及遗忘的内容又有了更深的认识. 以下内容是关于虚函数表.虚函数指针, ...
- 第4篇-JVM终于开始调用Java主类的main()方法啦
在前一篇 第3篇-CallStub新栈帧的创建 中我们介绍了generate_call_stub()函数的部分实现,完成了向CallStub栈帧中压入参数的操作,此时的状态如下图所示. 继续看gene ...