解决JavaScript类型系统的问题,大大提高代码的可靠程度
内容概述
- 强类型与弱类型
- 静态类型与动态类型
- JavaScript自由类型系统的问题
- Flow静态类型检查方案
- TypeScript语言规范与基本应用
类型系统
强类型与弱类型(类型安全)
- 强类型在语言层面限制函数的实参类型必须与形参类型相同,弱类型语言层面不会限制实参的类型。
- 强类型语言不允许有任意类型的数据隐式类型转换,而弱类型语言则允许任意的数据隐式类型转换。
JacaScript是弱类型语言,所有报出的类型错误,都是在代码层面,然后在运行时,通过逻辑判断手动抛出的。
静态类型与动态类型(类型检查)
静态类型:一个变量声明是它的类型就是明确的,声明过后,它的类型就不允许再修改。
动态类型:运行阶段才能明确变量的类型,而且变量的类型随时可以改变。
动态类型语言当中,它的变量是没有类型的,变量中存放的值是有类型的。
JavaScript类型系统特征
JavaScript是 弱类型 且 动态类型 的语言。
该语言的特点一是【任性】——缺失了类型系统的可靠性;二是【不靠谱】。
为什么JavaScript不是强类型/静态类型? 早期的JavaScript应用简单,JavaScript没有编译环节。弱类型且动态类型是早期JavaScript的优势。 如今,大规模应用下,这种【优势】就变成了短板。
弱类型的问题
- 在运行时才能发现异常 ```javascript const obj = {}
obj.foo()
obj没有foo方法,但是在语法层面是可行的,只是在运行时会报出错误,JavaScript必须在运行阶段才能够发现代码中的类型异常。
```javascript
const obj = {}
setTimeout(()=>{
obj.foo()
},10000)
像这种等待一段时间才会运行的代码,如果我们在测试的时候没有测试到这行代码,这个隐患就会留到代码当中,而强类型语言就没有这个问题,语法上就会报出错误。
可能造成函数功能发生改变
function sum (a,b) {
return a + b
}
console.log(100,'100')//100100
由于类型不明确,导致打印出的结果发生了根本性改变。
不符合人的认知 ```javascript const obj = {}
obj[true] = 100
console.log(obj[‘true’])//100
使用字符串true也能访问到属性,也就是说,如果我们不知道对象会把键名转换成字符串,通常会让人感到奇怪,不符合人的认知。
> 总结:
> - JavaScript当中的类型要等到运行时才能够被发现;
> - 由于类型不明确,有可能会造成函数功能发生改变;
> - 可能出现错误用法。
我们可以通过君子约定来避免,但是如果在大型项目中,使用君子约定有隐患,强制要求有保障。
<a name="z0qOR"></a>
### 强类型的优势
- 错误更早暴露
- 代码更智能,编码更准确
- 重构更牢靠
- 减少不必要的类型判断
<a name="l4obf"></a>
## **Flow静态类型检查方案**
---
Flow——JavaScript的类型检查器。
<a name="fYzyz"></a>
### 工作原理
让我们在代码当作,通过添加一个类型注解的方式来去标记我们代码当中,变量或者是参数是什么类型的,flow就会根据这些类型注解,就可以检查出代码当中是否会存在类型使用上的异常,从而实现在开发阶段对类型异常的检查。
这些类型注解可以使用Babel去除,所以说在生产环境当中不会有任何影响。
<a name="VGJns"></a>
### 快速上手Flow
1. 安装flow, npm init -y -> cnpm flow-bin -D
1. package.json中增加执行指令, "flow":"flow"
1. 初始化flow配置文件, npm run flow init
1. 在项目中使用
- 方法一 通过注释(不推荐)<br />// @flow 注释之后的内容才能被flow检测<br />在需要检测的内容后面添加 :数据类型 ,说明其中类型<br />需要关闭JavaScript类型检测<br />安装flow-remove-types npm install flow-remove-types --dev编译移除注解 flow-remove-types .(源代码所在的目录) -d dist(输出目录)
```javascript
// @flow
let a: number = 3;
- 方法二 安装babel, npm i babel-cli babel-preset-flow -D
创建.babelrc文件{
"presets": [
"presets":["@babel/preset-flow"]
}
运行babel编译移除注解 babel src -d dist
flow开发工具插件
flow Language Support
flow类型推断
可以根据代码的使用情况判断出变量和参数的类型。
flow数据类型
- 原始数据类型
``javascript number类型可以赋值的类型: 数值, NaN, Infinity
let a: number = NaN`
string类型
Boolean类型
void类型: 就是js中的undefined
null
- **数组数据类型**
```javascript
//Array需要泛型参数
const arr1: Array<number> = [1,2,3]
const arr2:number[] = [1,2,3]
//元组
const foo: [string,number] = ['foo',100]
- 对象数据类型 ```javascript const obj1: { foo?: string, bar: number } = {foo: ‘string’, bar: 100} //添加问号表示这个参数是可有可无的,可选的
const obj3: { [string]: string } = {}
obj3.key1 = ‘value’ obj3.key2 = ‘vslue2’
- **函数类型**
```javascript
function foo (callback: (string,number) => void) {
callback('string', 100)
}
- 特殊类型 ```javascript //字面量类型 //限制变量必须是某一个值 const a: ‘foo’ = ‘foo’
const type: ‘success’ | ‘waning’ | ‘danger’ = ‘success’
const b” string | number = ‘string’//100
type StringOrNumber = string | number const b: StringOrNumber = ‘string’
//mybe类型 const gender: ?number = null //相当于 const gender: number | null | undefined
- **混合类型**
```javascript
//Mixed接收任意类型的值
function passMixed(value: mined){
}
passMined('string')
passMixed(100)
Any
function passAny (value:any){
}
passAny('string')
passAny(100)
//Any是弱类型,mixed是强类型
flow扩展
- flow官网 https://flow.org/en/docs/types
- 第三方类型手册 https://www.saltycrane.com/cheat-sheets/flow-type/latest/
- flow运行环境API
http://github.com/facebook/flow/blob/master/lib/core.js
http://github.com/facebook/flow/blob/master/lib/dom.js
http://github.com/facebook/flow/blob/master/lib/cssom.js
http://github.com/facebook/flow/blob/master/lib/node.js
TypeScript
TypeScript 是 JavaScript 的超集,扩展了 JavaScript 的语法,因此现有的 JavaScript 代码可与 TypeScript 一起工作无需任何修改,TypeScript 通过类型注解提供编译时的静态类型检查。
TypeScript 可处理已有的 JavaScript 代码,并只对其中的 TypeScript 代码进行编译。
TypeSCript优缺点
优点
- TypeScript兼容性特别好
- 任何一种JavaScript运行环境都支持
- 功能更为强大,生态也更健全、更完善
- Angular/Vue.js 3.0中已经使用TypeScrpit
- TypeScript——前端领域中的第二语言
缺点
- 语言本身多了很多概念,好在TypeScript属于【渐进式】
- 项目初期,TypeScipt会增加一些成本,当开发大型项目时,比起带来的好处,缺点可以忽略了
基本使用
- 安装TypeScriptyarn init —yes yarn add typescript —dev
- 配置文件 yarn tsc —init
target 设置编译后javascri[t所采用的ECMAScript标准
module 输出的代码采用模块化方式
outDir 编译输出文件夹
rootDir 源代码所在文件夹
sourceMap 开启源代码测试
strict 开启严格模式 - 运行整个项目 tsc
- 中文错误消息 yarn tsc — locale zh-CN 并不推荐
数据类型
基本数据类型
const a: string = 'foobar'
const b: number = 100//NaN Infinity
const c: boolean = true
在非严格模式下,以上数据类型都可以为空
const e: void = undefined
const f: null = null
const g: undefined = undefined
const h: symbol = symbol()//报错
标准库声明
**
在es5中没有symbol数据类型,所以const h: symbol = symbol()语句会报错
解决方法:
1.把target改成es2015,这样会默认引入es6标准库
2.使用lib选项指定我们所引用的标准库
“lib”:[“es2015”,”DOM”]
标准库就是内置对象所对应的声明文件
Object类型
Typescript中的Object数据类型并单指普通对象类型,而是泛指所有的非原始类型,也就是对象数组和函数。
const foo: object = function () {} // [] // {}
//使用普通对象类型方法
//使用对象字面量
const obj: {} = {}
const obj: { foo: number, bar: string } = { foo: 123, bar: 'string' }
//更专业的方式是使用接口
数组类型
const arrr1: Array<number> = [1, 2, 3]
const arr2: number[] = [1, 2, 3]
元组类型
明确元的数量以及每个元素的数据类型的数组。
const tuple: [number, string] = [18, 'zce']
可以用数组下标的方式和数组解构的方式访问对应对象
枚举类型
它可以给一组数值取上一个比较好理解的名字,一个枚举中只会存在一个固定的值。
//JavaScript没有枚举类型,使用对象模拟 枚举类型
const PostStatus = {
Draft: 0,
Unpublished: 1,
Published: 2,
}
//typescript有专门的枚举类型
enum PostStatus {
Draft = 0,
Unpublished = 1,
Published = 2
}
enum PostStatus {
Draft,
Unpublished,
Published
}
//可以给状态指定值,也可以不指定值,后面的值会在第一个值上进行累加
enum PostStatus {
Draft = 'aaa',
Unpublished = 'bbb',
Published = 'ccc'
}
//状态的值除了是数字外,也可以是字符串,因为字符串不能进行累加,所有每一个状态都要初始化一个值。
枚举类型会入侵到我们的运行时的代码,换句话说就是它会影响我们编译后的结果。 我们在typescript使用的大多数类型,我们编译转化后会被移除掉,因为它只是让我们在编译当中做类型检查,而枚举不会,它最终会被编译成双向的键值对对象。 例如 PostStatus[PostStatus[“Draft”] = 0] = “Draft” PostStatus[PostStatus[“Unpublished”] = 1] = “Unpublished” PostStatus[PostStatus[“Published”] = 2] = “Published” 可以通过键获取值,也可以通过值去获取键 如果我们的代码当中,不会去用值去访问对应枚举名称,建议使用常量枚举 在enum前面加上const
函数类型
function func1 (a: number,b: number): string {
return 'func1'
}
//形参和实参必须完全对应
//接收任意个数的参数
…rest: number[]
const func2 = function (a: number, b: number): string {
return 'func2'
}
任意类型
function stringify (value: any) {
return JSON.stringigy
}
//any类型就是一个动态类型,跟普通的js变量是一样的
//any 类型是不安全的
类型断言
在一些特殊的情况下,typescript无法推断出变量的具体类型,而我们开发者能明确知道这个变量的类型,这个时候我们可以断言。
断言方式
1. as关键字
const num1 res as number
- 使用<>断言
const num2 = res //JSX 下不能使用
类型断言并不是类型转换,编译过后,类型断言就不存在了。
作用域问题
不同文件中出现重复定义变量
解决办法:
1.用一个立即执行函数包裹
(function () {
const a = 123
})()
2.使用export导出
这样文件会作为一个模块,模块具有单独作用域
export{}
接口Interfaces
一种规范,用来约定对象的结构,我们使用这个接口就必须要遵循这个接口全部的约定——约定对象当中有哪些成员,以及这些成员的数据类型。
interface Post {
title: string
content: sting
}
function printPost (post: Post) {
console.log(post.title)
console.log(post.content)
}
printPost({
title:'Hello TypeScript',
content:'A javascript superset'
})
接口就是用来约束对象的结构,编译过后看不到接口的代码,也就是说typescript的接口只是用来约束我们的对象结构的,在实际应用阶段,这个接口并没有意义。
可选成员、只读成员
interface Post {
title: string
content: sting
subtitle?: string //可选成员
readonly summary: string//只读成员
}
动态成员
interface Cache {
[prop: string]: string
}
类
描述一类具体事物的抽象特征,用来描述一类具体对象的抽象成员。ES6以前,函数 + 原型 模拟实现类,ES6开始有了专门的class。TypeScript增强了class的相关语法。
类的基本使用
class Persin {
name: string //= 'init name'
age: number
constructor (name: string, age:number) {
this.name = name
this.age = age
}
sayHi (msg: string): void {
console.log(`I am ${this.name},${msg}`)
}
}
类的属性在使用之前必须要进行声明
类的访问修饰符
- public: 公有属性,默认属性
- priavte:私有属性
protected:受保护的 ```javascript class Persin { publice name: string //公有属性,默认值 private age: number //私有属性 protected gender: boolean //受保护的
constructor (name: string, age:number) { this.name = name this.age = age this.gender = true }
sayHi (msg: string): void {
console.log(I am ${this.name},${msg}
)
}
}
如果构造函数添加了private属性,该类就不能被实例化和被继承,可以通过添加静态方法,访问内部属性。
```javascript
class Student extends Person {
private constructor (name: string, age: number) {
super(name, age)
console.log(this.gender)
}
static create (name: string, age: number) {
return new Student(name, age)
}
}
const jack Student.create('jack', 18)
类的只读属性
readonly
readonly应该跟在访问修饰符的后面,我们可以在类型声明的时候直接通过等号的方式进行初始化,也可以在构造函数当中进行初始化,两者只能选其一。
类与接口
interface Eat {
eat (food: string): void
}
interface Run {
run (distance: number): void
}
class Person implements Eat, Run {
eat (food: string) : void {
console.log(`优雅的进餐: ${food}`)
}
run (distance: number) {
console.log(`直立行走: ${distance}`)
}
}
class Animal implements Eat, Run {
eat (food: string): void {
console.log(`呼噜呼噜的吃: ${food}`)
}
run (distance: number) {
console.log(``爬行: ${distance})
}
}
抽象类
跟接口类似,可以包含具体实现
abstract class Animal {
eat (food: string): void {
console.log(`呼噜呼噜的吃: ${food}`)
}
abstract run (distance: number): void
}
class Dog extends Animal {
run(distance: number): void {
console.log(`四脚爬行,distance`)
}
}
const d = new Dog()
d.eat('xiam')
d.run(100)
泛型Generics
把我们定义时不能明确的类型变成一个参数,在使用的时候再去传递这样一个类型参数。
function createArray<T> (length: number, value: T): T[] {
const arr = Array<T>(length).fill(value)
return arr
}
类型声明Type Declaration
一个成员在定义的时候因为种种原因没有类型的声明,然后我们单独为它做一个类型声明。
declare function camelCase(input: string): string
const res = camelCase('hello typed')
存在原因,为了兼容普通的js模块。