JavaScript 深度剖析 - 类型系统 + TypeScript

文章目录

内容概述

  • 主要探讨 JavaScript 自有类型系统的问题以及如何借助一些优秀的技术方案解决这些问题
  • TypeScript 是在这个过程中,涉及到的语言

一、类型系统

在区分不同编程语言时,经常提到一些名词

  • 强类型与弱类型(类型安全)
  • 静态类型与动态类型(类型检查)

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

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

这种强弱类型之分根本不是某一个权威机构的定义,但是公认的理解为:强类型有更强的类型约束,而弱类型中几乎没有什么约束


  • 强类型是从语言的语法层面就限制了不允许传入不同类型的值
  • 如果传入的是不同类型的值,在编译阶段就会报出错误,而不是等到运行阶段再通过逻辑判断去限制
  • 在 JavaScript 当中所有报出的类型错误都是在代码层面运行时通过逻辑判断手动抛出的

node.js 源码中实现手动抛出错误

在这里插入图片描述

  • 变量类型允许随意改变的特点,并不是强弱类型的区别,就拿 Python 来说,它是一门强类型语言,但它的变量仍然是可以随时改变类型的

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

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

  • 在动态类型语言中的变量没有类型,而变量中存放的值是有类型的
  • JavaScript 就是一门标准的动态类型语言

1.3 常见编程语言在这两个角度下的表现

在这里插入图片描述

二、JavaScript 类型系统特征

  • 弱类型 且 动态类型
  • 本身的类型系统非常薄弱,甚至可以说 JavaScript 根本就没有一个类型系统
  • 灵活且多变
  • 缺乏了类型系统的可靠性
  • 没有编译环节

为什么 JavaScript 不是强类型 / 静态类型

  • 早前 JavaScript 应用简单,在这种情况下,类型系统的限制会使其变得很麻烦

  • 其次 JavaScript 没有编译环节,即便设置成一个静态类型的语言,而静态类型的语言需要在编译阶段做类型检查

  • 在大规模应用下,JavaScript 早期的优势就变成了短板

三、JavaScript 弱类型的问题

  • 程序中的类型异常需要等到运行时才能发现
const obj = {};
obj.foo(); // 写法可行,但运行时会报错

// 可能会留有隐患
// 如果是强类型,会直接在语法上报错,不需要等待运行
setTimeout(() => {
  obj.foo();
}, 1000000);
  • 类型不明确,就可能造成函数功能发生改变
function sum(a, b) {
  return a + b;
}
console.log(sum(100, 100)); // 200
console.log(sum(100, "100")); // 100100
  • 会出现对对象索引器的错误用法
// 在 JavaScript 中,对象的属性名只能是字符串或Symbol
const obj = {};
obj[true] = 100; // 属性名会自动转换为字符串
console.log(obj["true"]); // 100

四、 强类型的优势

  • 错误会更早暴露
  • 代码更智能,编码更准确
// 反例
function render (element) {
  // 开发工具没办法推断 element 是什么类型,不知道 element 里面有什么成员
  // 所以编译器没有提示,需要我们手动输入 className、innerHtml
  element.className = 'container'
  // 这里的 innerHtml 拼写错误,编译器没有任何提示
  element.innerHtml = 'hello world'
}
  • 重构更牢靠
// 反例
const util = {
  // 有多处使用 util 时,不敢轻易修改属性名
  aaa: () => {
    console.log("util func");
  },
};
// 而使用强类型语言,一但对象属性名发生变化,在重新编译时就会报错,可以轻松定位
// 甚至有些工具还可以自动把所以引用该成员的地方做修改
  • 减少在代码层面不必要的类型判断
// 反例
function sum(a, b) {
  if (typeof a !== "number" || typeof b !== "number") {
    throw new TypeError("arguments must be a number");
  }

  return a + b;
}
// 在强类型语言中,这段判断就不需要了,因为不是我们需要的类型就无法进入函数

