Comfortably Numb

反方向的钟

0%

高阶类型及其他

更加通用的 map

  1. TS 目前支持函数重载

Optional<T>见前文编程与类型系统 - 编程部分:第三章组合-使用类型表达多选一

'Optional map() & 和类型 map()'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  function map<T, U>(optional: Optional<T>, func: (value: T) => U):Optional<U>;
function map<T, U>(value: T | undefined, func: (value: T) => U): U | undefined;
function map<T, U>(a: Optional<T> | T | undefined, func: (value: T) => U) {
if (!a) return undefined
if ('getValue' in a) {
if (a.hasValue()) {
return new Optional<U>(func(a.getValue()))
} else {
return new Optional<U>()
}
} else {
return func(a)
}
}

在一个泛型类型上映射函数,map提供了另外一种方式来将存储数据的类型与操作该数据的函数解耦

'将Box的值取出,应用函数,然后放回一个Box'
1
2
3
4
5
6
7
8
9
10
11
12
class Box<T> {
value: T;
constructor(value: T) {
this.value = value
}
}
function map<T, U>(
box: Box<T>,
func: (value: T) => U
): Box<U> {
return new Box<U>(func(box.value))
}

处理结果或传播错误

使用process传播错误
1
2
3
4
5
6
7
// process不对undefine做任何有用的处理
// process有逻辑处理分支
function process(): string | undefined {
let value: number | undefined = readNumber()
if (!value) return undefined
return stringify(square(value))
}
使用 map 处理错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 和类型map
function map<T, U> (
value: T | undefined,
func: (value: T) => U
): U | undefined {
if (!value) return undefined
return func(value)
}
// process 实现没有分支,将错误传播的工作委托给map
function process(): string | undefined {
let value: number | undefined = readNumber()
// let squareValue: number | undefined = map(value, square)
// return map(squareValue, stringify)
// 直接使用lambda处理
return map(value, (value: number) => stringify(square(value)))
}

混搭函数的应用- map的另外一种应用

函子(functor): 执行映射操作函数的推广。对于任何泛型类型,以 Box<T> 为例,如果 map() 操作接受一个 Box<T> 和一个从 T 到 U 的函数作为实参,并得到一个 Box<U>,那么该 map() 就是一个函子

函数的函子:给定一个有任意数量的实参且返回类型 T 的值的一个函数,映射一个函数,使其接受 T 宗纬实参返回一个 U,从而使得到的函数接受与原函数相同的输入,但是返回类型 U 的一个值。

函数map和在一个函数上应用map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function map<T, U>(
f: (arg1: T, arg2: T) => T,
func: (value: T) => U
): (arg1: T, arg2: T) => U{
return (arg1: T, arg2: T) => func(f(arg1, arg2))
}
// 应用map
function add(x: number, y: number) {
return x + y
}
function stringify(value: number): string {
return value.toString()
}
const result: string = map(add, stringify)(40, 2)

习题

有一个接口 IReader<T>,定义了一个单独的方法 read(): T,实现一个函子:在 IReader<T> 上映射函数 (value: T) => U返回一个 IReader<T>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface IReader<T> {
read(): T
}
class MappedReader<T, U> implements IReader<T> {
reader: IReader<T>;
func: (value: T) => U;
constructor(reader: IReader<T>, func: (value: T) => U) {
this.reader = reader;
this.func = func
}
read(): U {
return this.func(this.reader.read())
}
}
function map<T, U>(
f: IReader<T>,
func: (value: T) => U
): IReader<U>{
return new MappedReader(reader, func)
}

单子

Either 函数见 编程与类型系统 - 编程部分:第三章组合-复合类型-自制Either

对于一个数据处理管道在链接了多个处理函数的情况来看:一旦出现错误情况的处理函子只会在管道中传播最初的错误,而不会进行处理。如果管道中的每个步骤都可能失败,那么函子就无法工作。由于函子规定前一个实参的返回值的类型是后一个处理函数的实参类型,一旦管道中的某个环节出错,返回的是一个错误类型,函子的第2个参数类型将出现不兼容的情况。

改造map函数,使其从 T 得到 Either<Error, U>,如下:

'Either bind()'
1
2
3
4
5
6
7
8
function bind<TLeft, TRight, URight> (
value: Either<TLeft, TRight>,
func: (value: TRight) => Either<TLeft, URight> // func的返回类型与map中的类型不同
): Either<TLeft, URight> {
if (value.isLeft()) return Either.makeLeft(value.getLeft())
// 简单返回对其应用 func 结果,而不是 Either.makeRight<func(value.getRight())>,只要保证 func 返回类型是 Either 即可
return func(value.getRight())
}

使用 bind() 实现 无分支的 readCatFromFile()

'无分支的 readCatFromFile()'
1
2
3
4
5
6
7
8
declare function openFile(path: string): Either<Error, FileHandle>;
declare function readFile(handle: FileHandle): Either<Error, string>;
declare function deserializeCat(serializeCat: string): Either<Error, Cat>;
function readCatFromFile(path: string): Either<Error, Cat> {
let handle: Either<Error, FileHandle> = openFile(path)
let content: Either<Error, string> = Either.bind(handle, readFile)
return Either.bind(content, deserializeCat)
}

map() 与 bind() 的区别

