解决JavaScript类型系统的问题,大大提高代码的可靠程度

内容概述

  • 强类型与弱类型
  • 静态类型与动态类型
  • JavaScript自由类型系统的问题
  • Flow静态类型检查方案
  • TypeScript语言规范与基本应用

**

类型系统


强类型与弱类型(类型安全)

  1. 强类型在语言层面限制函数的实参类型必须与形参类型相同,弱类型语言层面不会限制实参的类型。
  2. 强类型语言不允许有任意类型的数据隐式类型转换,而弱类型语言则允许任意的数据隐式类型转换。

JacaScript是弱类型语言,所有报出的类型错误,都是在代码层面,然后在运行时,通过逻辑判断手动抛出的。

静态类型与动态类型(类型检查)

静态类型:一个变量声明是它的类型就是明确的,声明过后,它的类型就不允许再修改。
动态类型:运行阶段才能明确变量的类型,而且变量的类型随时可以改变。

动态类型语言当中,它的变量是没有类型的,变量中存放的值是有类型的。

JavaScript类型系统特征

JavaScript是 弱类型 且 动态类型 的语言。
该语言的特点一是【任性】——缺失了类型系统的可靠性;二是【不靠谱】。

为什么JavaScript不是强类型/静态类型? 早期的JavaScript应用简单,JavaScript没有编译环节。弱类型且动态类型是早期JavaScript的优势。 如今,大规模应用下,这种【优势】就变成了短板。

弱类型的问题

  • 在运行时才能发现异常 ```javascript const obj = {}

obj.foo()

  1. obj没有foo方法,但是在语法层面是可行的,只是在运行时会报出错误,JavaScript必须在运行阶段才能够发现代码中的类型异常。
  2. ```javascript
  3. const obj = {}
  4. setTimeout(()=>{
  5. obj.foo()
  6. },10000)