五、Flow(JavaScript 的静态类型检查器)

5.1 Flow 概述

2014 年由 Facebook 推出的一款工具

  • 可以弥补 JavaScript 弱类型所带来的弊端,为 JavaScript 提供了更完善的类型系统
  • 在 React 和 Vue 中都可以看到 Flow 的使用

工作原理:

在代码中通过添加类型注解的方式去标记代码中的变量以及参数的类型,Flow 根据这些类型注解检查代码中是否存在类型使用异常,从而实现开发阶段对类型异常的检查,避免了直到运行阶段才发现类型使用的错误

// 这种冒号后面跟上类型的用法,叫做类型注解
// 表示前面成员必须接受一个 number 类型的值
function sum(a: number, b: number) {
  return a + b;
}
sum(100,50) // 正常
sum('100',50) // 会检测出异常

  • 对于代码中额外的类型注解,可以在运行之前通过 Babel 或者 Flow 官方提供的模块自动去除,所以在生产环境中这些类型注解不会有任何影响
  • 不需要给每个变量都添加类型注解,完全可以根据自己的需要添加
function sum(a: number, b) {
  // a:number
  // b:any
  return a + b;
}

5.2 Flow 快速上手

注意:Flow 的安装路径不允许出现中文,否则会报错

  1. 初始化 package.json 管理项目依赖
yarn init --yes
  1. 通过 yarn 安装 flow(作为项目的开发依赖安装)
yarn add flow-bin --dev
  1. 使用 Flow 之前通过注释的方式在项目代码开始的位置添加(只有这样 Flow 才会检查该文件)
 // @flow
  1. 关闭编译器自带的语法校验
这里使用 VSCode,打开配置选项,搜索 javascript validate 
  1. 初始化 .flowconfig 配置文件
yarn flow init
  1. 使用 yarn flow 可以自动找到 node_modules 下的 .bin 中找到 flow 命令
    (第一次执行 flow 会启动一个后台服务,去监视文件)