Box<T>为例,关注在泛型上下文中,map() 和 bind() 如何处理 T类型, U类型(Box<T> & Box<U>, T[] & U[], Optional<T> & Optional<U>, Either<T> & Either<U>

对于 Box<T>,函子(map())接受一个Box<T>和一个从 TU 的函数,返回一个 Box<U>;在一些场景中函数直接从 T 得到 Box<U>bind()接受一个 Box<T> 和一个从 TBox<U>的函数;

区别就是第二个参数 func 的参数到返回

单子模式

单子由 bind()和另一个更加简单的函数组成。另外的这个函数接受一个类型T,并将其封装到泛型类型中,如 BoxOptionalEitherT[]。该函数通常叫做 return()unit()`

单子模式:单子是一个泛型类型 H<T>。对于该类型,有一个 unit() 可接受类型 T 的一个值并返回类型 H<T> 的一个值。还有一个函数 bind() 可接受H<T> 的一个值和一个从 TH<U> 的函数,并返回类型 H<U> 的一个值。

continuation 单子

看以下代码:then()只是 Promise<T>bind() 的一种称呼;而 Promise.resolve()Promise<T>unit() 的一种称呼

链接promise
1
2
3
declare function getLocation(): Promise<Location>
declare function hailRideshare(location: Location): Promise<Car>;
let car: Promise<Car> = getLocation().then(hailRideshare)

列表单子

除数的例子

除数&所有除数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 除数
function divisors(n: number): number[] {
let result: number[] = []
for(let i = 2; i< n/2;i++) {
if (n % i == 0) {
result.push(i)
}
}
return result
}
// 所有除数
function allDivisors(ns: number[]): number[] {
let result: number[] = []
for(const n of ns) {
result = result.concat(divisors(n))
}
return result
}
所有变位词
1
2
3
4
5
6
7
8
declare function angram(input: string): string[]
function allAngrams(inputs: string[]: string[]) {
let result = string[] = []
for(const input of inputs) {
result = result.concat(angram(input))
}
return result
}

allDivisors()allAngrams()替换为一个泛型函数。该函数将接受一个 T数组 和从TU 数组的一个函数,并返回一个U 数组

'列表bind()'
1
2
3
4
5
6
7
8
9
10
11
12
13
function bind<T, U>(inputs: T[], func: (value: T) => U[]): U[] {
let result: U[] = []
for (const input of inputs) {
result = result.concat(func(input))
}
return result
}
function allDivisors(ns: number[]): number[] {
return bind(ns, divisors)
}
function allAngrams(inputs: string[]): number[] {
return bind(inputs, angram)
}

其他单子

  1. 状态单子:封装一个状态,并把该状态与值一起传递。在给定当前状态时,它将生成一个值和一个更新后的状态。使用 bind() 将这些函数链接起来在管道中传播和更新状态,而不需要显示的在一个变量中存储状态,从而能够使用纯粹的函数代码来处理和更新状态。

  2. IO单子:封装了副作用。允许实现能够读取用户输入或写入文件或终端的纯粹函数,也因为不纯粹的行为从函数中移除了出来封装到了 IO单子 中。

习题

'Lazy(): T'
1
2
3
4
5
6
7
8
9
10
type Lazy<T> = () => T;
function lazyUnit(val: T): Lazy<T> {
return () => val
}
function lazyMap<T, U>(f: Lazy<T>, func: (val: T) => U): Lazy<U> {
return () => func(f())
}
function lazyBind<T, U>(f: Lazy<T>, func: (val: T) => Lazy<U>): Lazy<U> {
return func(f())
}

继续学习

补充:范畴论部分内容

对《编程与类型系统》部分章节内容的补充,不代表本人对本书有深刻理解

第五章-Products and Coproducts

第六章-simply ADT(AlgeBraic Data Type) 简单代数数据类型

Product Type 乘积

Records 记录

Sum Type 和类型

Algebra of types

第九章-Function Types

Currying柯里化

第十三章-Free Monoids 自由单子

Learn you a Haskell 的代码对比补充

函子

单子

Speaking

1000 subjects

  1. tell a story about yourself

read a text out loud

Accent

Listening

Vocabulary

Conversation

第七章 - Subtyping 儿类型关系

类型之间的关系-儿类型关系:1. 在TS中消除类型的歧义;2.安全的反序列化;3.错误情况的值;4.和类型、集合以及函数的类型兼容。

在TS中区分相似的类型

回顾磅力秒那个例子,使用了 unique symbol 来区分两个相似类型,确保类型检查器不会把一个类型的值解释为另一个类型的值 - 通过模拟名义Subtype(亚类型、儿类型)👉见下文

省略NsType和LbsType的unique symbol唯一属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
declare const NsType: unique symbol;
class Ns {
value: number;
// [NsType]: void;
constructor(value: number) {
this.value = value
}
}
declare const LbfsType: unique symbol;
class Lbfs {
value: number;
// [LbfsType]: void;
constructor(value: number) {
this.value = value
}
}
// 将2个 NsType & LbsType 省略,将2个对象互传,编辑器不会报错
// 演示过程
function acceptNs(momentum: Ns) {
console.log(`Momentum: ${Momentum.value}Ns`)
}
acceptNs(new Lbfs(10)) // 实际调用传错参数,不会报错,控制台打印:Momentum: 10

Subtyping:如果在期望类型 T 的实例的任何地方,都可以安全地使用类型 S 的实例,那么称类型 S 是类型 T 的Subtype(亚类型、儿类型)。- 里氏(Liskov substitution principle)替换原则中的一种非正式定义

Subtyping 的构成

  1. Normal Subtyping: 显示声明一个类型是另一个类型的Subtype(亚类型、儿类型),则二者构成Subtyping 关系
  2. Structural Subtyping: 如果一个类型具有另一个类型的所有成员,并且可能还有其他成员,那么前者是后者的Subtype(亚类型、儿类型) - TS使用Structural Subtyping

Structural Subtyping 和 Normal Subtyping 的优缺点

  1. Normal Subtyping: 在类型间建立关系,即使类型是外部类型,不在控制范围内;
  2. Structural Subtyping: 默认结构相似不会混淆参数类型

在 TS 中模拟 Normal Subtyping

在TS中,unique symbol 生成了一个在所有代码中保持唯一的名称。用户声明的名称绝对不会匹配生成的名称,使用该名称创建一个属性,需要给这个属性定义一个类型,但是我们不关心它的实际值,只是为了区分类型,所以选择了最适合的单元类型void。所以当要创建 NSLbfs的Subtype(亚类型、儿类型),只能显示地继承。

Subtype(亚类型、儿类型)的极端情况

  1. 把任何类型赋值给它的类型(Assigning anything to):any, unknown
  2. 给任何东西赋值类型(assigning to anything):any- 从而绕过类型检查
反序列化any
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// JSON.parse()反序列化返回类型是 any
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
// 只是封装了 JSON.parse() 并返回了 any 类型的一个值
function deserialize(input: string): any {
return JSON.parse(input)
}
function greet(user: User): void {
console.log(`hi ${user.name}`)
}
// 反序列化一个有效的 User JSON
greet(deserialize('{"name": "Alice"}'))
// 也可以反序列化一个不是 User 对象的对象
greet('{}') // 输出hi undefined,因为 `any`会绕过类型检查

User的运行时检查 - isUser()

增加一个属性类型检查函数可以规避类型问题,但是仍然存在不足:没有强制调用 isUser,所以依然会存在忘记调用而出现的 undefined 的情况

增加isUser()函数调用前检查类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
function deserialize(input: string): any {
return JSON.parse(input)
}
function greet(user: User): void {
console.log(`hi ${user.name}`)
}
// 检查给定实参是否是 User 类型,我们认为,具有 string 类型的 name 属性的变量是 User 类型
function isUser(user: any): user isUser {
if (!user) {
return false
}
return typeof user.name === 'string'
}
let user: any = deserialize('{"name": 'Alice'}')
// 在使用之前,先检查 user 是否具有 string类型的name属性
if (isUser(user)) {
greet(user)
}
user = undefined
// 不会执行
if (isUser(user)) {
greet(user)
}

顶层类型(Top Type) - unknown

如果能够把任何值赋给一个类型,则称为顶层类型,和类型Object | null | undefined === unknown

一旦忘记调用 isUser,代码在编译过程就会报错 Argument of type 'unknown' is not assignable to parameter of type 'User'

unknown 和 any 的区别:unknown 需要存在可转换的类型,而 any 不需要直接可用。

使用 unknown 的强类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
// 返回 unknown
function deserialize(input: string): unknown {
return JSON.parse(input)
}
function greet(user: User): void {
console.log(`hi ${user.name}`)
}
// 检查给定实参是否是 User 类型,我们认为,具有 string 类型的 name 属性的变量是 User 类型
// 实参保持为 any
function isUser(user: any): user isUser {
if (!user) {
return false
}
return typeof user.name === 'string'
}
// 将变量声明为 unknown
// 这里一旦有值就会有类型从 any 转换为 unknown,向上转化类型是安全的
let user: unknown = deserialize('{"name": 'Alice'}')
// 在使用之前,先检查 user 是否具有 string类型的name属性
if (isUser(user)) {
greet(user)
}
user = deserialize('null')
// 不会执行
if (isUser(user)) {
greet(user)
}

错误情况的值

一个相反的问题:一种类型可以替代其他类型使用。

TurnDirection到角度的转换
1
2
3
4
5
6
7
8
9
10
11
enum TurnDirection {
Left,
Right
}
funtion turnAngle(turn: Turndirection): number {
switch(turn) {
case TurnDirection.Left: return -90;
case TurnDirection.Right: return 90;
default: throw new Error('Unknown TurnDirection')
}
}
报告错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
funtion fail(message: string): never {
console.error(message);
throw new Error(message)
}
funtion turnAngle(turn: Turndirection): number {
switch(turn) {
case TurnDirection.Left: return -90;
case TurnDirection.Right: return 90;
default: fail('Unknown TurnDirection')
}
}
// 这段代码可以工作,但是还差一点
// 在严格模式下(使用了 --strict)编辑器会报错
// Function lacks ending return statement and return tyoe does not include "undefined"
// 因为编辑器在 `default` 分支中没有找到 return 语句,所以标记错误
使用fail()并返回一个虚拟的值
1
2
3
4
5
6
7
8
9
10
funtion turnAngle(turn: Turndirection): number {
switch(turn) {
case TurnDirection.Left: return -90;
case TurnDirection.Right: return 90;
default: {
fail('Unknown TurnDirection')
return -1; // 因为 fail 会抛出错误,所以永远不会返回这个虚拟值
}
}
}

如果后续改变了 fail 的返回,虚拟值就会直接被抛出,不太好,后面还会面临修改,需要另一种解决方案

使用fail()并返回其结果的turnAngle()
1
2
3
4
5
6
7
8
9
10
// 即使返回never也没问题:因为 never 是所有类型的Subtype(亚类型、儿类型)
funtion turnAngle(turn: Turndirection): number {
switch(turn) {
case TurnDirection.Left: return -90;
case TurnDirection.Right: return 90;
default: {
return fail('Unknown TurnDirection');
}
}
}

如果后续需求更新了fail,使其在某些情况不再抛出错误,编译器将强制我们修改fail的返回类型,如果返回类型是void,这时试图将返回的值传递给 string 类型,就不会通过类型检查。

如果一个类型是其他类型的 Subtype(亚类型、儿类型),则为底层类型,底层类型是所有类型的 Subtype(亚类型、儿类型) 则需要具有其他类型的成员。因为其他类型可以有无限个类型和成员,所以底层类型也必须有无限个成员,然而这是不可能的。所以底层类型始终是一个空类型:不能为其创建实际值

允许的替换

Subtyping 与和类型(Sum Type)

Trangle | Square 作为 Trangle | Square | Circle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
declare const TriangleType: unique symbol;
class Trangle {
[TriangleType]: void;
// Trangle members
}
declare const SquareType: unique symbol;
class Square {
[SquareType]: void;
// Square members
}
declare const CircleType: unique symbol;
class Circle {
[CircleType]: void;
// Circle members
}
// 这里忽略函数的具体实现
declare function makeShape(): Trangle | Square;
declare function draw(shape: Trangle | Square | Circle): void;
// draw 接受 Trangle | Square | Circle 这3种类型之一
// 可以运行,makeShape提供的类型少了一个但是是draw可以接受的范围内
draw(makeShape())
Trangle | Square | Circle 作为 Trangle | Square
1
2
3
4
// 这里忽略函数的具体实现
declare function makeShape(): Trangle | Square | Circle;
declare function draw(shape: Trangle | Square):void;
draw(makeShape()) // 无法通过编译,draw 处理不了 Circle

当使用继承时,得到的 Subtype(亚类型、儿类型)Parent type(双亲类型) 的属性更多。对于Sum Type,则是相反的,Parent type(双亲类型)Subtype(亚类型、儿类型) 的类型更多。

Sum Type: Trangle | SquareTrangle | Square | Circle 的 Subtype(亚类型、儿类型)

Subtyping and collections(集合)

Triangle[]作为Shape[]
1
2
3
4
5
6
7
8
9
10
class Shape {/* Shape members */}
declare const TriangleType: unique symbol;
// Triangle 是 Shape 的 Subtype(亚类型、儿类型)
class Triangle extends Shape {
[TriangleType]: void;
// Triangle members
}
declare function makeTriangles(): Triangle[];
declare function draw(shapes: Shape[]): void;
draw(makeTriangles())

结论:数组会保留它们存储的底层类型的Subtyping关系,反过来则不成立。

Triangle 作为 ShapeSubtype(亚类型、儿类型),则 Triangle[]Shape[]Subtype(亚类型、儿类型),如果 Triangle 可以用作 Shape,那么 Triangle[] 也可以用作 Shape[]

LinkedList<Triangle>作为LinkedList<Shape>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 一个泛型链表集合
class LinkedList<T> {
value: T;
next: LinkedList<T> | undefined = undefined;
constructor(value: T) {
this.value = value
}
append(value: T): linkedList<T> {
this.next = new LinkedList(value);
return this.next
}
}
declare function makeTriangles(): linkedList<Triangle>;
declare function draw(shapes: linkedList<Shape>): void;
draw(makeTriangles()); // 代码能够编译

协变(Covariance):如果一个类型保留其底层类型的 subtyping 关系,就成该类型具有协变性。数组具有协变性。因为它保留了 subtyping 关系: TriangleShapeSubtype(亚类型、儿类型),所以 Triangle[]Shape[]Subtype(亚类型、儿类型)

Subtyping 和函数的返回类型

() => Triangle 作为 () => Shape
1
2
3
4
5
6
7
8
9
declare function makeTriangle(): Triangle;
declare function makeShape(): Shape();
function useFactory(factory: () => Shape): Shape {
return factory()
}
let shape1: Shape = useFactory(makeShape);
let shape2: Shape = useFactory(makeTriangle);
// Shape <---- Triangle // subtying 关系
// () => Shape <---- () => Triangle // subtying 关系

函数的返回类型具有协变性:如果 TriangleShape 的 Subtype(亚类型、儿类型),则可以使用返回 Triangle 替代返回 Shape的函数。反过来则不成立。

() => Shape 作为 () => Triangle
1
2
3
4
5
6
7
8
declare function makeTriangle(): Triangle;
declare function makeShape(): Shape();
function useFactory(factory: () => Triangle): Triangle {
return factory()
}
// 代码编译错误❌
let shape1: Shape = useFactory(makeShape);
let shape2: Shape = useFactory(makeTriangle);

Subtyping 和函数的实参类型

带参数的 (argument: Shape) => void(argument: Triangle) => void 的关系是什么?

引入一个render()函数
1
2
3
4
5
6
7
declare function drawShape(shape: Shape): void;
declare function drawTriangle(triangle: Triangle): void;
function render(
triangle: Triangle,
drawFunc: ((argument: Triangle) => void); void,
drawFunc(triangle);
)

逆变:如果一个类型颠倒了其底层类型的 subtying 关系,则称该类型具有逆变性。大部分语言中函数的实参是逆变的。

一个接受 Triangle 作为实参的函数, Shape <---- Triangle 箭头指向代表 subtying 关系, (Shape) => void ----> (Triangle) => void 箭头指向代表subtying 关系
可以被替换成接口 Shape 作为实参的函数,因为总是可以把 Triangle 传递给一个接受 Shape 作为实参的函数。函数之间的关系与其实参类型之间的关系相反。

TS是例外,因为TS反过来也成立:传入期望接受 Subtype(亚类型、儿类型) 的函数,而不是期望接受 Parent type(双亲类型),这是故意作出的设计决策,目的是方便实现常见的 JS 编程模式,不过,可能会导致运行时问题

Shape和包含isRightAbgled()方法的Triangle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Shape {
// Shape members
}
declare const TriangleType: unique symbol;
class Triangle extends Shape {
[TriangleType]: void;
// 判断给定的实例是否描述了一个直角三角形
isRightAbgled(): boolean {
let result: boolean = false;
// determine whether it is a right-angled triangle
return result
}
// more triangle members
}
反转绘制示例:更新后的绘制和渲染函数
1
2
3
4
5
6
7
8
9
declare function drawShape(shape: Shape): void;
declare funtion drawTriangle(triangle: Triangle): void;
// render 期望收到 Shape 和一个接受 Shape 作为实参的函数
function render(
shape: Shape,
drawFunc: (argument: Shape) => void
): void {
drawFunc(shape)
}
试图对Triangle的双亲类型调用isRightAbgled
1
2
3
4
5
6
7
8
9
10
11
12
13
14
funtion drawTriangle(triangle: Triangle): void {
console.log(triangle.isRightAbgled())
}
function render(
shape: Shape,
drawFunc: (argument: Shape) => void
): void {
drawFunc(shape)
}
// 使用 Shape 和 drawTriangle 来绘制
// drawTriangle 的实参变成了 Shape,将对 Shape 调用 isRightAbgled()
// 运行时isRightAbgled肯定会报错❌
// 这是实现TS有意作出的决策
render(new Shape(), drawTriangle)

在 TS 中,如果 Triangle 是 Shape 的 subtype,那么函数类型 (argument: Shape) => void and (argument: Triangle) => void 能够彼此替换,互为subtype,这种属性成为双变性(双向协变)。

第八章 - 面向对象变成的元素-应用subtype

OOP: Object-Oriebted Programming, 面向对象编程。对象可以包含数据和代码,数据是对象的状态,代码是一个或多个方法,也叫「消息」。在面向对象系统中,通过使用其他对象的方法,对象之间可以“对话”或者发送消息。

OOP关键特征:

  1. 封装:允许隐藏数据和方法
  2. 继承:使用额外的数据和代码扩展一个类型

使用接口定义契约

接口或契约: 描述了实现该接口的任何对象都理解的一组消息。消息是方法,包括名称,实参和返回类型。接口没有任何状态,相当于书面协议,规定了实现中将提供什么。

抽象类和接口的区别是什么?

  1. 抽象类 ConsoleLoggerALogger 之间的关系是所谓的 关系,即 ConsoleLogger 继承了 ALogger, 所以它是一个ALogger
  2. ILogger 定义了一个契约,没有继承,没有创建 关系,接口可以扩展也可以合并。接口最终让消费者受益,而不是实现接口的类受益。针对接口编程可以降低系统中组件的耦合。
使用抽象类实现日志系统
1
2
3
4
5
6
7
8
9
10
11
// ALogger 是一个抽象类
abstract class ALogger {
// log 是一个抽象方法,没有实现
abstract log(line: string): void;
}
// 继承了抽象类,实现了 log方法
class ConsoleLogger extends ALogger {
log(line: string): void {
console.log(line)
}
}
使用接口实现日志系统-这种场景中推荐使用接口
1
2
3
4
5
6
7
8
interface ILogger {
log(line: sring): void;
}
class ConsoleLogger implements ILogger {
log(line: string): void {
console.log(line)
}
}
扩展和合并接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 扩展
interface ILogger {
log(line: sring): void;
}
interface IExtendedLogger extends ILogger {
warn(line: sring)L void;
error(line: sring)L void;
}
// 合并
interface ISpeaker {
playSound(): void;
}
interface IVolumeControl {
up(): void;
down(): void;
}
interface ISpeakerWithIVolumeControl extends ISpeaker, IVolumeControl {
// 合并为一个
}

练习

实现一个 IterableIterator<T>接口`

1
2
3
4
5
6
7
8
9
interface Iterable<T> {
[Symbol.iterator](): Iterator<T>
}
interface Iterator<T> {
next(): IteratorResult<T>
}
interface IterableIterator<T> extends Iterable, Iterator<T> {

}

继承数据和行为

「是一个」经验准则

不好的继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x
this.y = y
}
}
class Circle extends Point {
radius: number;
constructor(x: number, y: number, radius: number) {
super(x, y)
this.radius = radius
}
}

继承和 是一个 关系: 继承会在 儿类型和双亲类型之间建立一个 是一个,如果基类是 Shape,派生类是 Circle,关系就是 Circle是一个Shape,上面 Circle 明显不是一个 Point。

建模层次

一种场景:当数据模型包含不同层次是,应该考虑继承。

参数化表达式的行为

另一种场景:大部分行为和状态是多个类型共有的,但是又一小部分行为或状态需要随不同实现而有变化。这里的多个类型应该通过是一个测试

注意⚠️:不要创建出非常深的类层次,否则一个对象的多个状态和方法可能来自层次结构中的不同级别,导致代码难以理解。通常让 subclass 是具体类,让层次结构上方的双亲类是抽象类比较好。

表达式层次结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// IExpression 不需要是类,因为它不保存状态
interface IExpression {
eval(): number;
}
abstract class BinaryExpression implements {
readonly a: number;
readonly b: number;
constructor(a: number, b: number) {
this.a = a;
this.b = b
}
// 抽象方法
abstract eval(): number
}
class SumExpression extends BinaryExpression {
eval(): number {
return this.a + this.b
}
}
class MyExpression extends BinaryExpression {
eval(): number {
return this.a * this.b
}
}

abstract class UnaryExpression implements {
readonly a: number;
constructor(a: number) {
this.a = a;
}
// 抽象方法
abstract eval(): number
}
class UnaryMinusExpression extends UnaryExpression {
eval(): number {
return -this.a
}
}

组合数据和行为

重写或扩展行为,比继承更好 -> 组合

只要可能就优先选择组合而不是继承

继承和组合-使用一个Shape
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Shape {
id: string;
constructor(id: string) {
this.id = id
}
}
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x
this.y = y
}
}
class Circle extends Shape {
center: Point;
radius: number;
constructor(id: string, center: Point, radius: number) {
super(id)
this.center = center
this.radius = radius
}
}

「有一个」经验准则

组合和 有一个 关系: 组合在容器类型和被包含类型之间建立了一个 有一个 的关系。如果类型是 Circle, 被包含的类型是 Point,那么他们的关系是 Circle有一个 Point

复合类

可以把复合类的状态的部分声明为私有成员,封装起来,然后使用额外的方法来增强这个复合类,让这些方法来访问实现中的私有成员。

向CEO提出问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CEO {
isBusy(): boolean {}
answer(question: string): string {}
}
class Department {}
class Budget {}
class Company {
private ceo: CEO = new CEO();
private departments: Department[] = []
private budget: Budget = new Budget()
askCEO(question: string): string | undefined {
if (!this.ceo.isBusy()) {
return this.ceo.answer(question)
}
}
}

实现适配器模式

适配器模式能够兼容2个不同的类,且不需要修改其中任何一个类。不把几个组件合并到一起,而是封装一个组件,并提供它需要的胶水代码,使其能够作为另一种类型使用。

一个外部的几何库,但是不适合我们的对象模型
1
2
3
4
5
6
7
8
namespace GeometryLibrary {
export interface ICircle {
getCenterX(): number;
getCenterY(): number;
getDiameter(): number; // 获取直径
}
// operations on ICircle omitted
}
CircleAdapter适配器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 实现了库期望的ICircle接口
class CircleAdapter implements GeometryLibrary.ICircle {
private circle: Circle;
constructor(circle: Circle) {
this.circle = circle
}
getCenterX(): number {
return this.circle.center.x;
}
getCenterY(): number {
return this.circle.center.y;
}
getDiameter(): number {
return this.circle.center.radius * 2;
}
}

扩展数据和行为

使用组合扩展行为

使用混入扩展行为

混入能够减少样板代码,允许混入不同的行为来构成一个对象,以及在多个类型中重用共有的行为。最呵呵实现横切关注点(cross-cutting concern): 程序中影响其他关注点单数不容易被分解的方面,例如引用计数,缓存,持久化等。

混入:混入通常是使用多重继承实现的(与本章开始的是一个经验准则发生了冲突)

从多重继承的角度看:创建一个 IHunter 类来实现捕猎行为,并让所有捕食性动物派生自这个类,这样 Cat既是Animal又是Hunter,混入与继承并不相同,可以只创建一个HunterBehavaior行为类来实现捕猎行为,并让所有捕食性动物包含这种行为。

混入与包含关系:混入在一个类型与其混入类型之间建立一个包含关系,与 是一个 关系不同

TS 中的混入

使用 extend()泛型函数,让该函数接受2个不同类型的实例,并把第二个实例的所有成员复制到第一个实例中。

& 交叉类型,交集

