解决JavaScript 类型系统的问题以及了解TypeScript
本文主要要了解的是JavaScript自有类型系统的问题以及解决这些问题的方法,而 TypeScript是解决这些问题的方法当中涉及到的语言。
- TypeScript 解决 JavaScript 类型系统的问题
- TypeScript 可以大大提高代码的可靠程度
主要了解以下几个重要的点
- 强类型与弱类型
- 静态类型与动态类型
- JavaScript 自有类型系统的问题
- Flow 静态类型检查方法
- TypeScript 语言规范与基本应用
解决JavaScript 类型系统的问题
从类型安全可以分为强类型 弱类型
从类型检查可以分为静态类型 动态类型
1. 强类型与弱类型
从类型安全的维度编程语言分为 强类型 弱类型
- 强类型 在语言层面限制函数的实参类型必须与形参类型相同
class Main {
static void foo (int num) {
System.out.println(num)
}
public static void main (String[] args) {
Main.foo(100); // ok
Main.foo('100') // error "100" is a string
Main.foo(Integer.parseInt('100')) // ok
}
}
- 弱类型 在语言层面不会限制实参的类型
function foo(num) {
console.log(num);
}
foo(100) // ok
foo('100') // ok
foo(parseInt('100')) // ok
由于这种强弱之分根本不是某一个权威机构的定义, 当时也没有明确的约定导致对界定细节有不一样的理解
-
强类型有更强的类型约束, 而弱类型中几乎没有什么约束
-
强类型中不允许任意的数据隐式类型转换,而弱类型中允许
-
强类型是语言的语法层面就约束了类型, 如果传入了不同类型爱编译阶段就会报错,而不是在运行中通过判断去限制
变量类型允许随时改变的特点 不是强弱类型的差异(python 的变量是允许改变类型的,但是python是一门强类型语言)
2. 静态类型与静态类型
从类型检查或者类型系统的维度分为静态类型与静态类型
- 静态类型语言: 一个变量声明时就确定了类型 ,声明后不允许再修改
- 动态类型语言: 运行时才能明确变量的类型,且变量的类型随时可以改变
var foo = 100
foo = 'bar'
console.log(foo);
动态类型的语言中 变量是没有类型的,变量中存放的值是有类型的
-
强类型 弱类型(类型安全) 主要区别就是是否允许隐式的类型转换
-
静态类型 动态类型(类型检查) 主要区别就是类型是否可以随意改变
-
两个维度的区分不能混淆, 强类型不一定是动态类型 弱类型也不一定是静态类型
JavaScript是一门弱类型且是动态类型的语言 语言本身的类型系统是很薄弱的,甚至可以说是没有类型系统,没有类型限制 灵活多变但是丢失了 类型系统的可靠性。
为什么JavaScript不能是强类型 / 静态类型的语言呢 ?
- 之前的JavaScript应用非常的简单 ,在简单需求的时候 类型限制显得多余
- JavaScript是一种脚本语言 脚本语言没有编译环节,静态类型语言需要再编辑阶段做类型检查,所以JavaScript也不能设计成静态类型语言
- 大规模应用下, 没有类型限制这种优势 就编程了短板
3. 弱类型语言开发的问题与强类型的优势
- 弱类型语言开发的问题
const obj = {}
obj.foo()
运行阶段才能发现代码异常 ,不立即运行的话可能一直有隐患,强类型 书写时语法就会报错
function sum(a, b) {
return a + b
}
console.log(100, 100);
console.log(100, '100');
虽然可以通过约定来规避 但是也是治标不治本,多人协作开发的时候约定可能也不会特别清晰,就会导致代码逻辑错误,强类型语言可以完全解决这个问题
const obj = {}
obj[true] = 10
console.log(obj['true']); // 10
声明的一个布尔值属性名 但是字符串也可以取到 就是因为javaScript是弱类型语言
语法层面的强制要求比约定来的更靠谱些
- 强类型的优势:
- 错误更早暴露 编译就会暴露出问题, 不用等到运行才去debug
- 代码更加智能 编码更准确 强类型时代码提示会不起作用
- 重构更牢靠 重构指的是对代码有破坏性的改动 例如修改已经存在的成员名称 或者删除对象中的某个成员
- 减少不必要的类型判断
4. Flow
Flow是一款JavaScript的类型检查器 为JavaScript提供更完善的类型系统
// 使用方法:
// @flow
function square (n: number) {
return n * n
}
square('100')
Flow 允许变量后面以冒号的形式写类型注解,根据类型注解判断代码是否存在类 型使用异常
实现开发中规避一些代码类型使用上的错误,就不用了等到运行才能知晓了
function sum(a:number, b:number) {
return a + b
}
// a:number 这样的书写方式叫做类型注解
Flow的特点 并不要求给每一个变量都增加类型注解
Flow相比于TypeScript来说只是一个工具几乎不需要什么学习成本,而TypeScript是一门语言需要学习成本比较高
- 安装flow检查工具
- 添加 @flow注释标记
- 添加类型注解
- 使用flow命令检测代码
有两种主流方案
-
使用low-remove-types
-
运行 npm install flow-remove-types --dev
npm install flow-remove-types 第一个参数就是目录 - d 第二个参数为输出目录 -
一般我们会创建一个src文件来存放源代码, 这样也可以避免我们使用flow去掉了第三方模块的类型注解
-
实际就是把编写的代码和生产运行的代码分开 ,在这中间加入编译环节 在开发阶段就可以使用flow的方法来区分类型
-
-
使用babel配合插件也可以达到去掉类型注解:
编译 最常见的编译工具就是babel,babel配合插件也可以达到去掉类型注解的功能-
安装babel npm install @babel/core @babel/cli @babel/preset-flow --dev
@babel/core 是babel的核心模块 @babel/cli可以让我们在命令行直接使用babel命令 @babel/preset-flow就是用来删除类型注解的 -
需要先自行添加一个babel的文件.babelrc, 在.babelrc 的文件中写入下面代码
-
之后就可以通过babel命令来去掉类型注解了 npm babel src -d dist 就是把src文件中的源码去掉注解后放到dist目录中
-
// .babelrc 文件中定义presets
{
"presets": ["@babel/preset-flow"]
}
插件扩展中有Flow Language Support 这个是Flow官方提供的插件, 安装后在类型错误的位置会有波浪线去表示
-
4.5 Flow类型推断 Type Inference
flow可以根据代码使用情况推断出传入参数的类型, 同时提醒我们就是类型推断,即使在没有使用类型注解的情况下, Flow根据类型推断也可以帮我们判断类型使用的问题,但是还是推荐使用类型注解 。
-
4.6 Flow类型注解 Type Annotations
建议尽可能使用类型注解
标记变量类型以及函数返回值类型 和函数参数类型
// 函数参数类型注解
function square(n: number) {
return n * n
}
// 标记变量类型注解
let num: number = 100
// 函数返回值类型注解
function foo():number {
return 100
}
@flow
const a: string = 'a';
const b: number = 100 || NaN || Infinity // js中 NaN也属于数字 以及无穷大
const c: boolean = false || true
const d: null = null
const e: void = undefined // undefined需要标记为void 和函数参数如果返回undefined 也要标记为void
const f: symbol = Symbol
@flow
const arr1: Array < number > = []
@flow
const obj1: {
foo: string,
bar: string
} = {foo:'123',bar :'456'} // 这样定义就要求这个对象必须要有一个 foo 属性和一个bar属性 值类型必须为字符串
const obj2: {
foo: String,
bar: number
} = {
bar: 100
} // 这样定义就要求这个对象必须要有一个 foo 属性 值类型为字符串 和一个bar属性 值类型必须为数字
const obj3: {
[string]: string
} = {} //这样定义表示这个对象可以有无数个键值对,前提是键和值都必须为字符串
obj3.key1 = 'value1'
obj3.key2 = 'value3'
@flow
function foo(callback: (string, number) => void) {
callback('string', 100)
}
foo(function (str, n)) {
// str => string
// n =>number
}
@flow
// 1. 字面量类型
const a:'foo' = 'foo'
// 2.. 联合类型或者 | 或类型
const type: 'success' | 'warning' | 'danger' = 'success'
// 还可以用type声明一个类型用于表示联合类型以后的结果
type StringOrNumber = string| number 相当于别名 可以重复使用
const b : StringOrNumber = 100
// 3. maybe类型
const gender: ?number = nudefined
// maybe类型相当于在基础类型的基础上扩展了 null 和undefined 用一个问号
const gender: number | void | null = undefined
// mixed = string | number | null | boolean | Function | Array | Object
function passMixed(value:mixed) {
if (typeof value === 'string') {
value.substr(1)
}
if (typeof value === 'nunmber') {
value * value
}
}
passMixed('string')
passMixed(100)
// any类型也有和mixed有一样的效果
function passMixed(value:any) {
value.substr(1)
value * value
}
passMixed('string')
passMixed(100)
两者差异 any是弱类型 mixed是强类型
实际使用尽量不要使用any类型 ,any存在的意义是为了兼容旧项目的代码
就是运行环境所产生的类型限制
const element: HTMLElement | null = document.getElementById(‘app’) // 这个位置如果传入数字 会报错 然后返回一个html的节点,如果没有找到会返回null
TypeScript
- TypeScript是JavaScript的超集
- 支持转换新特性,最低可以编译ES3版本
- 任何一种JavaScript运行环境都支持 因为最后TypeScript是编译成JavaScript的
- 相比Flow 生态健全 更完善 功能更强大 Flow稍微有点卡
- TypeScript是前端领域中的第二语言
缺点是:
-
语言本身多了很多概念 接口 泛型 枚举(TypeScript属于渐进式的 即使没有了解过也可以当做JavaScript去使用)
-
项目初期 TypeScript 会增加一些成本
1. TypeScript的使用
先安装 npm i --save --dev typescript
const hello = name => {
console.log(`hello , ${name}`);
}
hello('zhangsan')
yarn tsc 04-TypeScript.ts 运行之后会产生一个同名的js文件
const hello =( name:string) => {
console.log(`hello , ${name}`);
}
hello('zhangsan')
- 安装模块 安装后TypeScript提供了一个编译的模块 tsc
- tsc命令就可以去编译TypeScript文件 先检查类型使用异常 然后移除类型注解 还会转换ECMAScript的新特性
2. 配置文件
运行命令
npm i typescript --dev
tsc .\04-TypeScript.ts
生成配置文件 tsconfig.json 运行命令 tsc --init
文件内容如下
{
"compilerOptions": {
"module": "commonjs", // 输出代码用什么模式进行模块化
"target": "es5", // 设置编译后的js所采用的的ECMAScript的标准
"noImplicitAny": false,
"sourceMap": false, // true 开启源代码映射,可以通过.map 的文件去调试TypeScript文件的代码
"outDir": "dist" ,// 代码编辑结果输出的文件夹
"strict": true // 开启所有严格检查的选项 类型检查不会十分严格 严格模式下需要给每个变量声明类型
},
"exclude": [
"node_modules"
]
}
3. 原始类型
const a: string = 'foo'
const b: number = 123
const c: boolean = true
const d:string = null // "strict": true 提示为不能将类型“null”分配给类型“string”。
const e:void = undefined | null // 严格模式下 只能是undefined
const f :null = null
const g: undefined = undefined
const h: symbol = Symbol() 直接创建会报错 "Symbol" 仅指类型,但在此处用作值。因为在配置文件中的 target选择的是es5 而Symbol是es2015中声明的 所以会报错
4. 标准库声明
标准库-- 就是内置对象所对应的声明文件
解决symbol报错有两种方法
- 修改target的值为es2015
- 配置文件中的lib是一个数组 可以写多个标准库
lib中的数据会覆盖 target中的, 所以lib的标准库中尽量写全当前项目应用的所有的标准库
5. 显示中文错误消息
- 可以运行 tsc --locale zh-CN // 即可显示报错为中文消息
- 在vsCode中设置 文件 - 首选项 - 设置 - 搜索TypeScript-locale 设置值为zh-CN
6 . 作用域问题
不同文件中变量名称相同
声明在全局的变量名称重复会报错
-
解决办法是用函数包起来
-
可以把文件以模块的形式导出, 模块是有自己的作用域的 所以也是可以的
export { }
7. Object类型
Object类型 并不仅仅指对象它泛指除了原始类型以外的其他类型包括 函数 数组 和对象这几种类型
export { }
const foo: object = function () {
}
// 如果要确保声明的为对象 可以使用对象字面量的形式
const obj: { foo: string, bar: number } = {
foo: 'value',
bar:123
}
更专业的方式是使用接口
8. 数组类型
const arr1: Array<number> = [1, 2]
const arr2: number[] = [1, 2, 3]
function sum(...args :number[]) {
需要判断每个数组成员都是数字 之前需要一堆判断 现在只需要给参数加number【】即可
return args.reduce((prev, current) => prev + current, 0 )
}
9. 元祖类型 Tuple Types
const tuple: [number, string] = [18, 'zhangsan']
const age = tuple[0]
const name = tuple[1]
const [age,name] = tuple
元祖一般用来在函数中返回多个返回值
Object.entries() // 这个方法就返回一个元祖 类型和长度是固定的
10. 枚举类型 Enum Types
经常会用到用某几个值表示不同的状态
一般js中会使用一个对象来模拟枚举类型
const postStatus = {
Draft: 0,
Unpublished:1,
published:2
}
const post = {
title: 'hello world',
content: 'bye bye world',
// status: 0
status: postStatus.Draft
}
枚举类型的特点 :
1. 可以给每个数据起更好的名字来说明这个值
2. 枚举中只会存在固定的值
TypeScript中会使用Enum来声明一个枚举类型
enum PostStatus {
Draft = 0,
Unpublished = 1,
published = 2
}
注意: 枚举值位数字的话,枚举的值可以不指定, 不指定的话默认从有值的枚举值开始累加
枚举的值除了可以是数字之外还可以是字符串, 字符串需要写入每个枚举值
枚举类型会入侵运行代码也可以说影响编译结果,枚举最后会编译称为一个双向的键值对对象
双向的键值对对象: 就是可以通过值去获取键 也可以通过键去获取值
如果不需要去获取枚举的键和值建议使用常量枚举
就是在enum前加const 这样的话编译过后 在枚举值的地方都会替换成对应的值
11 函数类型
函数声明式
function func1(a:number, b:number) :string {
return 'func1'
}
func1(10,20)
函数表达式
const func2:(a:number,b:number) => string = function (a:number,b:number):string {
return 'func2'
} // 函数表达式是一个变量,变量有可能会成为参数。所以写法为上面类型箭头函数的写法 规定变量func2参数以及返回值的类型
12 任意类型
由于js为弱类型 很多内置API会支持任意类型的数据,ts基于js所以需要一个类型来接受任意类型的数据
function stringify(value:any) {
return JSON.stringify(value)
}
stringify('string')
stringify(123)
stringify(true)
let foo: any = 'string'
foo = 123
foo.bar()
any类型在语法上的书写就可以像js一样 语法上都不会报错
any类型不会存在类型检查 但是不建议去用any类型
13 隐式类型推断
没有明确用类型注解去标记这个变量
let age = 18 // 此时ts会推测这个变量类型为number
age = ‘string’ // 推测为数字类型后再赋值其他类型会报错
如果无法推断类型 就会默认这个变量的类型是any,如果赋值为数字就会推断为数字类型
14 类型断言
一些时候ts是无法推断变量类型的,但是作为我们开发人员是知晓的
假定这个nums 来自一个明确的接口
const nums = [11, 12, 13, 14]
const res = nums.find(i => i > 0)
const square = res * res
断言的两种写法
1. const num1 = res as number
2. const num2 = <number>res // JSX下不能使用
类型转换不是类型断言, 类型断言是发生在编译过程的 运行时会去掉,而类型转换是发生在运行过程中的
15 接口 interfaces
一种规范一种契约 约定对象的结构 一种抽象的概念
明确的体现就是去约定一个对象的成员和成员的类型
function printPost(post) {
这样是隐式的要求我们传入的post必须要有title和content两个属性
console.log(post.title);
console.log(post.content);
}
明确的写法:
interface Post {
title: string,
content: string
}
function printPost(post : Post) {
console.log(post.title);
console.log(post.content);
}
printPost({
title: '123',
content:'456'
})
printPost({
title: 123, //不能将类型“number”分配给类型“string”
content:'456'
})
接口只是做开发时的类型约束的,实际运行中接口并没有意义
接口中的约束的成员 有一些特殊的
可选成员 只读成员 动态成员
interface Post {
title: string,
content: string,
subtitle? : string, // 可选成员
readonly summary: string,//只读成员
}
// 动态成员
interface Cache {
[prop:string] : string //动态成员
}
const cache: Cache = {}
cache.foo = 'value2'
cache.bar = 'value2'
16 类 classes
面向对象编程中一个最重要的概念
作用 描述一类具体事物的抽象特征 手机属于一个类型 智能手机
TypeScript中增强了class的相关语法 特殊访问修饰符 和
新的类的特性
class Person {
// 参数需要先声明 以此来给参数做类型标注 否则会报错
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
sayHi(msg: string): void {
console.log(`I am ${this.name},${msg}`);
}
}
17 访问修饰符
class Person {
// 参数需要先声明 以此来给参数做类型标注 否则会报错
public name: string
private age: number
protected gender: boolean
constructor(name: string, age: number) {
this.name = name
this.age = age
}
sayHi(msg: string): void {
console.log(`I am ${this.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)
}
}
const tom = new Person('tom', 18)
console.log(tom.name);
console.log(tom.age); //私有
console.log(tom.gender); // 子集可以访问
- public // 公有的 所有的成员默认都是公有的可以不加
- private // 私有的 外部不可以访问
- protected // 只允许在子类中访问的成员
构造函数的访问修饰符 如果设置有私有的话就只能在类的内部来使用 不能实例化和在外部使用了 ,
可以在类的内部使用 static 来创建一个静态方法来实例化这个类以便于在外面调用
设置类为只读 readonly如果属性已经有访问修饰符了 只读应该写在访问修饰符的后面
18类与接口
类与类之间重合的可以用接口抽象出来
i
nterface EatAndRun {
eat(food: string): void
run(distance: number): void
}
interface Eat {
eat(food: string): void
}
interface Run {
run(distance: number): void
}
implements Eat, Run || implements EatAndRun
class Person implements EatAndRun{
eat(food: string) {
console.log(`优雅吃饭: ${food}`);
}
run(distance: number) {
console.log(`直立行走:${distance}`);
}
}
class Animal implements Eat, Run {
eat(food: string) {
console.log(`呼噜噜: ${food}`);
}
run(distance: number) {
console.log(`爬行:${distance}`);
}
}
19 抽象类
用abstract来定义 不能用new来创建
abstract class Animal {
eat(food: string) {
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('enenen')
d.run(100)
20 泛型 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)
const res = createArray<string>(3,'123')
21 类型声明
作用就是为了兼容普通的模块
描述为应用的某个变量或者方法在定义的时候并没有明确类型 ,所以可以用declare 来重新强调声明一下。
以引入模块为例:
import { camelCase } from 'lodash' //.d.ts文件就是TypeScript的类型声明的文件
declare function camelCase(input: string): string
const res = camelCase('hello world')