yarn flow
  1. 完成编码工作过后可以通过 yarn flow stop 命令结束服务(
yarn flow stop

5.3 Flow 编译移除注解

类型注解并不是 JavaScript 的标准语法,当我们添加类型注解过后,代码是无法正常运行的,我们可以使用工具在完成编码过后自动移除掉我们所添加的类型注解

  1. 使用官方提供的 flow-remove-types 移除

方式一

yarn add flow-remove-types --dev

方式二(第一个参数为源代码所在目录,第二个参数为输出目录)

yarn flow-remove-types 参数1 -d 参数2 
yarn flow-remove-types . -d dist 

一般情况下,我们会在项目的根目录下新建一个 src 目录,将源代码放入 src 中,编译的结果则输出到 dist 目录,这样就可以避免在转换到过程中误删第三方模块里面的类型注解

  1. 使用 Babel 配合插件移除

步骤1

yarn add @babel/core @babel/cli @babel/preset-flow --dev

步骤2

手动在项目 src 中添加 babel 配置文件 .babelrc 并在文件中添加

{
  "presets": ["@babel/preset-flow"]
}

步骤3

第一个参数为源代码所在目录、第二个参数为输出目录

yarn babel 参数1 -d 参数2
yarn babel src -d dist

5.4 Flow 开发工具插件(Flow Language Support)

在上节中,Flow 检测到的代码中的问题都是输出到控制台当中的,这种体验并不直观,更好的方式是在开发工具中直接显示出类型问题

  • 打开 VSCode 的插件面板搜索安装 Flow Language Support 的插件,这是 Flow 官方所提供的
  • 安装过后,VSCode 的状态栏就会显示 Flow 的工作状态,而且代码中的异常也可以直接标记为红色波浪线
  • 默认情况下只有在修改完代码过后保存才会生效,对于其他编辑器请查看:Flow 官网

5.5 Flow 类型推断

Flow 可自动推断出代码中每个成员的类型

/**
 * 类型推断
 *
 * @flow
 */

function square(n) {
  return n * n;
}
square('100') // 会报错

5.6 Flow 类型注解

添加类型注解可以更明确地限制类型(Flow 类型注解官方文档) ,更推荐使用第三方工具查询

5.6.1 原始类型

/**
 * 原始类型
 *
 * @flow
 */

const a: string = 'foobar'

const b: number = Infinity // NaN // 100

const c: boolean = false // true

const d: null = null

const e: void = undefined

const f: symbol = Symbol()

5.6.2 数组类型

/**
 * 数组类型
 *
 * @flow
 */

const arr1: Array<number> = [1, 2, 3] // 表示全部由数字组成的数组

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

// 元组(表示一个固定长度的数组)
const foo: [string, number] = ['foo', 100]

5.6.3 对象类型

/**
 * 对象类型
 *
 * @flow
 */

const obj1: { foo: string, bar: number } = { foo: 'string', bar: 100 }

// 可以在成员后面添加问号,表示成员可有可无
const obj2: { foo?: string, bar: number } = { bar: 100 }

// 如果需要明确限制键和值的类型,可以通过类似索引器的语法去设置
const obj3: { [string]: string } = {}
obj3.key1 = 'value1'
obj3.key2 = 'value2'

5.6.4 函数类型

/**
 * 函数类型
 *
 * @flow
 */

function square (n: number) { // 标记函数参数
  return n * n
}

function foo (): number { // 标记函数返回值
  return 100 // ok
  // return 'string' // 报错
  // return undefined // 没有返回值相当于返回 undefined 也会报错
}

function bar (): void { // 没有返回值应该标记为void
}

// 回调函数
function foo (callback: (string, number) => void) {
  callback('string', 100)
}
foo(function (str, n) {
  // str => string
  // n => number
})

5.6.5 特殊类型

/**
 * 特殊类型
 *
 * @flow
 */

// 字面量类型(用来限制变量必须是某一个值)
const a: 'foo' = 'foo'
const type: 'success' | 'warning' | 'danger' = 'success'


// 使用 type 关键词声明类型,表示多个类型联合过后的结果
type StringOrNumber = string | number
const b: StringOrNumber = 'string'


// Maybe 类型(有可能)
const gender: ?number = undefined
// 相当于
const gender: number | null | void = undefined

5.6.6 任意类型(Mixed & Any)

/**
 * Mixed Any
 *
 * @flow
 */

// Mixed:string | number | boolean | ....
// 如果没有明确数据类型,是不能使用对应的类型的方法的
function passMixed (value: mixed) {  // 强类型
  // 明确类型
  if (typeof value === 'string') {
    value.substr(1)
  }
  if (typeof value === 'number') { 
    value * value
  }
}
passMixed('string')
passMixed(100)


// Any:string | number | boolean | ....
function passAny (value: any) {  // 弱类型
// 仅表示在语法上不报错,不能保证在运行的时候不报错
  value.substr(1)
  value * value
}
passAny('string')
passAny(100)

5.7 Flow 运行环境API(内置对象)

/**
 * 运行环境 API
 *
 * @flow
 */

// 浏览器所内置的一些API所对应的一些类型限制
const element: HTMLElement | null = document.getElementById('app')

API 声明文件库

六、TypeScript(JavaScript的超集)

6.1 TypeScript 概述

优点:

  • 基于 JavaScript 之上的编程语言
  • 重点解决 JavaScript 语言自有的类型系统的不足
  • TypeScript 大大提高代码的可靠程度
  • 拥有更强大的类型系统
  • 支持 ES6+ 新特性
  • 可编译(低至 ES3,兼容性好)
  • 任何一种 JavaScript 运行环境都支持
  • 相比 Flow,功能更强大,生态也更健全、更完善
  • 目前有很多大型项目已经开始使用 TypeScript 开发(Angular / Vue.js 3.0)

缺点:

  • 语言本身多了很多概念,学习成本增加(但 TypeScript 属于渐进式的,可以按照 JavaScript 特性去写)
  • 项目初期,TypeScript 会增加一些成本(针对小项目)

在这里插入图片描述

6.2 TypeScript 快速上手

  1. 初始化 package.json 管理项目依赖
yarn init --yes
  1. 通过 yarn 安装 TypeScript(作为项目的开发依赖安装)
yarn add typescript --dev  
  1. TypeScript 文件拓展名默认为 .ts
  2. 编译 TypeScript 文件(会将 ts 代码编译成独立的 js 文件)

在编译过程中,TypeScript 会先去检查代码中的类型使用异常,然后移除类型注解等扩展语法,在这个过程中会自动转换 ECMAScript 的新特性

yarn tsc xxx(文件名).ts

6.3 配置文件

tsc 这个命令不仅仅能编译某个文件,还能编译整个工程

  1. 使用 yarn tsc --init 初始化 tsconfig.json 配置文件
yarn tsc --init
  1. 配置文件内 complierOptions 属性就是 TypeScript 编译器所对应的配置选项
target: 设置编译后的 JavaScrpit 采用的 ECMAScript 标准

module: 输出的代码会采用什么样的方式进行模块化

sourceMap: 开启源代码映射

outDir: 设置编译后的输出文件目录

rootDir: 设置源代码文件目录位置

strict: 严格模式
...
  1. 使用 yarn tsc 编译
yarn tsc

6.4 TypeScript 原始类型(Primitive Types)

这里我们先将 tsconfig.json 配置文件中的 target 设置改为 es5,方便我们发现一些特殊情况

const a: string = 'foobar'

const b: number = 100 // 包括 NaN Infinity

const c: boolean = true // 或 false
  • 在非严格模式下,string、number、boolean 都可以为空
  • 可以在配置文件修改 strictNullChecks 去仅对 null 进行限制,或者也可以关闭 strict 严格模式
const d:string = null
  • void 类型(空值)

一般在函数没有返回值的时候标记返回值类型,只能存放 null 和 undefined ,在严格模式下只能存放 undefined

const e: void = undefined
  • null 类型 与 undefined 类型
const f: null = null
const g: undefined = undefined
  • Symbol 类型
const h:symbol = Symbol()

6.5 TypeScript 标准库声明

场景:如果 target 中设置为 es5 ,但代码中使用了 es5 以后的新语法,代码会出现报错,如何解决这个问题

  • 如果要求编译后为 es5 以下,可以直接修改 tsconfig.json 中的 target
  • 如果一定要编译到 es5,还可以通过修改 tsconfig.json 中的 lib 选项指定引用的标准库
 "lib": ["ES2015"],  
  • 注意:lib 中的 “ES2015” 覆盖掉了默认标准库,包括里面 BOM 和 DOM,此时需要将 BOM 和 DOM 添加回来(标准库 BOM 和 DOM 被归类到一个标准库,统称为 DOM 标准库)
"lib": ["ES2015","DOM"], 

6.6 TypeScript 显示中文错误消息

  • 命令窗口中文错误消息:在使用 tsc 命令时,换成如下命令
yarn tsc --local zh-CN
  • VS code 中文错误消息:在配置选项中配置
 打开配置选项,搜索 typescript locale 并将其设置为 zh-CN

6.7 TypeScript 作用域问题

问题:

  • 默认文件中的成员会作为全局成员
  • 多个文件中有相同成员就会出现冲突

解决方案一:

(function () {
  const a = 123
})()

解决方案二:

// 在这个文件中所有的成员就变成了模块作用域中的局部成员了
const a = 123
export {} // 此处是 export 语法,并不是导出一个空对象

6.8 TypeScript Object类型(Object Types)

  • Object 类型泛指所有的非原始类型对象、数组、函数
// object 类型是指除了原始类型以外的其它类型
const foo: object = function () {}; // [] // {}
  • 如果我们需要普通的对象类型,我们需要去使用类似字面量的语法
  • 但是更专业的方式是使用接口(接口后续会单独介绍)
// 对象的属性不能多也不能少
const obj: { foo: number, bar: string } = { foo: 123, bar: 'string' }

6.9 TypeScript 数组类型(Array Types)

  • TypeScript 中定义数组的方式和 Flow 中几乎一致,它也有两种方式
// 第一种(使用Array泛型)
const arr1: Array<number> = [1, 2, 3] // 表示纯数字组成的数组
// 第二种
const arr2: number[] = [1, 2, 3]

案例

// 如果使用 JS,需要判断每个成员是不是数字
function sum(...args){
    return args.reduce((prev,current)=> prev + current,0)
}

// 而使用 TS,类型有保障,不用额外添加类型判断代码
function sum (...args: number[]) {
  return args.reduce((prev, current) => prev + current, 0)
}

6.10 TypeScript 元组类型(Tuple Types)

const tuple: [number, string] = [18, 'zce'] // 元组类型:只能存放对应属性

// const age = tuple[0]
// const name = tuple[1]
const [age, name] = tuple // 使用 ES2015 解构提取
// ES2017 获取对象中所有的键值数组
const entries: [string, number][] = Object.entries({
  foo: 123,
  bar: 456
})

const [key, value] = entries[0]
// key => foo, value => 123

6.11 TypeScript 枚举类型(Enum Types)

枚举的作用:

  • 可以给一组数值取更好理解的名字
  • 一个枚举中只会存在几个固定的值,不会出现超出范围的可能性
  • JavaScript 中并没有枚举这种数据结构,大部分场景我们可以使用对象去模拟
// 用对象模拟枚举
const PostStatus = {
  Draft: 0,
  Unpublished: 1,
  Published: 2
}

const post = {
  title: 'Hello TypeScript',
  content: 'TypeScript is a typed superset of JavaScript.',
  status: PostStatus.Draft 
}

在 TypeScript 有专门的 enum 枚举类型

enum PostStatus {
  Draft = 0, 
  Unpublished = 1,
  Published = 2
}
const post = {
  title: 'Hello TypeScript',
  content: 'TypeScript is a typed superset of JavaScript.',
  status: PostStatus.Draft 
}
  • 如果不指定等号后面的值,默认枚举中的值从 0 累加
enum PostStatus {
  Draft, // 0
  Unpublished, // 1
  Published // 2
}
const post = {
  title: 'Hello TypeScript',
  content: 'TypeScript is a typed superset of JavaScript.',
  status: PostStatus.Draft 
}
  • 如果给枚举第一个成员指定了具体值,后面的值会在第一个值的基础上累加
enum PostStatus {
  Draft = 6, 
  Unpublished, // 7
  Published // 8
}
const post = {
  title: 'Hello TypeScript',
  content: 'TypeScript is a typed superset of JavaScript.',
  status: PostStatus.Draft 
}
  • 枚举的值还可以是字符串(字符串是没办法自增的,所以需要明确每一个枚举成员的值)
enum PostStatus {
  Draft = 'aaa', 
  Unpublished = 'bbb',
  Published = 'ccc'
}
const post = {
  title: 'Hello TypeScript',
  content: 'TypeScript is a typed superset of JavaScript.',
  status: PostStatus.Draft 
}
  • 枚举类型会入侵到运行时的代码(会影响编译后的结果)

在 TypeScript 中使用的大多数类型,经过编译转换后,都会被移除;
而枚举则不会,它最终会编译为一个双向的键值对对象(可以通过键获取值,并通过值获取键),这样的好处是可以动态的根据枚举值获取枚举名称。

编译前

enum PostStatus {
  Draft, 
  Unpublished,
  Published
}
const post = {
  title: 'Hello TypeScript',
  content: 'TypeScript is a typed superset of JavaScript.',
  status: PostStatus.Draft 
}
PostStatus[0] // => Draft

编译后

var PostStatus;
(function (PostStatus) {
    PostStatus[PostStatus["Draft"] = 0] = "Draft";
    PostStatus[PostStatus["Unpublished"] = 1] = "Unpublished";
    PostStatus[PostStatus["Published"] = 2] = "Published";
})(PostStatus || (PostStatus = {}));
var post = {
    title: 'Hello TypeScript',
    content: 'TypeScript is a typed superset of JavaScript.',
    status: PostStatus.Draft
};
PostStatus[0]; // => Draftus: PostStatus.Draft 
  • 如果代码中不会使用索引器的方式去访问枚举,则建议使用常量枚举

编译前

// 常量枚举,不会侵入编译结果
const enum PostStatus {
  Draft,
  Unpublished,
  Published
}

编译后

// 可以看到,所使用的枚举会被移除
// 所使用到枚举的地方,会被替换为具体数值
// 枚举的名称会在后面用注释的方式标注
var post = {
    title: 'Hello TypeScript',
    content: 'TypeScript is a typed superset of JavaScript.',
    status: 0 /* Draft */
};

6.12 TypeScript 函数类型(Function Types)

函数声明

  • 参数个数需要保持一致
  • 如果需要参数可选,可以在参数名称后添加 ’?’ 或者使用 ES6 参数默认值的特性(可选参数或者默认值参数要出现在参数列表最后,因为参数会按照位置传递)
function func1 (a: number, b?: number = 10): string {
  return 'func1'
}

function func1 (a: number, b: number = 10, ...rest: number[]): string {
  return 'func1'
}

func1(100, 200)

func1(100)

func1(100, 200, 300)

函数表达式

  • 接收函数的变量也是有类型的
  • TypeScript 可以根据函数表达式推断出变量的类型
  • 如果把函数作为参数传递(回调函数),必须要约束形参的类型,可以使用类似箭头函数的方式表示参数可接受什么样的函数
const func2: (a: number, b: number) => string = function (a: number, b: number): string {
  return 'func2'
}

6.13 TypeScript 任意类型(Any Types)

  • any 是动态类型,可以接收任意参数
  • TypeScript 不会对 any 类型做类型检查,any 类型是不安全的
  • 不轻易使用,在兼容老代码时,难免会使用
function stringify (value: any) { 
  return JSON.stringify(value)
}
stringify('string')
stringify(100)
stringify(true)

let foo: any = 'string'
foo = 100
// TypeScript 不会对 any 类型做类型检查
foo.bar() //语法上不会报错

6.14 TypeScript 隐式类型推断(Type Inference)

  • 在 TypeScript 当中,如果没有通过类型注解去标记一个变量的类型,TypeScript
    会根据变量的使用情况去推断这个变量的类型,这种特性叫做隐式类型推断
  • 虽然有隐式类型推断,但还是建议给每个变量都添加明确的类型
let age = 18 // age 会被推断为 number 类型

age = 'string' // age已经被推断为 numbe,所以这里会报错
  • 如果 TypeScript 无法推断一个变量具体的类型,这个时候会将其类型标记为 any
let foo // any类型

foo = 100 

foo = 'string'

6.15 TypeScript 类型断言(Type assertions)

  • 在一些特殊情况下,TypeScript 无法推断一个变量的具体类型,而我们作为开发者,我们根据代码的使用情况是可以明确知道代码的类型的
  • 类型断言可以辅助 TypeScript 更加明确代码当中每一个成员的类型
  • 类型断言并不是类型转换(类型转换是代码运行时的概念,类型断言只是编译过程的概念)
// 假定这个 nums 数组是从一个明确的接口获取到的
const nums = [110, 120, 119, 112]

const res = nums.find(i => i > 0) // 返回值一定是一个数字

// 但是对于 TypeScipt 推断出来的类型为 number | undefined
// 此时就无法将返回值直接当作数字使用
// const square = res * res

// 此时我们可以断言 res 为 number 类型
// 1.使用as关键词断言
const num1 = res as number // 推荐使用
// 2.在变量前面使用尖括号断言
const num2 = <number>res // 存在的问题:JSX 下不能使用

6.16 TypeScript 接口(Interfaces)

  • 可以理解为一种规范,或者一种契约
  • 它是一种抽象的概念,可以用来约定对象的结构
  • 使用一个接口,就必须要遵循这个接口全部的约定
  • TypeScript 接口最直观的体现:约定对象当中应该有哪些成员,成员的类型是什么样的
  • 接口只是用来为有结构的数据进行类型约束的,在实际运行阶段并没有意义
interface Post {
 // 可以使用 ',' 分割,但是更标准的语法使用 ';' 分割,也可以省略
  title: string;
  content: string;
}

function printPost (post: Post) {
  console.log(post.title)
  console.log(post.content)
}

printPost({
  title: 'Hello TypeScript',
  content: 'A javascript superset'
})
  • 可选成员、只读成员
interface Post {
 // 可以使用 ',' 分割,但是更标准的语法使用 ';' 分割,也可以省略
  title: string;
  content: string;
  subtitle?: string;  // 可选成员 '?'(可有可无)
  readonly summary: string; // 只读成员 'readonly',在初始化后不能修改
}
  • 动态成员
interface Cache {
  [prop: string]: string
}

const cache: Cache = {}

cache.foo = 'value1' // 都必须是 string 类型键值
cache.bar = 'value2'

6.17 TypeScript 类(Classes)

6.17.1 类的基本使用

  • 描述一类具体事物的抽象特征
  • 类比到程序的角度:描述一类具体对象的抽象成员

  • 在 ES6 以前,JavaScript 通过函数配合原型的模式模拟实现类
  • 从 ES6 开始,JavaScript 中有了专门的 class
  • 在 TypeScript 中,除了可以使用所有 ECMAScript 中所有类的功能,还添加了一些额外的功能和用法
  • TypeScript 增强了 class 的相关语法
class Person {
  // 在 TypeScript 中需要明确在类型中声明所拥有的一些属性
  name: string
  // name: string = 'init name'
  age: number
  // 在 TypeScript 中,类的属性必须有初始值,可以在等号后面赋值或者在构造函数中初始化
  // 两者必须做其一,否则会报错
 
  constructor (name: string, age: number) {
    this.name = name // 如果没有提前声明,直接通过 this 访问当前类的属性会报错
    this.age = age
  }

  sayHi (msg: string): void { // ES6 的语法为类型声明方法
    console.log(`I am ${this.name}, ${msg}`)
  }
}

6.17.2 类的访问修饰符

  • private:私有属性(只能在类的内部访问)
  • public:共有属性(在 TypeScript 中类成员的访问修饰符,默认就是 public)
class Person {
 public name: string //  公有成员(默认,加不加都行)
 private age: number // 私有属性

  constructor (name: string, age: number) {
    this.age = age
    this.name = name 
  }

  sayHi (msg: string): void { 
    console.log(`I am ${this.name}, ${msg}`)
    console.log(this.age);
    
  }
}
const tom = new Person('tom',18)

console.log(tom.age); // 报错
  • protected:受保护的属性(与 private 相似,不同的是 protected 只允许在子类中访问对应成员)
class Person {
 public name: string 
 private age: number 
 protected gender:boolean // 受保护的属性

  constructor (name: string, age: number) {
    this.age = age
    this.name = name 
    this.gender = true
  }

  sayHi (msg: string): void { 
    console.log(`I am ${this.name}, ${msg}`)
    console.log(this.age);
    
  }
}

class Student extends Person{
    constructor(name:string,age:number){
        super(name,age)
        console.log(this.gender); // 正常访问
        
    } }

const tom = new Person('tom',18)

console.log(tom.gender); // 访问不到
  • 构造函数 constructor 的默认修饰符也是 public,如果设置为 private,就不能在外部被实例化,也不能被继承,但可以通过调用静态方法实现
class Person {
 public name: string 
 private age: number 
 protected gender:boolean 

  constructor (name: string, age: number) {
    this.age = age
    this.name = name 
    this.gender = true
  }

  sayHi (msg: string): void { 
    console.log(`I am ${this.name}, ${msg}`)
    console.log(this.age);
    
  }
}

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)