使用一个实例的成员扩展另一个实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function extend<First, Second>(first: First, second: Second): First & Second {
const result: unknown = {}
// 首先,迭代第一个对象的所有成员,把他们复制到result中
for (const prop in first) {
if (first.hasOwnProperty(prop)) {
(<First>result)[prop] = first[prop]
}
}
// 接下来,对第二个类型的成员做相同的处理
for(const prop in second) {
if (second.hasOwnProperty(prop)) {
(<Second>result)[prop] = second[prop]
}
}
return <First & Second>result
}
混入行为
1
2
3
4
5
6
7
8
9
10
11
12
13
// 不定义 Cat, 而是定义 喵喵行为 的宠物类型
// 豹子,猫 都可以喵喵
// 由于没有扩展捕猎行为,所以还不是 Cat
class MeowingPet extends Pet {
meow(): void {}
}
class HunterBehavior {
track(): void {};
stalk(): void {};
pounce(): void {};
}
type Cat = MeowingPet & HunterBehavior
const fluffy: Cat = extend(new MeowingPet(), new HunterBehavior())

习题

跟踪快递单和包裹的状态
1
2
3
4
5
6
7
8
9
10
11
12
13
class Letter {}
class Package {}
class Tracking {
private status: string;
constructot(status: string) {
this.status = status
}
updateStatus(status) {
this.status = status
}
}
type TrackingLetter = Letter & Tracking
type TrackingTracking = Package & Tracking

纯粹面向对象代码的替代方案

1.和类型

如果需要用相同的方式传递不同类型的对象,或者把他们放到一个公共的集合中,他们并非必须实现相同的接口,或者有一个公共的双亲类型。相反,可以利用和类型,就可以不用在类型之间建立任何关系而获得相同的行为。

2.函数式编程

避免了维护状态,直接计算返回,也不需要改变任何状态,(猜:这里我们可以窥见:既不需要维护状态也不需要改变状态,那么就可以实现 immutable?,所有实参固定成一个引用类型?)

3.泛型编程

将在第九章,第十章深入介绍。

第九章 - 泛型数据结构

要点:分离关注点;为数据布局使用泛型数据结构;遍历任何数据结构;设置数据处理管道;

解耦关注点

getNumbers()
1
2
3
4
5
6
7
8
9
10
11
12
type TransformFunction = (value: number) => number;
function getNumbers(transform: TransformFunction): number[] {

}
// 如果不需要转换,使用一个默认的doNothing
// doNothing 是恒等函数
function doNothing(value: number): number {
return value;
}
function getNumbers(transform: TransformFunction = doNothing): number[] {

}
assembleWidgets()
1
2
3
4
5
6
7
8
9
10
11
12
type PluckFunction = (widgets: Widget[]) => Widget[];
function assembleWidgets(pluck: PluckFunction): AssembleWidget[] {

}
// 如果不需要转换,使用一个默认的 pluckAll
// pluckAll 是恒等函数
function pluckAll(widgets: Widget[]): Widget[] {
return widgets;
}
function assembleWidgets(pluck: PluckFunction = pluckAll): AssembleWidget[] {

}

pluckAlldoNothing 非常相似,只是类型不同:都接受一个实参,不做任何处理就返回该实参,2个都是恒等函数。

代数中定义的恒等函数: f(x) = x

可重用的恒等函数

简单的可重用的恒等函数
1
2
3
4
5
6
7
8
9
10
// 简单实用 any 类型
// 当开始使用的时候,会失去类型安全。
function identity(value: any): any {
return value
}
function square(x: number): number {
return x * x
}
// 运行时失败❌
square(identity('hello'))

类型参数:泛型名称标志符

泛型恒等函数
1
2
3
4
5
6
7
8
9
10
11
12
13
function identity<T>(value: T): T {
return value;
}
// 编译器足够只能,不需要显示指定 T 是什么类型
function getNumnbers(
transform: TransformFunction = identity
): number[] {}

function assembleWidgets(pluck: PluckFunction = identity): AssembleWidget[] {

}
// 编译❌
square(identity('hello'))

可选类型

泛型类 Optional 类型:当处理没有赋值的情况时,使用的逻辑与该值的实际类型没有关系。Optional可以存储其他任何类型。对 Optional 的修改不会影响 T,而对 T 的修改也不会影响 Optional。这是 泛型编程 极为强大的特征。

泛型类型

泛型类型:指参数化一个或多个类型的泛型函数,类、接口等。能让代码的组件化程度更高。

习题

1
2
3
4
5
6
7
8
9
class Box<T> {
readonly value: T;
constructor(value: T) {
this.value = value
}
}
function UnBox<T>(boxed: Box<T>): T {
return boxed.value
}

泛型数据布局

数字二叉树&字符串链表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 数字二叉树
class NumberBinaryTreeNode {
value: number;
left: NumberBinaryTreeNode | undefined;
right: NumberBinaryTreeNode | undefined;
constructor(value: number) {
this.value = value
}
}
// 字符串链表
class StringLinkedListNode {
value: string;
next: StringLinkedListNode | undefined;
constructor(value: string) {
this.value = value
}
}

泛型数据结构

2个例子可以看出 NumberBinaryTreeNode & StringLinkedListNode在数据结构和类型上产生了不必要的耦合。当有新的 StringBinaryTreeNode 时会有冗余代码。

泛型二叉树
1
2
3
4
5
6
7
8
class BinaryTreeNode<T> {
value: T;
left: BinaryTreeNode<T> | undefined;
right: BinaryTreeNode<T> | undefined;
constructor(value: T) {
this.value = value
}
}
泛型链表
1
2
3
4
5
6
7
class LinkedListNode<T> {
value: T;
next: LinkedListNode<T> | undefined;
constructor(value: T) {
this.value = value
}
}

什么是数据结构

包含3个部分:
  1. 数据自身,数据结构包含数据;
  2. 数据的形状:例如二叉树,以分层方式布局数据,每层最多2个 children node。
  3. 一组保留星座的操作:添加或移除元素。
2个关注点:
  1. 数据,包括数据的类型,实例保存的实际值
  2. 数据的形状和保留形状的操作。

习题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 实现栈,后进先出
class Stack<T> {
private value: T[] = [];
public push(v: T): T[] {
this.value.push(v)
}
public pop(): T | undefined {
if (this.value?.length > 0) {
const p = this.value.pop()
return p
}
return undefined;
}
public peek(): T | undefined {
if (this.value?.length > 0) {
return this.value[this.value.length - 1]
}
}
}
// 实现元组
class Pair<T, U> {
readonly first: T;
readonly second: U;
constructor(f: T, s: U) {
this.first = f;
this.second = s;
}
}

遍历数据结构

中序遍历:递归地找到最深处左侧节点,然后依次遍历双亲节点,右侧节点,最左侧child tree遍历结束后访问该 child tree 的双亲节点,然后访问右侧节点

中序打印
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class BinaryTreeNode<T> {
value: T;
left: BinaryTreeNode<T> | undefined;
right: BinaryTreeNode<T> | undefined;
constructor(value: T) {
this.value = value
}
}
function printInOrder<T>(root: BinaryTreeNode<T>): void {
if(roor.left) {
printInOrder(root.left)
}
console.log(root.value)
if (root.right) {
printInOrder(root.right)
}
}
let root: BinaryTreeNode<number> = new BinaryTreeNode(1)
root.left = new BinaryTreeNode(2)
root.left.right = new BinaryTreeNode(3)
root.left.right = new BinaryTreeNode(4)
printInOrder(rrot)
// 2 3 1 4

使用迭代器

把遍历逻辑移动到自己的组件中。

迭代器:能够用来遍历数据结构的一个对象。提供了一个标准结构,将数据结构的实际形状对客户的隐藏起来。

自定义迭代器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 定义迭代器结果
type IteratorResult<T> = {
done: boolean;
value: T;
}
// 迭代器接口
interface Iterator<T> {
next(): IteratorResult<T>
}
// 二叉树迭代器
// 这种实现方式并不是最高效的,因为我们需要队列的元素数量和树的节点数量相同,也就是说,如果树的层次很深,会需要占用较大的内存来存储这个队列
// 后面会有更好的实现,先使用这个作为示例
class BinaryTreeIterator<T> implements Iterator<T> {
private values: T[]; // 值的队列
private root: BinaryTreeNode<T>;
constructor(root: BinaryTreeNode<T>) {
this.values = [];
this.root = root;
this.inOrder(root) // 构造函数进行中序遍历来填充值的队列
}
next(): IteratorResult<T> {
// 对 next() 的每次调用将通过调用 shift() 从队列中取出值
const result: T | undefined = this.values.shift()
if (!result) {
return {
done: true,
value: result
}
}
return {
done: false,
value: result
}
}
private inOrder(node: BinaryTreeNode<T>): void {
if (node.left) {
this.inOrder(node.left)
}
this.values.push(node.value)
if (node.right) {
this.inOrder(node.right)
}
}
}
链表迭代器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class LinkedListIterator<T> implements Iterator<T> {
private head: LinkedListNode<T>;
private current: LinkedListNode<T> | undefined;
constructor(head: LinkedListNode<T>) {
this.head = head
this.current = head
}
next(): IteratorResult<T> {
if (!this.current) {
return {
done: true,
value: this.head.value
}
}
const result: T = this.current.value
this.current = this.current.next
return {
done: false,
value: result
}
}
}
使用迭代器的print()
1
2
3
4
5
6
7
8
9
// print()是一个泛型函数,接受一个迭代器作为实参
function print<T>(iterator: Iterator<T>): void {
// 使用 next 初始化,取出第一个值
let result: IteratorResult<T> = iterator.next()
while(!result.done) {
console.log(result.value)
result = iterator.next()
}
}
使用迭代器的contains()
1
2
3
4
5
6
7
8
9
10
// print()是一个泛型函数,接受一个迭代器作为实参
function contains<T>(value: T, iterator: Iterator<T>): boolean {
// 使用 next 初始化,取出第一个值
let result: IteratorResult<T> = iterator.next()
while(!result.done) {
if (result.value === value) return true
result = iterator.next()
}
return false
}

迭代器是把数据结构和算法连接起来的胶水,使得这种解耦能够实现。

流水线化迭代代码

TS中已经预定义了 IteratorResult<T>Iterator<T> 类型

可迭代的接口
1
2
3
4
5
interface Iterable<T> {
// [Symbol.iterator] 是 TS 特殊语法,只是代表一个特殊名称。
// 与 前文中实现 Normal Subtype 技巧类似
[Symbol.iterator](): Iterator<T>;
}
可迭代的链表
1
2
3
4
5
6
7
8
9
10
11
class LinkedListNode<T> implements Iterable<T> {
value: T;
next: LinkedListNode<T> | undefined;
constructor(value: T) {
this.value = value;
}
// 通过在链表中创建 LinkedListIterator 的新实例来实现 Iterable<T> 接口
[Symbol.iterator](): Iterator<T> {
return new LinkedListIterator<T>(this)
}
}

在TS中,Iterable 数据结构允许使用 for...of语法。

'使用Iterator实参的print()和contains()'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function print<T>(iterator: Iterator<T>): void {
let result: IteratorResult<T> = iterator.next()
while(!result.done) {
console.log(result.value)
result = iterator.next()
}
}

function contains<T>(value: T, iterator: Iterator<T>): boolean {
let result: IteratorResult<T> = iterator.next()
while(!result.done) {
if (result.value === value) return true
result = iterator.next()
}
}
'使用 Iterable 实参的print()和contains()'
1
2
3
4
5
6
7
8
9
10
11
12
13
function print<T>(iterable: Iterable<T>): void {
for (const item of iterable) {
console.log(item)
}
}

function contains<T>(value: T, iterable: Iterable<T>): boolean {
for (const item of iterable) {
// return 可以退出 for...of 循环
if (item === value) return true
}
return false
}
使用生成器的二叉树迭代器
1
2
3
4
5
6
7
8
9
10
11
12
13
function* inOrderIterator<T>(root: BinaryTreeNode<T>):IterableIterator<T> {
if (root.left) {
for (const value of inOrderIterator(root.left)) {
yield value;
}
};
yield root.value
if (root.right) {
for (const value of inOrderIterator(root.right)) {
yield value;
}
}
}
使用生成器的链表迭代器
1
2
3
4
5
6
7
function* LinkedListIterator<T>(head: LinkedListNode<T>): IterableIterator<T> {
let current: LinkedListNode<T> | undefined = head;
while(current) {
yield current.value;
current = current.next;
}
}
使用生成器的可迭代链表
1
2
3
4
5
6
7
8
9
10
11
class LinkedListNode<T> implements Iterable<T> {
value: T;
next: LinkedListNode<T> | undefined;
constructor(value: T) {
this.value = value;
}
// [Symbol.iterator]() 只是返回 LinkedListIterator 的结果
[Symbol.iterator](): Iterator<T> {
return LinkedListIterator(this)
}
}

习题

泛型二叉树的先序遍历&顺序遍历数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 泛型二叉树结构
class BinaryTreeNode<T> {
value: T;
left: BinaryTreeNode<T> | undefined;
right: BinaryTreeNode<T> | undefined;
constructor(value: T) {
this.value = value
}
}
function* inOrderIteratorFirst<T>(root: BinaryTreeNode<T>):inOrderIteratorFirst<T> {
yield root.value
if (root.left) {
for (const value of inOrderIteratorFirst(root.left)) {
yield value;
}
};
if (root.right) {
for (const value of inOrderIteratorFirst(root.right)) {
yield value;
}
}
}

function* IteratorArr<T>(array: T[]): void {
for (const item of array) {
yield item;
}
}

数据流

迭代器并非是有限的

无限随机数据流
1
2
3
4
5
6
7
8
9
10
11
12
13
// 无限随机数据流
function* generateRandomNumbers(): IterableIterator<number> {
while(true) {
yield Math.random()
}
}
// 使用流中的数字
let iter: IterableIterator<number> = generateRandomNumbers()
// 迭代器 的调用方式
console.log(iter.next().value)
console.log(iter.next().value)
console.log(iter.next().value)
console.log(iter.next().value)

处理管道

处理管道的组件是一些函数,接受一个迭代器作为实参,返回一个迭代器。这种函数可以链接起来。这种模式是反应式编程的基础。

由前文实现 IterableIterator<T> 方式可知,IterableIterator<T>Iterable<T> 类型的 subtype, 函数实参是双向协变的。所以也可以传入 Iterable<T>

管道是延迟计算的,可以逐个(按调用顺序)处理值

square & take & 管道
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function* square(iter: Iterable<number>): IterableIterator<number> {
for (const value of iter) {
yield value ** 2
}
}
function* take<T>(iter: Iterable<T>, n: number): IterableIterator<T> {
for (const value of iter) {
if (n-- <= 0) return
yield value
}
}
const values: IterableIterator<number> = take(square(generateRandomNumbers()), 5)
for (const value of values) {
console.log(value)
}
// 练习 实现 drop 丢弃一个迭代器的前n个元素而返回其余元素
function* drop<T>(iter: Iterable<T>, n: number): IterableIterator<T> {
for (const [v, k] of iter) {
if (n-- > 0) continue
yield value
}
}
function* count(): IterableIterator<number> {
let n: number = 0
while(true) {
n++
yield n
}
}
for (const value of take(drop(count(), 5), 5)) {
console.log(value)
}

第十章 - 泛型算法和迭代器

更好的 map()filter()reduce()

