JavaScript自有类型系统的问题分析以及主流解决方案

1,197 阅读8分钟

分析

类型系统

类型安全角度 - 是否可以允许隐式类型转换

  • 强类型 - 强类型语言不允许任意的隐式类型转换
    • 强类型语言在参数传入时就会限制类型, 会报错
  • 弱类型 - 弱类型语言则允许任意的数据隐式类型转换
    • 弱类型语言在传入时不会限制,也不不会报错, 但会存在传入参数类型与运行代码参数要求类型不同 而导致运行结果与预期不同

类型检查角度 - 声明过后是否可以随时允许被再次修改

  • 静态类型语言 - 一个变量声明时就明确变量类型,明确过后不允许再次修改
  • 动态类型语言 - 运行阶段才能够明确变量类型,变量类型可以随时发生变化

javascript类型语言特征 : 弱类型且动态

  • 早期javascript非常简单
  • 没有编译环节

弱类型产生的问题

  • 1、异常需要等到运行时才能发现
const obj = {}

obj.foo()
  • 2、函数功能可能发生改变
function sum (a, b) {
  return a + b
}
console.log(sum(100, 100))
console.log(sum(100, '100'))
  • 3、对象索引器的错误用法
const obj = {}
obj[true] = 100 // 属性名会自动转换为字符串
console.log(obj['true'])

强类型的优势

  • 1、强类型代码错误更早暴露
  • 2、强类型代码更智能,编码更准确
  • 3、重构更可靠
  • 4、减少了代码层面上的不必要的类型判断
function sum (a, b) {
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new TypeError('arguments must be a number')
  }

  return a + b
}

解决方案

TypeScript

  • javascript 的超集
  • Typescript ( javascript、类型系统、ES6+ ) 编译为 javascript去工作
  • 渐进式的, 可以完全按照 javascript 标准语法编写代码

原始数据类型

const a: string = 'string'

const b: number = 10 // NaN Infinity

const c: boolean = true
// 在非严格模式(strictNullChecks)下,
// string, number, boolean 都可以为空
// const d: string = null
// const d: number = null
// const d: boolean = null

const e: void = undefined

const f: null = null

const g: undefined = undefined

// Symbol 是 ES2015 标准中定义的成员,
// 使用它的前提是必须确保有对应的 ES2015 标准库引用
// 也就是 tsconfig.json 中的 lib 选项必须包含 ES2015
const h: symbol = Symbol()

作用域问题

// 默认文件中的成员会作为全局成员
// 多个文件中有相同成员就会出现冲突
// const a = 123

// 解决办法1: IIFE 提供独立作用域
// (function () {
//   const a = 123
// })()

// 解决办法2: 在当前文件使用 export,也就是把当前文件变成一个模块
// 模块有单独的作用域
const a = 123

export {}

Object 类型

  • object 类型是指除了原始类型以外的其它类型
export {} // 确保跟其它示例没有成员冲突

// object 类型是指除了原始类型以外的其它类型
const foo: object = function () {} // [] // {}

// 如果需要明确限制对象类型,则应该使用这种类型对象字面量的语法,或者是「接口」
const obj: { foo: number, bar: string } = { foo: 123, bar: 'string' }
// 结构必须和声明类型一致, 不能多也不能少

Array 类型

export {} // 确保跟其它示例没有成员冲突

// 数组类型的两种表示方式

const arr1: Array<number> = [1, 2, 3]

const arr2: number[] = [1, 2, 3]

// 案例 -----------------------

// 如果是 JS,需要判断是不是每个成员都是数字
// 使用 TS,类型有保障,不用添加类型判断
function sum (...args: number[]) {
  return args.reduce((result, current) => result + current, 0)
}

sum(1, 2, 3) // => 6

Tuple (元组)

  • 明确元素数量,以及明确每个元素的类型的数组
const tuple: [number, string, boolean] = [12,'das', true]

// tuple[0] => 12
// tuple[1] => 'das'

const [num, desc] = tuple

Enum (枚举)

// 用对象模拟枚举
// const resultStatus = {
//   fail: 0,
//   success: 1,
//   pengding: 2
// }

// 标准的数值枚举, 枚举值不设置初始值会自动基于前一个值自增
// enum resultStatus = {
//   fail = 0,
//   success = 1,
//   pengding, // => 2
// }

// 字符串枚举
// enum resultStatus = {
//   fail = 'fail',
//   success = 'success',
//   pengding = 'pending',
// }

// 常量枚举, 不会侵入编译结果
const enum resultStatus = {
  fail,
  success,
  pengding,
}

const result = {
  code: '200',
  message: '',
  data: resultStatus.fail
}

// resultStatus[0] // => fail

函数类型

  • 对函数参数和返回值进行约束
function foo (a: number, b: number): string {
  return 'fooResult'
}

// ts调用函数需要形参与实参个数相同
// 可以通过ts可选参数 ( b? : number )和添加默认值( b: number = 20 )的方式解决
// 两种方式都必须放置在形参的最后位置
foo(10, 20)
foo(10)