const jack = Student.create('jack',18)

6.17.3 类的只读属性(readonly)

  • 如果属性已经存在访问修饰符,只读属性跟在修饰符之后
  • 对于只读属性,可以选择类型声明时直接通过等号初始化或者在构造函数中初始化,二选一
  • 初始化过后不允许被修改(无论内部还是外部)
class Person {
 public name: string 
 private readonly age: number // 只读
 protected gender:boolean 

  constructor (name: string, age: number) {
    this.age = age
    this.name = name 
    this.gender = true
  }

  sayHi (msg: string): void { 
    console.log(`I am ${this.name}, ${msg}`)
    console.log(this.age);
    
  }
}
const tom = new Person('tom',18)

tom.gender = false // 这里会报错

6.17.4 类与接口

  • 不同的类与类之间可能有共同的特征,对于公共特征一般使用接口去抽象
  • C## 以及 Java 建议接口的定义尽可能简单和细化(一个接口只去约束一个)
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}`)
  }
}

6.18 TypeScript 抽象类(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('something')
d.run(100)

6.19 TypeScript 泛型(Generics)

  • 指在定义函数、接口或者类时,没有去指定具体的类型,等到使用时候再去指定的特征
  • 以函数中为例,泛型就是在声明函数时,不去制定具体类型,等到调用的时候再去传递一个具体类型,其目的是为了极大程度的复用代码
function createNumberArray (length: number, value: number): number[] {
  // Array 对象默认创建的是 any 类型,使用泛型参数传递类型,这里的 Array 就是一个泛型类
  // 在 TypeScript 内部去定义 Array 类型的时候并不知道我们使用其存放什么样的数据
  // 使用泛型参数,让我们在调用时再去传递一个具体的类型
  const arr = Array<number>(length).fill(value) // ES6 fill()填充
  return arr
}

const res = createNumberArray(3, 100)
// res => [100, 100, 100]
  • 使用泛型把类型变成参数,在我们调用的时候再去传递这个类型
  • 泛型就是把我们定义时不能确定的类型变成一个参数,让我们去使用时再去传递类型参数
function createArray<T> (length: number, value: T): T[] {
  const arr = Array<T>(length).fill(value)
  return arr
}
const res = createArray<string>(3, 'foo')

6.20 TypeScript 类型声明(Type Declaration)

  • 在实际开发中需要使用到第三方 npm 模块,而这些模块不一定是通过 TypeScript 编写的,所以它提供的成员就不会有强类型体验

lodash

// (一)把字符串转化为驼峰格式 参数和返回值为string
import { camelCase } from 'lodash'

// (三)此时需要单独的类型声明
declare function camelCase (input: string): string

// (二)当我们直接去调用函数的时候发现并没有看到任何类型提示
const res = camelCase('hello typed')

  • 目前绝大多数 npm 模块都已经提供了对应的声明,只需要安装对应的声明模块即可
  • 类型声明模块是一个开发模块,它里面不会提供任何的具体的代码,只是对一个模块做对应的类型声明
  • 目前越来越多的模块已经在内部集成了这种类型声明文件
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

每天内卷一点点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值