使用迭代器的 map & filter & reduce
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 可应用到任何 Iterable<T>,而不只是数组
function* map<T, U>(iter: Iterable<T>, func: (item: T) => U):IterableIterator<U> {
for (const value of iter) {
yield func(value)
}
}
// pred=谓词
function* filter<T, U>(iter: Iterable<T>, pred: (item: T) => boolean):IterableIterator<U> {
for (const value of iter) {
if (pred(value)) {
yield func(value)
}
}
}
function* reduce<T>(iter: Iterable<T>, init: T, op: (x: T, y: T) => T):T {
let result: T = init
for (const value of iter) {
result=op(result, value)
}
return result
}

filter()/reduce()管道

从一个二叉树中取出偶数并对它们求和,使用第九章的 BinaryTreeNode<T>,采用中序遍历,并链接到一个偶数过滤函数和使用加法操作的reduce

1
2
3
4
5
6
7
8
9
10
let root: BinaryTreeNode<number> = new BinaryTreeNode(1);
root.left = new BinaryTreeNode(2);
root.left.right = new BinaryTreeNode(3)
root.right = new BinaryTreeNode(4)
// 1
// 2 4
// 3
// 中序遍历打印结果:2 3 1 4
const result: number = reduce(filter(inOrderIterator(root), (value) => value % 2 == 0), 0, (x, y) => x + y)
console.log(result) // 6

习题

1
2
3
4
5
6
7
8
9
10
// 连接所有非空字符串,处理 string 类型的一个可迭代结构
// reduce + filter
function concatStrAndFilterEmpty(iter: Iterable<string>): string {
return reduce(filter(iter, (value) => value.length > 0), '', (x: string, y: string) => x + y)
}
// 选择所有的奇数并求平方,处理 number 类型的一个可迭代结构
// map + filter
function squareOdds(iter: Iterable<number>): IterableIterator<number> {
return map(filter(iter, (value) => value % 2 == 1), (x) => x * x)
}

常用算法

实现流畅管道:封装更好的流畅的可迭代结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class FluentIterable<T> {
iter: Iterable<T>;
constructor(iter: Iterable<T>) {
this.iter = iter
}
map<U>(func: (item: T) => U): FluentIterable<U> {
return new FluentIterable(this.mapImpl(func))
}
private *mapImpl<U>(func: (item: T) => U): IterableIterator<U> {
for (const value of this.iter) {
yield func(value)
}
}
filter<U>(pred: (item: T) => boolean): FluentIterable<T> {
return new FluentIterable(this.filterImpl(pred))
}
private *filterImpl(pred: (item: T) => boolean): IterableIterator<T> {
for (const value of this.iter) {
if (pred(value)) {
yield value
}
}
}
// 增加take()
take<T>(n: number): FluentIterable<T> {
return new FluentIterable(this.takeImpl(n))
}
private *takeImpl<T>(n: number): IterableIterator<T> {
for (const value of this.iter) {
if (n-- <= 0) return
yield value
}
}
// 增加drop()
drop<T>(n: number): FluentIterable<T> {
return new FluentIterable(this.dropImpl(n))
}
private *dropImpl<T>(n: number): IterableIterator<T> {
for (const [v, k] of this.iter) {
if (n-- > 0) continue
yield value
}
}
reduce(init: T, op: (x: T, y: T) => T): T {
let result: T = init;
for(const value of this.iter) {
result = op(result, value)
}
return result
}
}
应用filter/reduce管道
1
2
3
4
5
6
7
8
9
10
let root: BinaryTreeNode<number> = new BinaryTreeNode(1);
root.left = new BinaryTreeNode(2);
root.left.right = new BinaryTreeNode(3)
root.right = new BinaryTreeNode(4)
// 1
// 2 4
// 3
// 中序遍历打印结果:2 3 1 4
const result: number = new FluentIterable(inOrderIterator(root)).filter(value => value % 2 == 0).reduce(0, (x, y) => x + y)
console.log(result)

约束类型参数

约束告诉编译器某个类型实参必须具有的能力(该类型独有的方法,属性等)。一旦要求泛型类型要必须有特定成员,就使用约束将允许类型的集合限制为具有必要成员的那些类型

使用类型约束的 renderAll
1
2
3
4
5
6
7
8
interface IRenderable {
render(): void;
}
function renderAll<T extends IRenderable>(iter: Iterable<T>): void {
for(const item of iter) {
item.render()
}
}

具有类型约束的泛型算法

以实现一个 max() 泛型算法为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Icomparable 接口
enum ComparisonResult {
LessThan,
Equal,
GreeterThan
}
interface Icomparable<T> {
compareTo(value: T): ComparisonResult;
}
// 迭代器无值的情况下会返回undefined
// 所以不使用 for of循环,而使用 next()手动向前推进迭代器
function max<T extends Icomparable<T>>(iter: Iterable<T>): T | undefined {
let iterator: Iterator<T> = iter[Symbol.iterator]()
// 取出第一个值
let current: IteratorResult<T> = iter.next()
if (current.done) return undefined
let result: T = current.value
while(true) {
current = iterator.next()
if (current.done) return result
if (current.value.compareTo(result) == ComparisonResult.GreeterThan) {
result = current.value
}
}
}
// 也可以接受一个 compare 方法作为实参
function max<T extends Icomparable<T>>(iter: Iterable<T>, compare: (x: T, y: T) => ComparisonResult): T | undefined {
let iterator: Iterator<T> = iter[Symbol.iterator]()
// 取出第一个值
let current: IteratorResult<T> = iter.next()
if (current.done) return undefined
let result: T = current.value
while(true) {
current = iterator.next()
if (current.done) return result
if (compare(current.value, result) === ComparisonResult.GreeterThan) {
result = current.value
}
}
}

习题

实现一个泛型函数 Clamp(value, min, max), 如果 `min < value < max ? value : (value <= min ? min : max)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum ComparisonResult {
LessThan,
Equal,
GreeterThan
}
interface Icomparable<T> {
compareTo(value: T): ComparisonResult;
}

function Clamp<T extends Icomparable<T>>(v: T, min: T, max: T): T | undefined {
if (!value) return undefined
if (value.compareTo(min) === ComparisonResult.LessThan) return min
if (value.compareTo(max) === ComparisonResult.GreeterThan) return max
return value
}

高效 reverse 和其他使用迭代器的算法

使用一个栈存储来实现reverse的空间是O(n),时间也是T(n),都是线性的;使用数组交换两头来实现reverse的空间是O(1),只用了一个临时变量,时间是T(n/2)

迭代器的基础模块

判断迭代器合适应该停止=捕获2个迭代器相同的时机:重新定义为一组接口,公开一个get()方法,用于返回T类型的一个值,使用这个方法从迭代器中读取值

'IReadable, IIncrementable, IInputIterator'
1
2
3
4
5
6
7
8
9
10
11
// get() 用于获取迭代器当前的值
interface IReadable<T> {
get(): T;
}
// increment() 用于将迭代器推进到下一个元素
interface IIncrementable<T> {
increment(): void;
}
interface IInputIterator<T> extends IReadable<T>, IIncrementable<T> {
equals(other: IInputIterator): boolean;
}

为链表实现一个满足新的 IInputIterator<T> 接口的 LinkedListInputIterator<T>

输入迭代器: 能够遍历序列一次并提供其值的迭代器,不能第二次重放值。并非必须遍历持久性数据结构,也可以从生成器或者其他某种数据源提供值。

链表输入迭代器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class LinkedListInputIterator<T> implements IInputIterator<T> {
private node: LinkedListNode<T> | undefined;
constructor(node: LinkedListNode<T> | undefined) {
this.node = node
}
increment(): void {
if(!this.node) throw Error();
this.node = this.node.next
}
get(): T {
if(!this.node) throw Error();
return this.node.value
}
equals(other: IInputIterator<T>): boolean {
return this.node == (<LinkedListInputIterator<T>>other).node
}
}
// 使用:链表上的一对迭代器
const head: LinkedListNode<number> = new LinkedListNode(0)
head.next = new LinkedListNode(1)
head.next.next = new LinkedListNode(2)
let begin: IInputIterator<number> = new LinkedListInputIterator(head)
// end 的值是 undefined
let end: IInputIterator<number> = new LinkedListInputIterator(undefined)

输出迭代器: 能够遍历一个序列并向其卸任值的迭代器,并不需要能够读值。同样的,并非必须遍历持久性数据结构,也可以把值卸任其他输出。

'IWritable and IOutputIterator'
1
2
3
4
5
6
interface IWritable<T> {
set(value: T): void;
}
interface IOutputIterator<T> extends IWritable<T>, IIncrementable<T> {
equals(other: IOutputIterator<T>): boolean;
}

实现一个写入到控制台的输出迭代器:写入到输出流。例如:网络连接,标准输出,标准错误等,但是不能读值。

控制台输出迭代器
1
2
3
4
5
6
7
8
9
10
11
class ConsoleOutputIterator<T> implements IOutputIterator<T> {
set(value: T): void {
console.log(value)
}
// 这个例子没有遍历数据结构,所以可以啥也不做
increment(): void {}
equals(other: IOutputIterator<T>): boolean {
// 始终返回false,因为写入控制台没有可以比较的序列末尾
return false
}
}
具有输入和输出迭代器的map
1
2
3
4
5
6
7
8
9
10
11
12
function map<T, U>(
begin: IInputIterator<T>,
end: IInputIterator<T>,
out: IOutputIterator<U>,
func: (value: T) => U
): void{
while (!begin.equals(end)) {
out.set(func(begin.get()))
begin.increment()
out.increment()
}
}

有用的find()

前向迭代器:向前推进,可以读取当前位置的值以及更新该值的迭代器。前向迭代器也可以被克隆,使得推进该迭代器不会影响该迭代器的克隆。跟输入输出迭代器不同,它可以多次遍历一个序列。在推进原迭代器时,副本迭代器不会移动,是独立的。因此它可以多次遍历。

'IForwardIterator and LinkedListIterator 实现 IForwardIterator'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
interface IForwardIterator<T> extends IReadable<T>, IWritable, IIncrementable<T> {
equals(other: IForwardIterator<T>): boolean;
clone(): IForwardIterator<T>
}
class LinkedListIterator<T> implements IForwardIterator<T> {
private node: LinkedListNode<T> | undefined;
constructor(node: LinkedListNode<T> | undefined) {
this.node = node
}
increment(): void {
if (!this.node) return
this.node = this.node.next
}
get(): T {
if(!this.node) throw Error()
return this.node.value
}
// set() 是 IWritable<T> 需要的一个额外的方法,用于封信链表节点的值
set(value: T): void {
if(!this.node) throw Error()
this.node.value = value
}
// equals 现在期望收到另外一个 IForwardIterator<T>
equals(other: IForwardIterator<T>): boolean {
return this.node = (<LinkedListIterator<T>>other).node
}
//
// 创建一个新的迭代器,指向与当前迭代器相同的节点
clone(): IForwardIterator<T> {
return new LinkedListIterator(this.node)
}
}
使用前向迭代器的find
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function find<T> (
begin: IForwardIterator<T>,
end: IForwardIterator<T>,
pred: (value: T) => booean
): IForwardIterator<T> {
while(!begin.equals(end)) {
if (pred(begin.equals(end))) {
return begin
}
begin.increment()
}
return end
}
// 应用:将链表中的42替换为0
let head: LinkedListNode<number> = new LinkedListNode(1)
head.next = new LinkedListNode(2)
head.next.next = new LinkedListNode(42)
let begin: IForwardIterator<number> = new IForwardIterator(head)
let end: IForwardIterator<number> = new IForwardIterator(undefined)
let iter: IForwardIterator<number> = find(begin, end, (value: number) => value == 42)
// 调用
if (!iter.equals(end)) {
iter.set(0)
}

高效的reverse()

数组实现reverse()时是从数组的两端开始互换元素,一直递增前向索引。递减后向索引,直到两者相遇。

双向迭代器:不仅具有前向迭代器的所有能力,而且还可以递减。既能前向,又能后向遍历。可以读写当前元素的值,克隆自己,以及向前或向后步进。

链表不支持后向迭代,但是可以使用一个双向链表保存其前导节点和后继节点的引用。

'IBiddirectionalIterator and ArrayIterator'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
interface IBiddirectionalIterator<T> extends IReadable<T>, IWritable<T>, IIncrementable<T> {
decrement(): void;
equals(other: IBiddirectionalIterator<T>): boolean;
clone(): IBiddirectionalIterator<T>
}
class ArrayIterator<T> implements IBiddirectionalIterator<T> {
private array: T[];
private index: number;
constructor(array: T[], index: number) {
this.array = array;
this.index = index
}
get(): T {
return this.array(this.index)
}
set(value: T): void {
this.array(this.index) = value
}
increment(): void {
this.index++
}
decrement(): void {
this.index--
}
equals(other: IBiddirectionalIterator<T>): boolean {
return this.index == (<ArrayIterator<T>>other).index
}
clone(): IBiddirectionalIterator<T> {
return new ArrayIterator(this.array, this.index)
}
}
使用双向迭代器的reverse
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function reverse<T>(
begin: IBiddirectionalIterator<T>,
end: IBiddirectionalIterator<T>
): void {
while(!begin.equals(end)) {
// end 从数组末尾后面的一个元素开始,所以在使用之前需要递减
end.decrement()
if (begin.equals(end)) return
const tmp: T = begin.get()
begin.set(end.get())
end.set(temp)
begin.increment()
}
}
// 翻转一个数值数组
let array: number[] = [1,2,3,4,5];
let begin: IBiddirectionalIterator<number> = new ArrayIterator(array, 0)
let end: IBiddirectionalIterator<number> = new ArrayIterator(array, array.length) // 将数组的end迭代器初始化为索引长度,所以初始array[end]值为undefined
reverse(begin, end)
console.log(array) // [5,4,3,2,1]

高效地获取元素

随机访问迭代器:能够以O(1)时间向前或向后跳过任意多元素。可以读写当前元素的值、克隆自己以及向前或向后移动任意元素。

数组可以通过索引快速获取任何元素,但是双向链表/链表均不支持随机访问迭代器。

'IRandomAccessIterator & 更新后的 ArrayIterator'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface IRandomAccessIterator<T> extends IReadable<T>, IWritable<T>, IIncrementable<T> {
decrement(): void;
equals(other: IRandomAccessIterator<T>): boolean;
clone(): IRandomAccessIterator<T>;
move(n: number): void;
distance(other: IRandomAccessIterator<T>): number;
}
class ArrayIterator<T> implements IRandomAccessIterator<T> {
// ...
// 与上面相同的部分
// 更新
equals(other: IRandomAccessIterator<T>): boolean {
return this.index == (<ArrayIterator<T>>other).index
}
clone(): IRandomAccessIterator<T> {
return new ArrayIterator(this.array, this.index)
}
// 将迭代器推进 n 个步长(n 可以是负数,代表向后移动)
move(n: number): void {
this.index += n
}
distance(other: IRandomAccessIterator<T>): number {
return this.index - (<ArrayIterator<T>>other).index
}
}

实现一个 elementAt() 算法,返回只想序列中第n个元素的迭代器。使用一个输入迭代器来实现时:需要递增迭代器 n 次此案达到期望的元素,时间复杂度为 O(n);使用随机访问迭代器时,时间复杂度为 O(1);

访问指定位置的元素
1
2
3
4
5
6
7
8
9
10
function elementAtRandomAccessIterator<T> (
begin: IRandomAccessIterator<T>,
end: IRandomAccessIterator<T>,
n: number
): IRandomAccessIterator<T>{
// 移动 n 个元素
begin.move(n)
if (begin.distance(end) <= 0) return end;
return begin;
}

自适应算法

使用输入迭代器的elementAt
1
2
3
4
5
6
7
8
9
10
11
function elementAtForwardIterator<T> (
begin: IForwardIterator<T>,
end: IForwardIterator<T>,
n: number
): IForwardIterator<T>{
while(!begin.equals(end) && n > 0) {
begin.increment()
n--
}
return begin
}
自适应的elementAt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function isRandomAccessIterator<T> (
iter: IForwardIterator<T>
): iter is IRandomAccessIterator<T>{
return 'distabce' in iter
}
function elementAt<T> (
begin: IForwardIterator<T>,
end: IForwardIterator<T>,
n: number
): IForwardIterator<T> {
if(isRandomAccessIterator(begin) && isRandomAccessIterator(end)) {
return elementAtRandomAccessIterator(begin, end, n)
} else {
return elementAtForwardIterator(begin, end, n)
}
}

补充:

ES6遍历器 Iteratorfor...of

ES6中的:Array, Map, Set, String, TypedArray, arguments, NodeList都自带遍历器接口Iterable定义见上,但是对象没有,原因见下

一种接口,为各种不同的数据结构提供统一的访问机制。

3种作用

  1. 为各种不同的数据结构提供统一的访问接口;
  2. 使得数据成员能够按照某种次序排列-对象的属性没有固定顺序,这就是为什么对象类型没有遍历器接口,不过可以曲线使用 Object.keys() or Object.values() 来实现遍历。
  3. Iterator接口主要提供给 for...of 消费
模拟 Iterator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let it = makeIterator(['a', 'b'])
it.next() // {value: 'a', done: false}
it.next() // {value: 'b', done: false}
it.next() // {value: undefined, done: true}
function makeIterator(array) {
let nextIndex = 0
return {
next: function () {
return nextIndex < array.length ?
// array[nextIndex]
// nextIndex++
{ value: array[nextIndex++], done: false}
:
{ value: undefined, done: true}

}
}
}

默认调用Iterator接口的场合

  1. 解构赋值
  2. 扩展运算符
  3. yield* 后面根的是一个可遍历的结构,它会在调用该结构的遍历器接口;
  4. 其他场合:使用数组作为参数的场合: for...of, Array.from(), Map(), Set(), WeakMap(), WeakSet(), Promise.all(), Promise.race()

ES6迭代器 generator

  1. 使用 generator 生成遍历器对象;
  2. 状态机:保存多个内部状态;
  3. 调用方式和前文的 Iterator 一样,不会立即执行,需要反复调用next,直到遇到 return 或者 下一个yield`;
  4. yield 后面的表达式是惰性的,只有next指针指向该语句时才会求值。
  5. yield* 后可跟 generator - generator 嵌套使用
  6. iter.next() 迭代器实例的**next是同步的**