像这种等待一段时间才会运行的代码,如果我们在测试的时候没有测试到这行代码,这个隐患就会留到代码当中,而强类型语言就没有这个问题,语法上就会报出错误。

  • 可能造成函数功能发生改变

    1. function sum (a,b) {
    2. return a + b
    3. }
    4. console.log(100,'100')//100100

    由于类型不明确,导致打印出的结果发生了根本性改变。

  • 不符合人的认知 ```javascript const obj = {}

    obj[true] = 100

console.log(obj[‘true’])//100

  1. 使用字符串true也能访问到属性,也就是说,如果我们不知道对象会把键名转换成字符串,通常会让人感到奇怪,不符合人的认知。
  2. > 总结:
  3. > - JavaScript当中的类型要等到运行时才能够被发现;
  4. > - 由于类型不明确,有可能会造成函数功能发生改变;
  5. > - 可能出现错误用法。
  6. 我们可以通过君子约定来避免,但是如果在大型项目中,使用君子约定有隐患,强制要求有保障。
  7. <a name="z0qOR"></a>
  8. ### 强类型的优势
  9. - 错误更早暴露
  10. - 代码更智能,编码更准确
  11. - 重构更牢靠
  12. - 减少不必要的类型判断
  13. <a name="l4obf"></a>
  14. ## **Flow静态类型检查方案**
  15. ---
  16. Flow——JavaScript的类型检查器。
  17. <a name="fYzyz"></a>
  18. ### 工作原理
  19. 让我们在代码当作,通过添加一个类型注解的方式来去标记我们代码当中,变量或者是参数是什么类型的,flow就会根据这些类型注解,就可以检查出代码当中是否会存在类型使用上的异常,从而实现在开发阶段对类型异常的检查。
  20. 这些类型注解可以使用Babel去除,所以说在生产环境当中不会有任何影响。
  21. <a name="VGJns"></a>
  22. ### 快速上手Flow
  23. 1. 安装flow, npm init -y -> cnpm flow-bin -D
  24. 1. package.json中增加执行指令, "flow""flow"
  25. 1. 初始化flow配置文件, npm run flow init
  26. 1. 在项目中使用
  27. - 方法一 通过注释(不推荐)<br />// @flow 注释之后的内容才能被flow检测<br />在需要检测的内容后面添加 :数据类型 ,说明其中类型<br />需要关闭JavaScript类型检测<br />安装flow-remove-types npm install flow-remove-types --dev编译移除注解 flow-remove-types .(源代码所在的目录) -d dist(输出目录)
  28. ```javascript
  29. // @flow
  30. let a: number = 3;
  • 方法二 安装babel, npm i babel-cli babel-preset-flow -D
    创建.babelrc文件
    1. {
    2. "presets": [
    3. "presets":["@babel/preset-flow"]
    4. }
    1. 运行babel编译移除注解 babel src -d dist

flow开发工具插件

flow Language Support

flow类型推断

可以根据代码的使用情况判断出变量和参数的类型。

flow数据类型

  • 原始数据类型 ``javascript number类型可以赋值的类型: 数值, NaN, Infinitylet a: number = NaN`

string类型

Boolean类型

void类型: 就是js中的undefined

null

  1. - **数组数据类型**
  2. ```javascript
  3. //Array需要泛型参数
  4. const arr1: Array<number> = [1,2,3]
  5. const arr2:number[] = [1,2,3]
  6. //元组
  7. 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’

  1. - **函数类型**
  2. ```javascript
  3. function foo (callback: (string,number) => void) {
  4. callback('string', 100)
  5. }
  • 特殊类型 ```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

  1. - **混合类型**
  2. ```javascript
  3. //Mixed接收任意类型的值
  4. function passMixed(value: mined){
  5. }
  6. passMined('string')
  7. passMixed(100)
  8. Any
  9. function passAny (value:any){
  10. }
  11. passAny('string')
  12. passAny(100)
  13. //Any是弱类型,mixed是强类型

flow扩展

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 并不推荐

**

数据类型

基本数据类型

  1. const a: string = 'foobar'
  2. const b: number = 100//NaN Infinity
  3. const c: boolean = true
  4. 在非严格模式下,以上数据类型都可以为空
  5. const e: void = undefined
  6. const f: null = null
  7. const g: undefined = undefined
  8. const h: symbol = symbol()//报错

标准库声明
**
在es5中没有symbol数据类型,所以const h: symbol = symbol()语句会报错
解决方法:
1.把target改成es2015,这样会默认引入es6标准库
2.使用lib选项指定我们所引用的标准库
“lib”:[“es2015”,”DOM”]

标准库就是内置对象所对应的声明文件

Object类型

Typescript中的Object数据类型并单指普通对象类型,而是泛指所有的非原始类型,也就是对象数组和函数。

  1. const foo: object = function () {} // [] // {}
  2. //使用普通对象类型方法
  3. //使用对象字面量
  4. const obj: {} = {}
  5. const obj: { foo: number, bar: string } = { foo: 123, bar: 'string' }
  6. //更专业的方式是使用接口

数组类型

  1. const arrr1: Array<number> = [1, 2, 3]
  2. const arr2: number[] = [1, 2, 3]

元组类型

明确元的数量以及每个元素的数据类型的数组。

  1. const tuple: [number, string] = [18, 'zce']

可以用数组下标的方式和数组解构的方式访问对应对象

枚举类型

它可以给一组数值取上一个比较好理解的名字,一个枚举中只会存在一个固定的值。

  1. //JavaScript没有枚举类型,使用对象模拟 枚举类型
  2. const PostStatus = {
  3. Draft: 0,
  4. Unpublished: 1,
  5. Published: 2,
  6. }
  7. //typescript有专门的枚举类型
  8. enum PostStatus {
  9. Draft = 0,
  10. Unpublished = 1,
  11. Published = 2
  12. }
  13. enum PostStatus {
  14. Draft,
  15. Unpublished,
  16. Published
  17. }
  18. //可以给状态指定值,也可以不指定值,后面的值会在第一个值上进行累加
  19. enum PostStatus {
  20. Draft = 'aaa',
  21. Unpublished = 'bbb',
  22. Published = 'ccc'
  23. }
  24. //状态的值除了是数字外,也可以是字符串,因为字符串不能进行累加,所有每一个状态都要初始化一个值。

枚举类型会入侵到我们的运行时的代码,换句话说就是它会影响我们编译后的结果。 我们在typescript使用的大多数类型,我们编译转化后会被移除掉,因为它只是让我们在编译当中做类型检查,而枚举不会,它最终会被编译成双向的键值对对象。 例如 PostStatus[PostStatus[“Draft”] = 0] = “Draft” PostStatus[PostStatus[“Unpublished”] = 1] = “Unpublished” PostStatus[PostStatus[“Published”] = 2] = “Published” 可以通过键获取值,也可以通过值去获取键 如果我们的代码当中,不会去用值去访问对应枚举名称,建议使用常量枚举 在enum前面加上const

函数类型

  1. function func1 (a: number,b: number): string {
  2. return 'func1'
  3. }
  4. //形参和实参必须完全对应
  5. //接收任意个数的参数
  6. rest: number[]
  7. const func2 = function (a: number, b: number): string {
  8. return 'func2'
  9. }

任意类型

  1. function stringify (value: any) {
  2. return JSON.stringigy
  3. }
  4. //any类型就是一个动态类型,跟普通的js变量是一样的
  5. //any 类型是不安全的

类型断言

在一些特殊的情况下,typescript无法推断出变量的具体类型,而我们开发者能明确知道这个变量的类型,这个时候我们可以断言。

断言方式
1. as关键字

const num1 res as number

  1. 使用<>断言

    const num2 = res //JSX 下不能使用

类型断言并不是类型转换,编译过后,类型断言就不存在了。

作用域问题

不同文件中出现重复定义变量

解决办法:
1.用一个立即执行函数包裹

  1. (function () {
  2. const a = 123
  3. })()

2.使用export导出
这样文件会作为一个模块,模块具有单独作用域

  1. export{}


接口Interfaces

一种规范,用来约定对象的结构,我们使用这个接口就必须要遵循这个接口全部的约定——约定对象当中有哪些成员,以及这些成员的数据类型。

  1. interface Post {
  2. title: string
  3. content: sting
  4. }
  5. function printPost (post: Post) {
  6. console.log(post.title)
  7. console.log(post.content)
  8. }
  9. printPost({
  10. title:'Hello TypeScript',
  11. content:'A javascript superset'
  12. })

接口就是用来约束对象的结构,编译过后看不到接口的代码,也就是说typescript的接口只是用来约束我们的对象结构的,在实际应用阶段,这个接口并没有意义。

可选成员、只读成员

  1. interface Post {
  2. title: string
  3. content: sting
  4. subtitle?: string //可选成员
  5. readonly summary: string//只读成员
  6. }

动态成员

  1. interface Cache {
  2. [prop: string]: string
  3. }

描述一类具体事物的抽象特征,用来描述一类具体对象的抽象成员。ES6以前,函数 + 原型 模拟实现类,ES6开始有了专门的class。TypeScript增强了class的相关语法。

类的基本使用

  1. class Persin {
  2. name: string //= 'init name'
  3. age: number
  4. constructor (name: string, age:number) {
  5. this.name = name
  6. this.age = age
  7. }
  8. sayHi (msg: string): void {
  9. console.log(`I am ${this.name},${msg}`)
  10. }
  11. }

类的属性在使用之前必须要进行声明

类的访问修饰符

  • 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}) } }

  1. 如果构造函数添加了private属性,该类就不能被实例化和被继承,可以通过添加静态方法,访问内部属性。
  2. ```javascript
  3. class Student extends Person {
  4. private constructor (name: string, age: number) {
  5. super(name, age)
  6. console.log(this.gender)
  7. }
  8. static create (name: string, age: number) {
  9. return new Student(name, age)
  10. }
  11. }
  12. const jack Student.create('jack', 18)

类的只读属性

readonly
readonly应该跟在访问修饰符的后面,我们可以在类型声明的时候直接通过等号的方式进行初始化,也可以在构造函数当中进行初始化,两者只能选其一。

类与接口

  1. interface Eat {
  2. eat (food: string): void
  3. }
  4. interface Run {
  5. run (distance: number): void
  6. }
  7. class Person implements Eat, Run {
  8. eat (food: string) : void {
  9. console.log(`优雅的进餐: ${food}`)
  10. }
  11. run (distance: number) {
  12. console.log(`直立行走: ${distance}`)
  13. }
  14. }
  15. class Animal implements Eat, Run {
  16. eat (food: string): void {
  17. console.log(`呼噜呼噜的吃: ${food}`)
  18. }
  19. run (distance: number) {
  20. console.log(``爬行: ${distance})
  21. }
  22. }

抽象类

跟接口类似,可以包含具体实现

  1. abstract class Animal {
  2. eat (food: string): void {
  3. console.log(`呼噜呼噜的吃: ${food}`)
  4. }
  5. abstract run (distance: number): void
  6. }
  7. class Dog extends Animal {
  8. run(distance: number): void {
  9. console.log(`四脚爬行,distance`)
  10. }
  11. }
  12. const d = new Dog()
  13. d.eat('xiam')
  14. d.run(100)

泛型Generics

把我们定义时不能明确的类型变成一个参数,在使用的时候再去传递这样一个类型参数。

  1. function createArray<T> (length: number, value: T): T[] {
  2. const arr = Array<T>(length).fill(value)
  3. return arr
  4. }

类型声明Type Declaration

一个成员在定义的时候因为种种原因没有类型的声明,然后我们单独为它做一个类型声明。

  1. declare function camelCase(input: string): string
  2. const res = camelCase('hello typed')

存在原因,为了兼容普通的js模块。