any ( 任意类型、弱类型 )

value: any

  • 允许存放任意类型的值
  • 语法上不会报错
  • 不会有任何类型检测,所以存在类型安全问题,所以不建议使用
  • 在兼容老代码上很方便

隐式类型推断

// 类型推断为 number, 后续再给赋值其他类型会报错
let age = 18 // number
// age = 'string'

// 类型推断为 any, 所以后续赋值不会报错
let foo
foo = 100
foo = 'string'

// 虽然会简化一些代码, 但还是建议为每个变量添加明确的类型标注, 方便后期维护,与代码注释的好处一致

类型断言 ( as )

  • 告诉ts 我确定他是什么类型

两种实现方式 :

  • const result1 = res as number
  • const result2 = res // 注意 会和jsx 语法冲突

类型断言与类型转换的区别

  • 类型断言是在编译阶段
  • 类型转换是在运行阶段

interfaces ( 接口 )

  • 约定一个对象当中具体有哪些成员,并且成员的类型
interface Get {
  name: string
  desc: string
}

function printGet (get: Get) {
  console.log(get.name)
  console.log(get.desc)
}

printGet({
  name: 'name',
  desc: 'desc'
})
  • 可选成员 - 可传可不传
interface Info {
  subname?: string
}
  • 只读成员 - 不允许外界修改
interface Info {
  readonly idno: string
}
  • 动态成员 - 不确定具体成员
// [key: string]中 key 代表是键名, string代表键名的类型
interface Info {
  [key: string]: string
}

const info: Info = {}
info.name = 'gkm'
info.age = '18'

calss ( 类 )

  • 描述一类事物的具体特征
  • typeScript增强了class的相关语法

修饰符

  • 1、public:public表明该数据成员、成员函数是对所有用户开放的,所有用户都可以直接进行调用
  • 2、private:private表示私有,私有的意思就是除了class自己之外,任何人都不可以直接使用。
  • 3、protected:protected对于子女、朋友来说,就是public的,可以自由使用,没有任何限制,而对于其他的外部class,protected就变成private。
对于构造函数的访问修饰符问题
  • 构造函数的访问修饰符,默认也是public, 如果我们把构造函数的修饰符设置成 private , 那么这个类型就不能在外部被实例化,同时也不能被继承.这种情况下, 我们就只能在这个类的内部添加一个静态方法( 使用 Static 关键字), 在静态方法中创建类型的实例, 因为 private 只能在内部访问

readonly ( 只读 )

只读属性只能在属性声明阶段直接赋值或者在构造函数中初始化. 两者只能选择其中一种

class Person {
  // name: string = ‘gkm’
  public name: string // 公有属性, 不加修饰符默认为公有属性
  private age: number // 私有属性, 只能在内部访问
  // readonly 只读属性 有修饰符的情况下要跟在修饰符的后面
  protected readonly gender: boolean // 受保护的属性, 只能在子类中访问, 在实例中不能访问

  constructor(name: string, age: number) {
    // 如果初始属性没赋值,就需要加上
    this.name = name
    this.age = age
    this.gender = true
  }

  sayHi (msg: string) :vaid {
    console.log(`I am ${name}, ${msg}`)
  }
}

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

// public constructor
// const gkm = new Person('gkm', 18)
// private constructor
// const gkm = Student.create('gkm', 18)

类与接口

  • 接口可以把一些类中的共有的属性和方法抽象出来,用来约束实现此接口的类
  • 一个接口可以约束多个类,一个类可以抽象出多个接口
  • 我们利用 implements 关键字来实现
  • 用接口约束的类中必须包含这个方法,否则会报错
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 关键字实现抽象类和方法
  • 抽象类只能被继承, 不能再使用 new 的方式定义实例对象, 子类可以对抽象类进行不同的实现
  • 抽象方法只能出现在抽象类中并且抽象方法不能在抽象类中被具体实现,只能且必须在抽象类的子类中实现
  • 我们一般用抽象类和抽象方法抽离出事物的共性 以后所有继承的子类必须按照规范去实现自己的具体逻辑
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('嗯西马')
d.run(100)

Generics ( 泛型 )

  • 泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性
function createNumberArray (length: number, value: number): number[] {
  const arr = Array<number>(length).fill(value)
  return arr
}

function createStringArray (length: number, value: string): string[] {
  const arr = Array<string>(length).fill(value)
  return arr
}

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

// const res = createNumberArray(3, 100)
// res => [100, 100, 100]

const res = createArray<string>(3, 'foo')

类型声明

  • 在typescript中我们引用第三方模块,如果这个模块中不包含所对应的模块声明文件, 我们可以尝试安装所对应的类型声明模块, 一般命名为 @types/后面对应模块名(比如 : @types/lodash ), 如果没有这种对应模块, 那我们就只能自己使用 declare 语句声明说对应的模块类型
declare function foo (name: string): string