generator 错误捕获

外部执行函数时使用 try catch 捕获

generator 的异步应用

协程 generator 函数实现

最大的特点是可以交出函数的执行权;但是流程管道却不方便。

iter.next(1) 函数 next可传入参数作为上一步的返回结果,前提是 return 的是 yield

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 这种写法可以
function* gen(x) {
const y = yield x + 2
return y
}
// 这种不行
function* gen(x) {
yield x + 2
return 2
}
const g = gen(1)
g.next() // {value: 3, done: false}
// 如果上一步value不为undefined,那么只改变value,done的值由当前调用次数决定
g.next(2) // {value: 2, done: true}
// 已经执行完毕后值不变,就是undefined
g.next(3) // {value: undefined, done: true}

Trunk 函数

自动执行 Generator 函数的一种方法

参数的求值策略:

  1. 传值策略 - JS
  2. 传名策略 - 编译器

async await

  1. forEach可以传入 async 函数实现并发

  2. async 函数内使用for 循环、或 reduce()实现继发

  3. map 内传入异步实现不了并发,只能得到一组异步函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    const testArr = [1,2,3,4,5]
    testArr.map(async (item) => {
    return await item
    })
    // 等于
    async () => {
    return await 1
    }
    async () => {
    return await 2
    }
    async () => {
    return await 3
    }
    async () => {
    return await 4
    }
    async () => {
    return await 5
    }
  4. async 函数可以保留运行堆栈: 而普通函数调用异步函数则没有保留运行堆栈。

异步遍历器

异步遍历器接口定义
1
2
3
4
5
6
7
8
9
10
interface AsyncIterable {
[Symbol.asyncIterator](): AsyncIterator;
}
interface AsyncIterator {
next(): Promise<IteratorResult>
}
interface IteratorResult {
value: any;
done: boolean;
}

next()返回的 value属性是一个 Promise 对象,而done还是同步的,在 调用 then()await 后返回真正的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const asyncIterable = creatAsyncIterable(['a', 'b'])
const asyncIterator = asyncIterable[Symbol.asyncIterator]()
// then
asyncIterator.next().then(r1 => {
console.log(r1) // {value: 'a', done: false}
return asyncIterator.next()
}).then(r2 => {
console.log(r2) // {value: 'b', done: false}
return asyncIterator.next()
})
.then(r3 => {
console.log(r3) // {value: undefined, done: true}
})
// await
async funcction f() {
const asyncIterable = creatAsyncIterable(['a', 'b'])
const asyncIterator = asyncIterable[Symbol.asyncIterator]()
console.log(await asyncIterator.next()) // {value: 'a', done: false}
console.log(await asyncIterator.next()) // {value: 'b', done: false}
console.log(await asyncIterator.next()) // {value: undefined, done: true}
}
// 异步遍历器的 next() 是可以连续调用的
// 1. Promise.all() 将所有的 next 方法放在 Promise.all() 里面
const [{value: v1}, {value: v2}] = await Promise.all([asyncIterator.next(), asyncIterator.next()])
console.log(v1, v2)
// 2. 一次性调用所有的 next 方法,然后 await 最后一步操作
async function runner() {
const writer = openfile('somefile.txt')
writer.next('hello')
writer.next('world')
await writer.return()
}
runner()

for await..of 遍历异步 Iterator 接口

也可以用于同步遍历器

for..of 循环自动调用这个对象的异步遍历器 next,会得到一个 Promise 对象, await 处理这个 Promise 对象的 resolve值,try catch来处理这个 Promise 对象的 reject

异步 Generator 函数

返回一个异步遍历器对象 AsyncIterator,跟在 yield命令后面的应该是一个 Promise 对象, 如果不是也会被自动包装成一个Promise 对象

1
2
3
4
5
6
async function* gen() {
yield 'hello'
}
const genObj = gen()
genObj.next().then(x => console.log(x))
// 如果抛错会被Promise.catch捕获

第二章:基本类型

never 与 自定义 never 类型

  1. 无法实例化的类
  2. 没有元素的枚举
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export {}
declare const EmptyType: unique symbol; // symbols 的子类, 用于常量和属性名,必须用const不能用let
// 私有构造函数确保其他代码不能实例化这个类型
// 因此,也无法用 InstanceType 获取实例 类型
class Empty {
[EmptyType]: void;
private constructor() {}
}

// Empty 与 never 作用相当
// 因为不能创建 Empty 实例,所以根本不能添加 return 语句
function raise(msg: string): Empty {
console.log(`Error "${msg}" raised at ${new Date()}`)
throw new Error(msg)
}
// 没有值的枚举
enum EmptyType {}

单元类型

单元类型只允许有一个值,JS & TS 中都是 void

利用函数的 副作用而不是返回值, React & Vue3: useEffect, useEffective -> 考虑这俩的返回值是什么?获得其真正的功能。函数接受任意数量的实参却不返回任意有意义的值,这种函数称为 动作 or 消费者

  1. void
  2. 自制单元类型:只有一个值的枚举,或者没有状态的单例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 只有一个值的枚举
enum UnitTypeSingle {
value = 'void' // 值是什么并不重要,满足枚举特性即可
}
// 没有状态的单例
// 与上面的 Empty 类似,只不过有一个值,可以 return
declare const UnitType: unique symbol;
// 私有构造函数确保其他代码不能实例化这个类型
// 因此,也无法用 InstanceType 获取实例 类型
// 仅作为类型使用,无法实例化
class Unit {
[EmptyType]: void;
static readonly value: Unit = new Unit(); // 单个实例,静态只读属性
private constructor() {}; // 不能实例化
}
function greet(): Unit {
return Unit.value
}

数值类型常见陷阱

  1. 整数类型与溢出

处理上溢下溢的3种主要方式:环绕(简单丢弃不合适的位),饱和(停在最大值,算数运算失去结合性),报错

1
2
3
4
5
6
7
//  4位无符号编码表示:0~15,即 `b^N-1b^N-2....b1b0` 转为 10进制 `b^N-1 x 2^N-1 + ....b^0 x 2^0`
// 4位带符号编码表示:-8~7,首位1即负数,0即正数,负数编码是 2^N - (绝对值)
-8 = 2^4 - 8 //
// 算数上溢 - 数超出表达范围
// 下溢 - 数太小无法给定 位数 表示

// 上溢 - 环绕: 1111(15) + 1 => 10000 超出4位,丢弃1位(从右到左)1 => 0000,即环绕回0
  1. 浮点类型和圆整

TS & JS: 使用 binary64 编码将数字表示为 64 位浮点数,0.10 的近似数为 0.100000000000000005551115123216,处理 0.1 时 近似数被圆整为 0.1,相加 3次后的和被圆整成0.10000000000000004,所以 0.30 !== 0.1 + 0.1 + 0.1

1
2
// 防止圆整
Number.isSafeInteger() // 一个整数值能否在不被圆整的情况下表示出来
  1. 比较浮点数

确保2浮点数差值在给定阈值内,Js内的给定阈值是 Number.epsilon

  1. 任意大整数JS: BigInt

    JS 原生提供BigInt函数,可以用它生成 BigInt 类型的数值。转换规则基本与Number()一致,将其他类型的值转为 BigInt

1
2
3
4
5
6
typeof 123n // 'bigint'
typeof 123 // number
-42n // 正确
+42n // 报错 - BigInt 可以使用负数 - ,不能使用 +,与 asm.js 语法冲突
BigInt(123) // 123n
BigInt('123') // 123n

编码

UTF-32 是固定编码,一个字符由4个字节组成,UTF-8是变长编码,字节数依赖字符数

使用数组和引用构建数据结构

固定大小数组(具有极快的读取/更新能力,利于表示稠密数据):连续内存区域,不能在原内存位置增长或者缩减。如果要 push 一个值,则改变了 固定,此时则需要分配一个新数组,将之前的5个值复制过来

考虑 React 中业务对数组的处理:使用 slice & concat 而不是 push & pop,对 不可变 的处理

高效列表

  1. 使用 链表 实现 列表数据结构: 访问某个元素开销较大 T(N),append 则很容易
  2. 使用 数组 实现 列表数据结构: 访问某个元素很容易,append 开销较大 T(N)

大部分语言都使用 具有额外容量的基于数组的列表实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class NumberList {
private numbers: number[] = new Array(1);
private length: number = 0;
private capacity: number = 1;
at(index: number): number {
if (index >= this.length) {
throw new RangeError()
}
return this.numbers[index];
}
append(value: number) {
if (this.length < this.capacity) {
this.numbers[this.length] = value
this.length++
return
}
// 扩容
this.capacity = this.capacity * 2
let newNumbers: number[] = new Array(this.capacity)
// 复制原数组值到新扩容后的数组
for (let i = 0; i< this.length; i++>) {
newNumbers[i] = this.numbers[i]
}
// append value
newNumbers[this.length] = value
// 迁移数组
this.numbers = newNumbers
this.length++
}
}

二叉树

  1. 基于数组的二叉树实现 - 稀疏二叉树浪费空间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 树的下标从1开始
    class Tree {
    nodes: (number | undefined)[] = []
    // 给定上层节点的索引时,计算左侧节点位置
    left_node_index(index: number): number {
    return index * 2
    }
    // 给定上层节点的索引时,计算右侧节点位置
    right_node_index(index: number): number {
    return index * 2 + 1
    }
    add_level() {
    // 数组扩容
    let newNodes: (number | undefined)[] = new Array(this.nodes.length * 2 + 1)
    // 扩容后复制原数组值
    for (let i = 0; i < this.nodes.length; i++) {
    newNodes[i] = this.nodes[i]
    }
    // 迁移数组
    this.nodes = newNodes
    }
    }
  2. 对树的根节点的引用来表现树 - 紧凑二叉树实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class TreeNode {
    value: number;
    left: TreeNode | undefined; // 引用其他节点
    right: TreeNode | undefined;// 引用其他节点
    constructor(value: number) { // 初始化节点
    this.value = value
    this.left = undefined
    this.right = undefined
    }
    }

关联数组 - 字典/哈希表(JS/TS)

固定大小数组+引用(引用的追加效果更好,利于表示稀疏数据),引用指向一个列表(参考上述高效列表)

第三章:组合

复合类型

  1. 元组 Tuple - 固定长度数组(一组不同或相同类型),按分量值位置访问值,容易出错

  2. 记录 Record - 通过属性名称访问值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 自定义元组
    class Pair<T1, T2> {
    m0: T1;
    m1: T2;
    constructor(m0: T1, m1: T2) {
    this.m0 = m0
    this.m1 = m1
    }
    }
    type Point2D = Pair<number, number>
    // Record
    class Point<T1, T2> {
    x: T1;
    y: T2;
    constructor(x: T1, y: T2) {
    this.x = x
    this.y = y
    }
    }
    // point1 只能有 x, y2个属性
    const point1: Point<number, number> = {
    x: 1,
    y: 4
    }
  3. 维护不变量:class 创建的变量(object)可以有关联的方法

    • 不需要保证不变量,属性公有即可
    • 属性只读,也不需要维护不变量,在构造函数中验证初始值即可

确保值的格式正确的规则称为不变量:以JS/TS 为例,通过让属性私有,并提供更新私有属性方法,可以确保维持不变量的稳定

使用类型表达多选一

  1. 使用枚举替代常量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    enum DayofWeek {
    Sunday,
    Monday,
    Tuesday,
    Webnesday,
    Thursday,
    Friday,
    Saturday
    }
    // 自定义可选类型
    class Optional<T> {
    private value: T | undefined;
    private assigned: boolean;
    constructor(value?: T) {
    this.value = value ? value : undefined
    this.assigned = value ? true : false
    }
    hasValue():boolean {
    return this.assigned
    }
    getValue(): T {
    if(!this.assigned) throw Error();
    return this.value as T
    }
    }
  2. 结果或错误:返回一个结果或者返回错误

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    enum InputError {
    NoInput,
    Invaild
    }
    class Result {
    error: InputError | undefined;
    value: DayofWeek | undefined;
    constructor(error?: InputError, value?: DayofWeek) {
    this.error = error || undefined
    this.value = value || undefined
    }
    }
    function parseDayofWeek(input: string): Result {
    if (!input) {
    return new Result(InputError.NoInput)
    }
    switch(input.toLowerCase()) {
    // sunday ~ monday
    // new Result(undefained, input)
    default:
    return new Result(InputError.Invaild);
    }
    }
  3. 自制 Either 类型

    考虑上述的 Result 并不通用,进一步提取 Either,使其可以应用到其他地方

    Either 类型 包含2个类型, TLeft & TRight, 约定 TLeft 存储错误类型, TRight存储有效值类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    class Either<TL, TR> {
    private readonly value: TL | TR;
    private readonly left: boolean
    // 使用私有构造函数,因为需要确保 value 和 left 标志是同步的
    private constructor(value: TL | TR, left: boolean) {
    this.value = value
    this.left = left
    }
    isLeft(): boolean {
    return this.left
    }
    getLeft(): TL {
    if(!this.isLeft()) throw new Error()
    return <TL>this.value
    }
    isRight(): boolean {
    return !this.left
    }
    getRight(): TR {
    if(!this.isRight()) throw new Error()
    return <TR>this.value
    }
    static makeLeft<TL, TR>(value: TL) {
    return new Either<TL, TR>(value, true)
    }
    static makeRight<TL, TR>(value: TR) {
    return new Either<TL, TR>(value, false)
    }
    }
    // 使用 Either 改造上述 返回
    enum InputError {
    NoInput,
    Invaild
    }
    type Result = Either<InputError, DayofWeek>
    function parseDayofWeek(input: string): Result {
    if (!input) {
    return Either.makeLeft(InputError.NoInput)
    }
    switch(input.toLowerCase()) {
    case 'sunday':
    return Either.makeRight(DayofWeek.Sunday)
    // 其他同理
    // ...
    default:
    return Either.makeLeft(InputError.Invaild);
    }
    }

变体类型:标签联合类型 (ts tagged union ?)

变体类型包含任意数量的基本类型的值,标签指的是 即使基本类型有重合的值,仍然能够准确说明该值来自哪个类型

  1. 先来欣赏一段体操

    1
    2
    3
    4
    5
    6
    7
    8
    // 将 tagged union 类型转为 union type
    interface Example {
    a: string;
    b: number;
    }
    type SingleProp<T, K extends keyof T> = {[P in keyof T]?: P extends K ? T[P] : never}
    type oneOf<T> = {[K in keyof T]: Pick<T, K> & SingleProp<T, K>}[keyof T]
    type ExamplePropUnion = oneOf<Example>
  2. 几何形状集合:添加一个 kind 属性标签,方便使用时判断或者强制转换形状

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Point {
    readonly kind: string = 'Point'
    x: number = 0;
    y: number = 0;
    }
    class Circle {
    readonly kind: string = 'Circle'
    x: number = 0;
    y: number = 0;
    radius: number = 0
    }
    type Shape = Point | Circle
  3. 实现一个通用的变体:而不需要类型自身存储一个标签

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    // 自制变体
    class Variant<T1, T2, T3> {
    readonly value: T1 | T2 | T3;
    readonly index: number;
    private constructor(value: T1 | T2 | T3, index: number) {
    this.value = value;
    this.index = index;
    }
    static make1<T1, T2, T3>(value: T1): Variant<T1, T2, T3> {
    return new Variant<T1, T2, T3>(value, 0);
    }
    static make2<T1, T2, T3>(value: T2): Variant<T1, T2, T3> {
    return new Variant<T1, T2, T3>(value, 1);
    }
    static make3<T1, T2, T3>(value: T3): Variant<T1, T2, T3> {
    return new Variant<T1, T2, T3>(value, 2);
    }
    }
    // 更新 几何形状集合
    class Point {
    x: number = 0;
    y: number = 0;
    }
    class Circle {
    x: number = 0;
    y: number = 0;
    radius: number = 0
    }
    class Rectangle {
    x: number = 0;
    y: number = 0;
    width: number = 0;
    height: number = 0;
    }
    type Shape = Variant<Point, Circle, Rectangle>;
    let shapes: Shape[] = [
    Variant.make2(new Circle()),
    Variant.make3(new Rectangle()),
    ]
    for (let shape of shapes) {
    switch (shape.index) {
    case 0:
    let point: Point = <Point>shape.value;
    console.log(`Point ${JSON.stringify(point)}`)
    break;
    case 1:
    let cicle: Circle = <Circle>shape.value;
    console.log(`Circle ${JSON.stringify(cicle)}`)
    break;
    case 2:
    let rectangle: Rectangle = <Rectangle>shape.value;
    console.log(`Point ${JSON.stringify(rectangle)}`)
    break;
    default:
    throw new Error()
    }
    }

访问者模式

角度1: 面向对象

实现一个文档,该文档包含3个项目:段落、图片和表格,在屏幕中渲染或者为有视障人群阅读的功能;
可见 IDocItem 存储了描述文档的信息,不应该负责其他工作,如 渲染和阅读,如果添加新功能,就需要更新 IDocItem接口以及所有实现类:ParagraphPictureTable,来实现这个新功能。

  1. 不使用访问者模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    // 不使用访问者模式
    class Renderer {/* Rendering methods */}
    class ScreenReader {/* Screen reading methods */}
    interface IDocItem {
    render(renderer: renderer): void;
    read(ScreenReader: ScreenReader): void;
    }
    // 实现段落
    class Paragraph implements IDocItem {
    // Paragraph members omitted
    render(renderer: renderer) {
    // draw
    }
    read(ScreenReader: ScreenReader) {
    // read
    }
    }
    // 图片
    class Picture implements IDocItem {
    // Picture members omitted
    render(renderer: renderer) {
    // draw
    }
    read(ScreenReader: ScreenReader) {
    // read
    }
    }
    // 表格
    class Table implements IDocItem {
    // Table members omitted
    render(renderer: renderer) {
    // draw
    }
    read(ScreenReader: ScreenReader) {
    // read
    }
    }
    let doc: IDocItem = [new Paragraph(), new Table()]
    let renderer: Renderer = new Renderer()
    for (let v of doc) {
    v.render(renderer)
    }
  2. 使用访问者模式: 在一个对象结构的元素上执行的操作,这种模式允许在定义新操作时,不改变其操作元素的类

    使用 双分派 机制,让文档接受访问者,然后把自己传递给访问者来实现任务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    interface IVistor {
    visitParagraph(paragraph: Paragraph): void;
    visitPicture(picture: Picture): void;
    visitTable(table: Table): void;
    }

    class Randerer implements IVistor {
    // Randerer methods ommited
    visitParagraph(paragraph: Paragraph) {/* do something */};
    visitPicture(picture: Picture){/* do something */};
    visitTable(table: Table) {/* do something */};
    }
    class ScreenReader implements IVistor {
    // Randerer methods ommited
    visitParagraph(paragraph: Paragraph) {/* do something */};
    visitPicture(picture: Picture) {/* do something */};
    visitTable(table: Table) {/* do something */};
    }
    interface IDocItem {
    accept(visitor: IVistor, printer: Printer<>): void;
    }
    // 实现段落
    class Paragraph implements IDocItem {
    // Paragraph members omitted
    accept(visitor: IVistor) {
    visitor.visitParagraph(this)
    }
    }
    // 图片
    class Picture implements IDocItem {
    // Picture members omitted
    accept(visitor: IVistor) {
    visitor.visitPicture(this)
    }
    }
    // 表格
    class Table implements IDocItem {
    // Table members omitted
    accept(visitor: IVistor) {
    visitor.visitTable(this)
    }
    }
    let doc: IDocItem = [new Paragraph(), new Table()]
    let renderer: IVistor = new Renderer()
    for (let v of doc) {
    v.accept(renderer)
    }

角度2: 泛型标签联合类型

  1. 访问变体

    使用变体访问者进行处理: 将域对象与访问者完全分离开,甚至不需要在存储文档类内实现 accept 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    // 变体访问者
    function visit<T1, T2, T3>(
    variant: Variant<T1, T2, T3>,
    func1: (value: T1) => void;
    func2: (value: T2) => void;
    func3: (value: T3) => void;
    ): void {
    switch (variant.index) {
    case 0:
    func1(<T1>variant.value);
    break;
    case 1:
    func2(<T2>variant.value);
    break;
    case 2:
    func3(<T3>variant.value);
    break;
    default:
    throw new Error();
    }
    }
    // 使用变体访问者进行处理
    class Randerer {
    // Randerer methods ommited
    visitParagraph(paragraph: Paragraph) {/* do something */};
    visitPicture(picture: Picture) {/* do something */};
    visitTable(table: Table) {/* do something */};
    }
    class ScreenReader {
    // Randerer methods ommited
    visitParagraph(paragraph: Paragraph) {/* do something */};
    visitPicture(picture: Picture) {/* do something */};
    visitTable(table: Table) {/* do something */};
    }
    // 文档不再需要一个公共接口
    // 实现段落
    class Paragraph {
    // Paragraph members omitted
    }
    // 图片
    class Picture {
    // Picture members omitted
    }
    // 表格
    class Table {
    // Table members omitted
    }
    // 将文档存储到变体中
    let doc: Variant<Paragraph, Picture, Table>[] = [
    Variant.make(new Paragraph(), 0),
    Variant.make(new Table(), 2),
    ]
    let renderer = new Renderer()
    for (let v of doc) {
    visit(
    v,
    (paragraph: Paragraph) => renderer.visitParagraph(paragraph),
    (picture: Picture) => renderer.visitPicture(picture)
    (table: Table) => renderer.visitTable(table)
    )
    }
  2. 习题:实现 visit() 在给定 Variant<T1, T2, T3> 返回 Variant<U1, U2, U3>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function visit<T1, T2, T3, U1, U2, U3> (
    variant: Variant<T1, T2, T3>,
    func1: (value: T1) => U1,
    func2: (value: T2) => U2,
    func3: (value: T3) => U3
    ): Variant<U1, U2, U3> {
    switch (variant.index) {
    case 0:
    return Variant.make1(func1(<T1>variant.value));
    case 1:
    return Variant.make2(func2(<T2>variant.value));
    case 2:
    return Variant.make3(func3(<T3>variant.value));
    default:
    throw new Error();
    }
    }

    代数数据类型 AlgeBraic Data Type, ADT

    在类型系统中组合类型的方式

    乘积类型 => 复合类型(元组 & 记录)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    enum A {
    a1,
    a2
    }
    enum B {
    b1,
    b2
    }
    // 2 X 2 = 4
    type AB = [A, B]; // [A.a1, B.b1], [A.a1, B.b2], [A.a2, B.b1], [A.a2, B.b2]
    type AB1 = Record<A, B>; // {0: B; 1: B}

    和类型 -> 多选一类型 & 变体类型

    TS 提供的 | 操作, 前面的 Optional, Either, Variant

    1
    type AB = A | B // A.a1 | A.a2 | B.b1 | B.b2, 2 + 2 = 4种

第四章 - 类型安全

如果2种类型具有相同形状,那么TS会认为类型兼容,如果使用 unique symbol ,就不能将一种隐式类型解释为另一种类型

避免基本类型偏执来防止错误解释

基本类型偏执反模式: 指依赖基本类型来表示所有的内容,因为类型系统没有显示捕捉到值的意义,所以很容易出错

  1. 不兼容组件
1
2
3
4
5
6
7
8
9
// lbfs(磅力秒),Ns(牛顿秒)
function trajectoryCorrection(momenttum: number) {
if (momenttum < 2 /* Ns */) {
disintegrate()
}
}
function provideMomenttum() {
trajectoryCorrection(1.5 /* lbfs */)
}
  1. 增加 磅力秒 & 牛顿秒 类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
declare const NsType: unique symbol;
class Ns {
readonly value: number;
[NsType]: void;
constructor(value: number) {
this.value = value
}
}
declare const LbfsType: unique symbol;
class Lbfs {
readonly value: number;
[LbfsType]: void;
constructor(value: number) {
this.value = value
}
}
// 转化单位:从 lbfs => Ns
function lbfsToNs(lbfs: Lbfs): Ns {
return new Ns(lbfs.value * 4.448222)
}
function trajectoryCorrection(momenttum: Ns) {
if (momenttum < new Ns(2) /* Ns */) {
disintegrate()
}
}
function provideMomenttum() {
trajectoryCorrection(lbfsToNs(new Lbfs(1.5)))
}

实施约束

使用构造函数实施约束

1
2
3
4
5
6
7
8
9
10
11
declare const celsiusType: unique symbol;
class Celsius {
readonly value: number;
[celsiusType]: void;
constructor(value: number) {
if (value < -273.15) throw new Error(); // 试图创建一个无效的温度,构造函数会抛出异常
// 或者:强制修改无效值
// if (value < -273.15) value = -273.15;
this.value = value
}
}

使用工厂实施约束

不想抛出异常,而是希望返回 undefined 或者其他某个不是温度而是表示失败的值以创建一个有效的实例时,使用工厂函数;如果构造函数和验证对象的逻辑很复杂,则应该在外部实现逻辑更加合理,构造函数不应该做太复杂的工作,而应该只做初始化对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
declare const celsiusType: unique symbol;
class Celsius {
readonly value: number;
[celsiusType]: void;
// private 声明构造函数,只有工厂函数才能调用
private constructor(value: number) {
this.value = value
}
static makeCelsius(value: number): Celsius | undefined {
if (value < -273.15) return undefined;
return new Celsius(value)
}
}

习题:实现一个 Percentage 类型来表示 0~100之间的值,小于 0 置0,大于 100 置 100

1
2
3
4
5
6
7
8
9
10
11
12
13
14
declare const percentageType: unique symbol;
class Percentage {
readonly value: number;
[percentageType]: void;
// private 声明构造函数,只有工厂函数才能调用
private constructor(value: number) {
this.value = value
}
static makePercentage(value: number): Percentage | undefined {
if (value < 0) value = 0;
if (value > 100) value = 100;
return new Percentage(value)
}
}

添加类型信息

类型转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 类型强制转换导致运行时错误
class Bike {
ride(): void {}
}
class SportsCar {
drive(): void {}
}
let myBike: Bike = new Bike()
Bike.ride()
// 先把 myBike 转成 unknown 再转成 SportsCar
let myPretendSportsCar: SportsCar = <SportsCar><unknown>myBike;
// 或者写成:
// let myPretendSportsCar: SportsCar = myBike as unknown as SportsCar;
myPretendSportsCar.drive() // 运行时错误

常见类型转换

  1. 向上转换 - 这个是安全的:将派生类型的对象解释为基类型,可自动隐式转换
  2. 向下转换 - 这个不是安全的:将基类型转化成派生类型,不会自动完成向下转换,因为编译器无法自动确定某个值是多个派生类的哪一个。
  3. 拓宽转换:一种常见的隐式转换-从固定位数的整数类型(8位无符号整数)转换为另一个位数更多的整数类型(16位无符号整数)。
  4. 缩窄转换:位数更多的整数转换成位数更少的无符号整数,只能用小类型可以表示的值。**必须显示指定这种转换**

隐藏和恢复类型信息

  1. 同构集合:包含相同类型项的集合
  2. 异构集合:包含不同类型项的集合

异构集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 层次结构
interface IDocItem {
/* 段落,图片和表格 公共部分 */
}
class Paragraph implements IDocItem {
/* 段落独有部分 */
}
class Picture implements IDocItem {
/* 图片独有部分 */
}
class Table implements IDocItem {
/* 表格独有部分 */
}
class MyDoc {
items: IDocItem[]
}
// 和类型
class Paragraph {
/* 段落 */
}
class Picture {
/* 图片 */
}
class Table {
/* 表格 */
}
class MyDoc1 {
items: (Paragraph | Picture | Table)[]
}
// unknown 类型
class MyDoc2 {
items: unknown[];
}

异构类型优缺点

类型 优点 缺点
层次结构 能够轻松地使用基础类型的任何属性和方法,不需要转换 集合中的类型必须通过基础类型或者实现的接口彼此相关
和类型 不要求类型彼此相关 如果没有 Variantvisit(), 就需要转换为实际的类型来驱动
unknown类型 可以存储任何内容 需要跟踪实际类型并转换为对应的类型才能使用

序列化

JSON.stringify() & JSON.parse()

  1. 序列化及跟踪类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Cat {
meow() { /* how about bark ? */ }
}
class Dog {
bark() { /* how about meow ? */ }
}
function serializeCat(cat: Cat): string {
return 'c' + JSON.stringify(cat)
}
function serializeDog(dog: Dog): string {
return 'd' + JSON.stringify(dog)
}
function tryDeserializeCat(from: string): Cat | undefined {
if (from[0] !== 'c') return undefined
return <Cat>Object.assign(new Cat(), JSON.parse(from .substr(1)))
}
  1. 带有跟踪类型的反序列化
1
2
3
4
5
6
7
8
let catString: string = serializeCat(new Cat())
let dogString: string = serializeDog(new Dog())
let maybeCat: Cat | undefined = tryDeserializeCat(catString)
if (maybeCat) {
let cat: Cat = <Cat>maybeCat
cat.meow()
}
maybeCat = tryDeserializeCat(dogString) // undefined

第五章 - 函数类型

一个简单的策略模式

一等函数:当语言看待函数的方式与看待其他任何值相同时,则该语言支持一等函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Car {
// Represents a car
}
type WashingStrategy = (car: Car) => void;
function standardWash(car: Car): void {
// perform standard wash
}
function premiumWash(car: Car): void {
// perform premium wash
}
class CarWash {
service(car: Car, premium: boolean) {
let washStrategy: WashingStrategy
if (premium) {
washStrategy = premiumWash
} else {
washStrategy = standardWash
}
washStrategy(car)
}
}

不使用 switch 语句的状态机

  1. 将状态集合定义为一个枚举,跟踪当前的状态

缺点:状态和相应的逻辑没有联系在一起,同时还需要单独维护枚举和case,会有值和逻辑不同步的风险

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 状态机用一个枚举表示
enum TextProcessingMode {
Text,
Marker,
Code
}
class TextProcessor {
private mode: TextProcessingMode = TextProcessingMode.Text;
private result: string[] = [];
private codeSample: string[] = [];
processText(lines: string[]): string[] {
this.result = []
this.mode = TextProcessingMode.Text
// 处理文本文档意味着处理每行文本,并返回结果字符串数组
for (let line of lines) {
this.processLine(line)
}
return this.result
}
private processLine(line: string): void {
switch(this.mode) {
case TextProcessingMode.Text:
this.processTextLine(line);
break;
case TextProcessingMode.Marker:
this.processMarkerLine(line);
break;
case TextProcessingMode.Code:
this.processCodeLine(line)
break;
}
}
// 处理一行文本,如果本行以 `<!--`开头,则加载代码示例,并转移到下一个状态
private processTextLine(line: string): void {
this.result.push(line)
if (line.startsWith('<!--')) {
this.loadCodeSample(line);
this.mode = TextProcessingMode.Marker
}
}
private processMarkerLine(line: string): void {
this.result.push(line)
if (line.startsWith('```ts')) {
this.result = this.result.concat(this.codeSample)
this.mode = TextProcessingMode.code
}
}
private processCodeLine(line: string): void {
if (line.startsWith('```')) {
this.result.push(line)
this.mode = TextProcessingMode.Text
}
}
private loadCodeSample(line: string) {
// load sample based on marker, store in this codeSample
}
}
  1. 另一种完全由函数实现的状态机,不需要在内部跟踪状态也不需要枚举

缺点:1. 需要为每种状态关联更多信息;2. 可能更需要显示声明的状态和转移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class TextProcessor {
private result: string[] = []
private processLine: (line: string) => void = this.processTextLine
private codeSample: string[] = []
processText(lines: string[]): string[] {
this.result = []
this.processLine = this.processTextLine
for (let line of lines) {
this.processLine(line)
}
return this.result
}
private processTextLine(line: string): void {
this.result.push(line)
if(line.startsWith('<!--')) {
this.loadCodeSample(line)
this.processLine = this.processMarkerLine; // 通过把 this.processLine更新为合适的方法来实现状态转移:一种策略模式?
}
}
private processMarkerLine(line: string): void {
this.result.push(line)
if(line.startsWith('```ts')) {
this.processLine = this.processCodeLine; // 通过把 this.processLine更新为合适的方法来实现状态转移:一种策略模式?
}
}
private processCodeLine(line: string): void {
if(line.startsWith('```ts')) {
this.result.push(line)
this.processLine = this.processTextLine; // 通过把 this.processLine更新为合适的方法来实现状态转移:一种策略模式?
}
}
private loadCodeSample(line: string) {
// load sample based on marker, store in this codeSample
}
}
  1. 使用和类型的状态机

缺点:代码量较多

习题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 1. 一个具有open, close2种状态简单连接为一个状态机,使用 connect 打开,使用 disconnect关闭
enum State {
open = 'OPEN',
closed = 'CLOSED'
}
class Connection {
private state: string = State.open
private processState: () => void = this.connect
private connect() {
this.state = State.open
this.processState = this.disconnect
}
private disconnect() {
this.state = State.closed
this.processState = this.connect
}
}
// 2. 使用 process方法将前面的连接实现为一个函数状态机,process 函数扭转状态

declare function read(): string;
class Connection {
private processState: () => void = this.disconnect
public process() {
this.processState()
}
private connect() {
const value: string = read()
if (!value) {
this.processState = this.disconnect
} else {
console.log(value)
}
this.processState = this.disconnect
}
private disconnect() {
this.processState = this.connect
}
}

使用延迟值避免高开销的计算

  1. 立即创建
1
2
3
4
5
6
7
8
9
10
11
class Bike {}
class Car {} // 假设创建 Car 开销较大
function chooseMyRide(bike: Bike, car: Car) Car | Bike {
if (isItRaining()) {
return car
} else {
return bike
}
}
// 调用时就创建Car
chososeMyRide(new Bike(), new Car())
  1. 延迟生成 car
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Bike {}
class Car {} // 假设创建 Car 开销较大
// 参数改为一个返回 Car 的函数
function chooseMyRide(bike: Bike, car: () => Car) Car | Bike {
if (isItRaining()) {
return car()
} else {
return bike
}
}
function makeCar(): Car {
return new Car()
}
// 调用时就创建Car
chososeMyRide(new Bike(), makeCar)

lambda 匿名函数

1
2
3
4
5
6
7
8
9
10
11
12
13
// 使用匿名函数
class Bike {}
class Car {} // 假设创建 Car 开销较大
// 参数改为一个返回 Car 的函数
function chooseMyRide(bike: Bike, car: () => Car) Car | Bike {
if (isItRaining()) {
return car()
} else {
return bike
}
}
// 调用时就创建Car
chososeMyRide(new Bike(), () => new Car())

使用 map,filter,reduce

map()

对每个值调用一个函数,返回结果集合

1
2
3
4
5
6
7
8
// 自制 map()
function map<T, U>(items: T[], func: (item: T) => U) : U[] {
let result: U[] = []
for (const item of items) {
result.push(func(item))
}
return result
}

filter()

丢弃某些元素,接受一个实参并返回一个 boolean 的函数也称为 谓词

1
2
3
4
5
6
7
8
9
10
// 自制 filter()
function filter<T>(items: T[], pred: (item: T) => boolean): T[] {
let result: T[] = []
for (const item of items) {
if (pred(item)) {
result.push(item)
}
}
return result
}

reduce()

将所有集合项合并为一个值

1
2
3
4
5
6
7
8
// 自制 reduce()
function reduce<T>(items: T[], op: (x: T, y: T) => T, init: T):T {
let result: T = init
for (const item of items) {
result = op(result, item)
}
return result
}

幺半群

抽象代数处理集合以及集合上的操作。 如果类型 T 的一个操作接受 2个 T 作为实参,并返回另一个 T, 即 (T, T) => T,则可以把该操作解释为 T 值集合上的一个操作。例如:number 集合 +,即 (x, y) => x + y, 就构成了一个代数结构。
相关性:表明对元素序列应用操作的顺序并不重要,因为最终结果都是相同的。

如果集合 T 上的操作有一个操作元,并且具有相关性,那么得到的代数结构称为 幺半群

习题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 实现一个 first 函数,返回数组的第一个满足 谓词的元素,如果数组为空,则返回undefined, 功能与 find 相似
function first<T>(items: T[], pred: (item: T) => boolean): T | undefined {
if (!items || items.length) return undefined
for (const item of items) {
if (pred(item)) {
return item
}
}
return undefined
}
// all<T>(items: T[], pred: (item: T) => boolean): boolean
function all<T>(items: T[], pred: (item: T) => boolean): boolean {
if (!items || items.length) return undefined
for (const item of items) {
if (!pred(item)) {
return false
}
}
return true
}

第六章 - 函数类型的高级应用

一个简单的装饰器模式

可扩展对象的 行为,而不必修改对象的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Widget {}
// 接口
interface IWidgetFactory {
makeWidget(): Widget;
}
// 实现接口
// 如果想获取多个实例,可以直接使用 WidgetFactory
class WidgetFactory implements IWidgetFactory {
public makeWidget(): Widget {
return new Widget() // WidgetFactory 只是创建一个新的 Widget
}
}
// 装饰器,负责单例行为
// 如果想获取单个实例,使用SingletonDecorator
class SingletonDecorator implements IWidgetFactory {
private factory: IWidgetFactory
private instance: Widget | undefined = undefined
constructor(factory: IWidgetFactory) {
this.factory = factory
}
public makeWidget(): Widget {
if (!this.instance) {
this.instance = this.factory.makeWidget() // makeWidget() 实现了单例逻辑,并确保只会创建一个 Widget
}
}
}

函数装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Widget {}
type WidgetFactory = () => Widget;
function makeWidget(): Widget {
return new Widget()
}
function singletonDecorator(factory: WidgetFactory): WidgetFactory {
let instance: Widget | undefined = undefined;
// singletonDecorator() 返回一个 lambda, 该 lambda 实现了单例行为,并使用给定工厂创建一个 Widget
return (): Widget => {
if (!instance ) {
instance = factory()
}
return instance
}
}
function use10Widgets(factory: WidgetFactory) {
for(let i = 0;i<10;i++) {
let widget = factory()
}
}
use10Widgets(singletonDecorator(makeWidget))

闭包

lambda捕获singletonDecorator() 尽管返回一个 lambda,但该 lambda引用了 factory 实参和 instance 变量,而该对象是 singletonDecorator()函数的局部变量

比较对象和闭包:
对象:对象代表一组方法的某个状态;闭包:代表捕获到某个状态的函数

习题

1
2
3
4
5
6
7
8
9
10
11
12
class Widget {}
type WidgetFactory = () => Widget;
function factory() {
return new Widget()
}
function loggingDecorator(factory: WidgetFactory): WidgetFactory {
// 返回一个 lambda 延迟调用
return () => {
console.log('Widget created')
return factory()
}
}

实现一个计数器

一个面向对象的计数器

1
2
3
4
5
6
class Counter {
private n: number = 1; // 禁止被外部访问修改
next(): number {
return this.n++
}
}

使用闭包实现的计数器

1
2
3
4
5
type Counter = () => number;
function makeCounter(): Counter {
let n: number = 1
return () => n++
}

可恢复的计数器

跟踪自己状态的函数,调用时,不会从头开始运行,而是从上一次返回时的状态恢复执行;在 TS 中不使用 return 关键字来退出函数,而是使用 yield: 2个限制:1. 必须将函数声明为一个生成器; 2. 并且其返回类型必须是可叠戴的迭代器;

1
2
3
4
5
6
7
8
function* counter(): IterableIterator<number> {
let n: number = 1;
while(true) {
yield n++
}
}
let counter1: IterableIterator<number> = counter()
counter1.next()

习题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 使用 闭包创建函数 返回斐波那契数列中的下一个数字
function fib1(): () => number {
let a: number = 0;
let b: number = 0;
return () => {
let next: number = a
a = b
b = b + next
return next
}
}
// 使用生成器创建函数 返回斐波那契数列中的下一个数字
function* fib2(): IterableIterator<number> {
let a: number = 0;
let b: number = 0;
while(true) {
let next: number = a
a = b
b = b + next
return next
}
}

异步执行的运行时间长的操作

同步执行

1
2
3
4
5
6
7
8
9
10
11
function greet(): void {
const readlineSync = require('readline-sync');
let name: string = readlineSync.question('what is your name')
console.log(`Hi ${name}`)
}
function weather(): void {
const open = require('open')
open('heeps://weather.com/')
}
greet(); // 先调用 greet
weather(); // 等待 greet 执行完毕

异步执行:回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function greet(): void {
// 使用异步 readline
const readline = require('readline')
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
rl.question('what is your name', (name: string) => {
console.log(`Hi ${name}`)
rl.close()
})
}
function weather(): void {
const open = require('open')
open('heeps://weather.com/')
}
greet(); // rl.question 会直接跳过先执行 剩下的 同步函数 weather,等回调通知
weather(); // 不必等待 greet 执行完毕

异步执行模型

  1. 在事件循环中倒数

优点:不需要同步;缺点:在 I/O 操作等待数据时排队效果很好,但是 CPU密集操作(复杂计算)仍然会造成阻塞。因为这种操作没有等待数据,而是需要CPU周期,对于这种任务线程的效果更好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type AsyncFunction = () => void;
let queue: AsyncFunction[] = []; // 队列将是一个函数数组
function countDown(counterId: string, fro: number): void {
console.log(`${counterId}: ${from}`)
if (from > 0) {
queue.push(() => countDown(counterId, from - 1))
}
}
queue.push(() => countDown('counter1', 4)) // 启动倒数1
queue.push(() => countDown('counter2', 4)) // 启动倒数2
while(queue.length > 0) {
let func: AsyncFunction = <AsyncFunction>queue.shift()
func()
}
// 输出
// counter1: 4
// counter2: 2
// counter1: 3
// counter2: 1
// counter1: 2
// counter2: 0
// counter1: 1
// counter1: 0

简单异步代码

链式回调很难阅读,嵌套太深

链接 promise

Promise: 将来某个时刻可用值的一个代理。Continuation: 在promise的结果可用后调用的函数称为 continuation;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
declare function getUserName(): Promise<string>;
declare function getUserBirthday(name: string): Promise<Date>;
declare function getUserEmail(birthday: Date): Promise<string>;
getUserName().then((name: string) => {
console.log(`hi ${name}!`)
return getUserBirthday(name)
}).then((birthday: Date) => {
const today: Date = new Date()
if (birthday.getMonth() === today.getMonth() && birthday.getDay() === today.getDay()) {
console.log('Happy birthday!')
return getUserEmail(birthday)
}
}).then((emain: string) => {
// 处理邮件
})

async/await

async 不会改变函数类型,只会将一个函数标记成异步函数,并允许在其中调用 await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用async/await
async function getUserData(): Promise<void> {
let name: string = await getUserName();
console.log(`hi ${name}!`);
let birthday: Date = await getUserBirthday(name)
const today: Date = new Date()
if (birthday.getMonth() === today.getMonth() && birthday.getDay() === today.getDay()) {
console.log('Happy birthday!')
let email: string = getUserEmail(birthday)
}
}

try {
getUserData()
} catch {
// 异常将从 await 调用抛出,在 try/catch 语句中捕获错误
}

What is Haskell?

When you are starting out in Haskell, you need keep these items in your mind:

  1. Purely function programming language: f(x) => g, if it is called twice with the same parameters x, it is guaranteed to return the same result g
  2. Lazy
  3. Statically Typed: It means type inference, you don’t explicitly state their type.

Run ghc’s interactive mode to Get a very basic feel for Haskell

Operators

  1. simple arithmetic

    +, -, *, /; if we want to have a negative number, it’s always best to surround it with parentheses. like 5 * (- 3)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    $ ghci
    GHCi, version 6.8.2: http://www.haskell.org/ghc/ :? for help
    Loading package base ... linking ... done.
    prelude>
    # +-*/
    $ ghci> 2 + 15
    17
    $ ghci> 49 * 100
    4900
    $ ghci> 5 / 2
    2.5
    $ ghci> 50 * 100 - 4999
    1
    # use parentheses to change it
    $ ghci> 50 * (100 - 4999)
    -244950
    $ ghci> 5 * - 3 # ❌
    $ ghci> 5 * (- 3) # ✅
    -15
  2. Boolean algebra: True, False, &&, ||, not

    equality: ==, /=. what about doing 5 + "llama" or 5 == True ? we will get a big error message! Don’t do that!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    $ ghci> True && False
    False
    $ ghci> True && True
    True
    $ ghci> False || True
    True
    $ ghci> not False
    True
    $ ghci> not (True && True)
    False
    $ ghci> 5 == 5
    True
    $ ghci> 1 == 0
    False
    $ ghci> 5 /= 5
    False
    $ ghci> 5 /= 4
    True
    # but we can do 5 + 4.0
    $ ghci> 5 + 4.0 # 5 can act like an integer or a floating-point number.
    9.0

function: prefix, infix

prefix function

prefix function like succ function takes anything that has a defined successor and return that successor. it means it can increase the parameter(like numbers).
also, The functions min and max take two things that can be put in an order(like numbers).

succ, min, max
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ghci> succ 8
9
# min returns the one that's lesser
ghci> min 9 10
9
ghci> min 3.4 3.2
3.2
# max return the one that's greater
ghci> max 100 101
101
# Function applications have the highest precedence, as reflected by the fact that when mixed with other operators
# What that means for us is that these two statements are equivalent.
ghci> succ 9 + max 5 4 + 1
16
ghci> (succ 9) + (max 5 4) + 1
16
# if we wanted to get the successor of the product of numbers 9 and 10, we couldn't write succ 9 * 10
ghci> succ 9 * 10
100
# It is equivalent to
ghci> (succ 9) * 10
100
# we have to write this statement to get 91
ghci> succ (9 * 10)
91

infix function

backticks: `

1
2
3
4
5
6
7
8
# division
# there maybe some confusion as to which number is doing the division and which one is being divided.
ghci> div 92 10
9
# we can call it as an infix function by surrounding it with backticks.
# it's much clearer
ghci> 92 `div` 10
9

重表单功能项目

整个项目80%都是填写表单,一开始没防备,生写新建和编辑,痛苦不堪,后来上了 antd 的 Form 表单组件好多了,但是时间紧迫改的又急,业务代码变的很混乱,先记录一下重构思路,声明:代码均已脱敏。

示例组件: 1个parent组件+3个child组件,其中,某个child还嵌套了2层,层级较深

1
2
3
4
// parent.tsx
// childA.tsx
// childB.tsx
// childC.tsx

页面卡顿,function 函数组件的UI更新问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// parent.tsx
import React, { useState, useEffect } from 'react'
export const Parent = () => {
const [count, setCount] = useState(0)
return <ChildA count={count} onchange={(c) => setCount(c)} />
}
// ChildA.tsx
export const ChildA = ({count, onChange}: {count: number; onChange: (c: number) => void;}) => {
useEffect(() => {

}, [count])
return (
<div onClick={() => }>You click it {count} times</div>
)
}

页面卡顿,children 组件被重复渲染: 使用 useMemo & useCallback & useReducer 替换 useState

页面卡顿,child 的 useEffect 狂刷页面,因为有依赖是多层嵌套引用对象类型

useEffect 内部接口请求的处理 + 外部请求 = 出现2个相同定义函数 = 冗余代码

组件组织结构优化

有2层嵌套的map渲染组件,原来的思路是EitherBox 内直接 map => AndBox,而 AndBox 内也 map =>(<div>xxx</div>)

原思路
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export const EitherBox = ({children: any}: props) => {
const style = {
ors: data.length
}
return <div className={style.ors}>{children.length > 1 && (<div>or</div>)}{children}</div>
}
export const AndBox = ({children: any}: props) => {
return <div>{children.length > 1 && (<div>and</div>)}{children}</div>
}

export default function pageA () => {
const data = getSomeData<Record<string, Record<string, unknown>[]>[]>() ?? []
return (
<EitherBox>
{data.length === 0 && (<div>add</div>)}
{data.length > 0 && data?.map(item => (
<AndBox>
{item.length === 0 && (<div>none</div>)}
{item.length > 0 && item?.map(v => (
<div onClick={}>{v}</div>
))}
</AndBox>
))}
</EitherBox>
)
}
优化后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 改下名称
export const EitherUnit = ({data: Record<string, Record<string, unknown>[]>[], children: TSX.element}: props) => {
const style = {
ors: data.length
}
return <div className={style.ors}>{children.length > 1 && (<div>or</div>)}{children}</div>
}
export const AndUnit = ({children: TSX.element}: props) => {
return <div>{children.length > 1 && (<div>and</div>)}{children}</div>
}
export const EitherMap = ({data: Record<string, Record<string, unknown>[]>[], children: TSX.element}: props) => {
if (!Array.isArray(data)) {
return <div>暂无</div>
}
if (data.length === 0) {
return <div>add</div>
}
return <EitherUnit data={data}>{children}</EitherUnit>
}
export const AndMap = ({data, children, ...rest}: props) => {
if (!Array.isArray(data)) {
return <div>input Error</div>
}
if (data.length === 0) {
return <div>none</div>
}

return (
<AndUnit>
{data.map(v => (
<div>{v}</div>
))}
</AndUnit>
)
}
export default function pageA () => {
const data = getSomeData<Record<string, Record<string, unknown>[]>[]>() ?? []
return (
<EitherMap data={data}>
{data?.map(item => (
<AndBox data={item} />
))}
</EitherMap>
)
}

使用 immer / immutable

类型复用逻辑

type && interface 的使用,考虑项目中类型,简短的可做基础类型的优先考虑 type, 浏览器请求接口的参数和返回值的类型使用 interface,函数&其他变量,集合类型 优先使用 type

type和interface 区别在于:inteface无法应用于union type | intersection type | conditional type | tuple,但是可以 augumented 而type不行

把类型当作值的集合,理解 类型中的 union type - 交集 和 intersection type -并集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// type
type AB = 'A' | 'B' // union type 并集
type NamedVariable = {age: number} & { name: string} // intersection type 交集
type mytuple = [number, string] // tuple元组
// inteface
// a.ts
export interface BaseInfo {
name :string;
age: number;
}
// b.ts
import { BaseInfo } from 'path/to/a.ts'
interface BaseInfo {
phoneNum: string
}
const base: BaseInfo = {
name: 'xx',
age: 21,
phoneNum: '19xxxxxxxxx'
}
// 把类型当作值的集合
type AA = {aa: 'A'}
type BB = {bb: 'B'}
type ABor = AA | BB
type ABand = AA & BB
type AAkeys = keyof AA // 'aa'
type BBkeys = keyof BB // 'bb'
type ABandKeys = keyof ABand // 'aa' | 'bb', keyof ABand === (keyof AAkeys) | (keyof BBkeys)
type ABorKeys = keyof ABor // never, keyof ABor === (keyof AAkeys) & (keyof BBkeys)

const aaa: ABand = {
aa: 'A',
bb: 'B'
}

useEffect: 基于 useMemo & useCallback 导致的更新问题

写Vue3写多了,初次写React项目很不适应,首先面临的一个大问题就是 useEffect 更新 child 组件,数次进入无限循环,把浏览器搞崩了。由于我本人首次写 React 上了强 debuff, 导致首次写的 react 项目业务代码一坨屎,本人痛定思痛,决定对业务代码脱敏后重构一次,以彰显写更棒的代码的决心

改进: 考虑使用 immer.js

改进后: 思考 Class 组件 🆚 函数组件 的便利性

Class & enum 横跨值&类型

其他:

  1. 使用 Vue3 实现相似功能

第一章 预言成真

炎帝之少女-女娃好游泳,已有西方上帝赠与的开光水晶球预示其将溺亡,其父及众人忧心忡忡勒令少女禁止下水,少女忤逆,于预言10年后在东海溺亡。

女娃死了,炎帝命人在三界搜寻其魂魄。最终在发鸠之山发现了新生的女娃,名曰精卫,可惜肉身损坏不能还魂。

起初,精卫只觉得轻盈了许多,视觉上也看得更高更广了,还能在水里自由的打水漂。过了几个月后,精卫发觉由于无法与家人朋友沟通觉得孤独,而他们却已经开始习惯女娃变成了一只鸟,甚至逗起来了。于是精卫开始觉得愤怒,痛苦,最终精卫大闹议事厅,在议事厅里对着亲人飚屎,尖叫,直至力竭离家出走,回到了发鸠之山。

回到了新生地后精卫到底也觉得没人伺候环绕更让自己难以忍受,只好到东海去扔石子发泄,这里是痛苦之源,只有把东海填平才能泄愤。被好事者告与炎帝,炎帝无可奈何。每日投2次,虽然不多,但毕竟是初为人鸟,然而看热闹的人越来越多,被围观者怂恿每日投掷3次甚至更多,紧接着已经无以为继,怒发冲冠,谁多说一句开始啄人,最后已经没有围观者。没有了围观者,精卫再一次经历了从愤怒到失望的情绪过山车后开始规律投石,心情好投2颗,心情不好只往海里拉泡屎,不仅自己拉,甚至在发鸠之山培养追随者一起拉屎。仇恨早已消失,此后在后悔,愤怒,失望,自闭,仇恨和平静中反复沉沦。

然而东海并没有因为这几颗石子被填满哪怕一立方米,这些投石和每日生物新生的消亡、吃喝拉撒以及泥石冲刷几乎可以忽略不计。

微不足道的石子等同于无效的复仇。然而精卫尚且不能理解这个道理。只能向炎帝发问:复仇并没有解决他的痛苦,到底他的痛苦来源是什么?
被扔进不周山的水晶球被炎帝又捡了回来,炎帝要求水晶球再做一则新的预言,要求显示如果女娃在第一个预言之后不再游泳后的命运,否则就会被扔进东海当成填海的边角料。

新预言的故事

女娃不再游泳,终日沉闷不语,在海边,湖边,河边一切有水的地方保持观望,仿佛下一秒就准备入水。不久开始生病,反反复复怎么都治不好,巫师问了情况,告诉炎帝在家里搞个大水桶,让女娃每日泡一泡,反正面积小高度小游不起来。女娃的病情开始有所缓解,但也终归没治好,就这样预言的10年之期很快过去,女娃离解禁的日子越来越近了。在最后一天女娃纵身一跃跳进了东海溺亡了。

没想到女娃选择自杀来反抗溺亡的命运,但是结局是没有变的,他最终依旧是在预言的时机死亡,反抗并没有作用。

水晶球

水晶球由于频繁干涉东方神话的独立发展(地狱事务)被谛听一脚踩碎了,形成了现代的溶洞景观,这是后话了。

痛苦的根源

水晶球的2个预言基本宣告了女娃溺死的既定命运,即使开了上帝视角。
无论是选择什么都逃脱不了的死亡。那么在第二则预言后为什么女娃是失去了反抗被预示的既定命运,假如他信上帝的水晶球,那么死前他不信他信上帝,而死后是他不信他不信上帝。终日悔恨,被剥夺了正常的生活,失去了掌控自己的人生的能力才是核心痛苦。

人生所有的可能性被水晶球抹去,再无让人探索的欲望,女娃不再是生机勃勃的女娃,变成了行尸走肉。那么,他的痛苦是来源于水晶球吗?

假如没有水晶球,他依旧无法避免溺亡的结局,但是在溺亡之前他的所剩人生里没有被死亡的恐惧所控制,活着的时候他是完全自由的。
没有水晶球也就没有新预言,他和家人还可以通过幻想「假如」来宽慰悲剧,安抚心灵。

水晶球的存在是人生痛苦的潘多拉魔盒,一旦知晓其存在就是悲剧,更遑论打开。可以说,人生少见 「种瓜的瓜」,几乎没有「只要」「就会」,更多的是「假如」「也许」,而潘多拉打开了就只剩下「倒霉」「恐惧」「悲惨」「解脱」「自杀」直到「最终失去一切」。

现代中国神话已经偏离了当时东方神话发展的路线,已经失去了培养神棍和巫婆的土壤,那么,我们就可以脱离水晶球的魔咒了吗?

显然不能,水晶球只是一个隐喻。预示着命运的大致走向和人生发展历程的隐喻。最显而易见的是慢性疾病,乃至癌症。意志的衰弱不一定会拖垮躯体,躯体脱离意志还能靠机器和药物续命,然而,躯体的衰弱几乎会拖垮意志,而意志无法脱离躯体而存在,最终走向共同死亡的结局。要说这两种有何不同,前者只包含一种单纯的痛苦,相当于电脑某个软件中了病毒,经过介入修复后可以康复和正常人无异,后者是多重痛苦,相当于电脑本身硬件出了问题,所有软件都得承受硬件卡顿崩溃的后果,最终不是更换硬件就是成为废弃的电子垃圾。

人当然不会成为一种废弃的电子垃圾,人只有一种结局一种命运:无可避免的死亡。区别在于,健康人的达摩克里斯剑或许永远不会到来,不健康的人24小时都在感受即将死亡的恐惧。

也许是明天,也许是下一刻,达摩克里斯斩下的前1秒,人生只剩下恐惧,没有了其他色彩,其他可能性,带着枷锁,每天除了等待死亡就是无所事事。

女娃死后尚且还可以转生为鸟投石泄愤,结果却是微不足道的复仇,甚至无法从「某种痛苦中」解脱,普通人别无选择,只剩下惨烈的自杀。

「我永远不会选择自杀」- 加缪

自杀之所以称得上惨烈,是因为这是最大且唯一的赌注,赌上生命的代价来反抗当下的命运,而结局自己是永远无法知晓的,刹那间却承受了人生最大的痛苦和一瞬间失去了全部的可能性。悲剧就在于,无法知晓反抗的结局,甚至生者也无法断定反抗是有效的。

无法判定的反抗,甚至包含反抗的结局,对于反抗本身来说是微不足道的,对于自己的命运来说却是毁灭性的。为了反抗既定命运,最终,却失去了一切。如加缪所说,自杀最终是无效的。