一、函数:函数的类型签名
(1)函数的类型就是描述了函数入参类型与函数返回值类型,使用:
的语法进行类型标注。
function foo(name: string): number {
return name.length;
}
(2)函数类型声明混合箭头函数声明时,代码的可读性会非常差。因此,一般不推荐这么使用,要么直接在函数中进行参数和返回值的类型声明,要么使用类型别名将函数声明抽离出来
// 方式一
const foo = (name: string): number => {
return name.length
}
// 方式二
const foo: (name: string) => number = (name) => {
return name.length
}
type FuncFoo = (name: string) => number
const foo: FuncFoo = (name) => {
return name.length
}
(3)如果只是为了描述这个函数的类型结构,我们甚至可以使用 interface 来进行函数声明
interface FuncFooStruct {
(name: string): number
}
这时的 interface 被称为 Callable Interface,看起来可能很奇怪,但我们可以这么认为,interface 就是用来描述一个类型结构的,而函数类型本质上也是一个结构固定的类型罢了
二、函数:void 类型
(1)在 TypeScript 中,一个没有返回值(即没有调用 return 语句)的函数,其返回类型应当被标记为 void 而不是 undefined,即使它实际的值是 undefined。
// 没有调用 return 语句
function foo(): void { }
// 调用了 return 语句,但没有返回值
function bar(): void {
return;
}
(2)在 TypeScript 中,undefined 类型是一个实际的、有意义的类型值,而 void 才代表着空的、没有意义的类型值。
三:函数:可选参数与rest参数
(1)在函数类型中我们也使用 ?
描述一个可选参数。
1、可选参数必须位于必选参数之后。
// 在函数逻辑中注入可选参数默认值
function foo1(name: string, age?: number): number {
const inputAge = age || 18; // 或使用 age ?? 18
return name.length + inputAge
}
// 直接为可选参数声明默认值
function foo2(name: string, age: number = 18): number {
const inputAge = age;
return name.length + inputAge
}
2、 也可以直接将可选参数与默认值合并,但此时就不能够使用 ?
了,因为既然都有默认值,那肯定是可选参数。
function foo(name: string, age: number = 18): number {
const inputAge = age || 18;
return name.length + inputAge
}
3、在某些情况下,这里的可选参数类型也可以省略,如这里原始类型的情况可以直接从提供的默认值类型推导出来。但对于联合类型或对象类型的复杂情况,还是需要老老实实地进行标注
(2)rest 参数的类型标注也比较简单,由于其实际上是一个数组,这里我们也应当使用数组类型进行标注。
function foo(arg1: string, ...rest: any[]) { }
四、函数:重载
(1)要想实现与入参关联的返回值类型,我们可以使用 TypeScript 提供的函数重载签名(Overload Signature)。
function func(foo: number, bar: true): string;
function func(foo: number, bar?: false): number;
function func(foo: number, bar?: boolean): string | number {
if (bar) {
return String(foo);
} else {
return foo * 599;
}
}
const res1 = func(599); // number
const res2 = func(599, true); // string
const res3 = func(599, false); // number
这里我们的三个
function func
其实具有不同的意义:
function func(foo: number, bar: true): string
,重载签名一,传入 bar 的值为 true 时,函数返回值为 string 类型。function func(foo: number, bar?: false): number
,重载签名二,不传入 bar,或传入 bar 的值为 false 时,函数返回值为 number 类型。function func(foo: number, bar?: boolean): string | number
,函数的实现签名,会包含重载签名的所有可能情况。
1、基于重载签名,我们就实现了将入参类型和返回值类型的可能情况进行关联,获得了更精确的类型标注能力。
2、这里有一个需要注意的地方,拥有多个重载声明的函数在被调用时,是按照重载的声明顺序往下查找的。因此在第一个重载声明中,为了与逻辑中保持一致,即在 bar 为 true 时返回 string 类型,这里我们需要将第一个重载声明的 bar 声明为必选的字面量类型
3、实际上,TypeScript 中的重载更像是伪重载,它只有一个具体实现,其重载体现在方法调用的签名上而非具体实现上。而在如 C++ 等语言中,重载体现在多个名称一致但入参不同的函数实现上,这才是更广义上的函数重载。
五、异步函数、Generator 函数等类型签名
(1) 对于异步函数、Generator 函数、异步 Generator 函数的类型签名,其参数签名基本一致,而返回值类型则稍微有些区别。
async function asyncFunc(): Promise<void> {}
function* genFunc(): Iterable<void> {}
async function* asyncGenFunc(): AsyncIterable<void> {}
1、其中,Generator 函数与异步 Generator 函数现在已经基本不再使用,这里仅做了解即可。
2、而对于异步函数(即标记为 async 的函数),其返回值必定为一个 Promise 类型,而 Promise 内部包含的类型则通过泛型的形式书写,即 Promise<T>。
六、 Class:类与类成员的类型签名
(1)Class 主要结构只有构造函数、属性、方法和访问符(Accessor)。
(2) 属性的类型标注类似于变量,而构造函数、方法、存取器的类型编标注类似于函数。
class Foo {
prop: string;
constructor(inputProp: string) {
this.prop = inputProp;
}
print(addon: string): void {
console.log(`${this.prop} and ${addon}`)
}
get propA(): string {
return `${this.prop}+A`;
}
set propA(value: string) {
this.prop = `${value}+A`
}
}
(3) 唯一需要注意的是,setter 方法不允许进行返回值的类型标注,你可以理解为 setter 的返回值并不会被消费,它是一个只关注过程的函数。类的方法同样可以进行函数那样的重载,且语法基本一致。
(4)就像函数可以通过函数声明与函数表达式创建一样,类也可以通过类声明和类表达式的方式创建。很明显上面的写法即是类声明,而使用类表达式的语法则是这样的。
const Foo = class {
prop: string;
constructor(inputProp: string) {
this.prop = inputProp;
}
print(addon: string): void {
console.log(`${this.prop} and ${addon}`)
}
// ...
}
七、Class:修饰符
(1) 在 TypeScript 中我们能够为 Class 成员添加这些修饰符:public
/ private
/ protected
/ readonly
。除 readonly 以外,其他三位都属于访问性修饰符,而 readonly 属于操作性修饰符(就和 interface 中的 readonly 意义一致)。
(2)这些修饰符应用的位置在成员命名前
class Foo {
private prop: string;
constructor(inputProp: string) {
this.prop = inputProp;
}
protected print(addon: string): void {
console.log(`${this.prop} and ${addon}`)
}
public get propA(): string {
return `${this.prop}+A`;
}
public set propA(value: string) {
this.propA = `${value}+A`
}
}
- public:此类成员在类、类的实例、子类中都能被访问。
- private:此类成员仅能在类的内部被访问。
- protected:此类成员仅能在类与子类中被访问,你可以将类和类的实例当成两种概念,即一旦实例化完毕(出厂零件),那就和类(工厂)没关系了,即不允许再访问受保护的成员。
(3) 当你不显式使用访问性修饰符,成员的访问性默认会被标记为 public。实际上,在上面的例子中,我们通过构造函数为类成员赋值的方式还是略显麻烦,需要声明类属性以及在构造函数中进行赋值。简单起见,我们可以在构造函数中对参数应用访问性修饰符
class Foo {
constructor(public arg1: string, private arg2: boolean) { }
}
new Foo("linbudu", true)
八、Class:静态成员
(1) 在 TypeScript 中,你可以使用 static 关键字来标识一个成员为静态成员:
class Foo {
static staticHandler() { }
public instanceHandler() { }
}
(2)不同于实例成员,在类的内部静态成员无法通过 this 来访问,需要通过 Foo.staticHandler
这种形式进行访问。我们可以查看编译到 ES5 及以下 target 的 JavaScript 代码(ES6 以上就原生支持静态成员了),来进一步了解它们的区别。
var Foo = /** @class */ (function () {
function Foo() {
}
Foo.staticHandler = function () { };
Foo.prototype.instanceHandler = function () { };
return Foo;
}());
1、静态成员直接被挂载在函数体上,而实例成员挂载在原型上,这就是二者的最重要差异。
2、静态成员不会被实例继承,它始终只属于当前定义的这个类(以及其子类)。
3、原型对象上的实例成员则会沿着原型链进行传递,也就是能够被继承。
九、Class:继承、实现、抽象类
-
继承
(1) TypeScript 中使用 extends 关键字来实现继承
class Base { }
class Derived extends Base { }
1、 对于这里的两个类,比较严谨的称呼是 基类(Base) 与 派生类(Derived)。
2、关于基类与派生类,我们需要了解的主要是派生类对基类成员的访问与覆盖操作。
(2)基类中的哪些成员能够被派生类访问,完全是由其访问性修饰符决定的。派生类中可以访问到使用 public
或 protected
修饰符的基类成员。除了访问以外,基类中的方法也可以在派生类中被覆盖,但我们仍然可以通过 super 访问到基类中的方法
class Base {
print() { }
}
class Derived extends Base {
print() {
super.print()
// ...
}
}
(3)在派生类中覆盖基类方法时,我们并不能确保派生类的这一方法能覆盖基类方法,万一基类中不存在这个方法呢?所以,TypeScript 4.3 新增了 override
关键字,来确保派生类尝试覆盖的方法一定在基类中存在定义
class Base {
printWithLove() { }
}
class Derived extends Base {
override print() {
// ...
}
}
在这里 TS 将会给出错误,因为尝试覆盖的方法并未在基类中声明。通过这一关键字我们就能确保首先这个方法在基类中存在,同时标识这个方法在派生类中被覆盖了。
-
抽象类
(1)抽象类是对类结构与方法的抽象,简单来说,一个抽象类描述了一个类中应当有哪些成员(属性、方法等),一个抽象方法描述了这一方法在实际实现中的结构。
abstract class AbsFoo {
abstract absProp: string;
abstract get absGetter(): string;
abstract absMethod(name: string): string
}
注意,抽象类中的成员也需要使用 abstract 关键字才能被视为抽象类成员
(2) 实现(implements)一个抽象类
class Foo implements AbsFoo {
absProp: string = "linbudu"
get absGetter() {
return "linbudu"
}
absMethod(name: string) {
return name
}
}
此时,我们必须完全实现这个抽象类的每一个抽象成员。需要注意的是,在 TypeScript 中无法声明静态的抽象成员。
(3)interface 不仅可以声明函数结构,也可以声明类的结构
interface FooStruct {
absProp: string;
get absGetter(): string;
absMethod(input: string): string
}
class Foo implements FooStruct {
absProp: string = "linbudu"
get absGetter() {
return "linbudu"
}
absMethod(name: string) {
return name
}
}
类去实现了一个接口。这里接口的作用和抽象类一样,都是描述这个类的结构。
(4)还可以使用 Newable Interface 来描述一个类的结构(类似于描述函数结构的 Callable Interface)。
class Foo { }
interface FooStruct {
new(): Foo
}
declare const NewableFoo: FooStruct;
const foo = new NewableFoo();
十、SOLID 原则
SOLID 原则是面向对象编程中的基本原则,它包括以下这些五项基本原则。
S,单一功能原则,一个类应该仅具有一种职责,这也意味着只存在一种原因使得需要修改类的代码。如对于一个数据实体的操作,其读操作和写操作也应当被视为两种不同的职责,并被分配到两个类中。更进一步,对实体的业务逻辑和对实体的入库逻辑也都应该被拆分开来。
O,开放封闭原则,一个类应该是可扩展但不可修改的。即假设我们的业务中支持通过微信、支付宝登录,原本在一个 login 方法中进行 if else 判断,假设后面又新增了抖音登录、美团登录,难道要再加 else if 分支(或 switch case)吗?
L,里式替换原则,一个派生类可以在程序的任何一处对其基类进行替换。这也就意味着,子类完全继承了父类的一切,对父类进行了功能地扩展(而非收窄)。
I,接口分离原则,类的实现方应当只需要实现自己需要的那部分接口。比如微信登录支持指纹识别,支付宝支持指纹识别和人脸识别,这个时候微信登录的实现类应该不需要实现人脸识别方法才对。这也就意味着我们提供的抽象类应当按照功能维度拆分成粒度更小的组成才对。
D,依赖倒置原则,这是实现开闭原则的基础,它的核心思想即是对功能的实现应该依赖于抽象层,即不同的逻辑通过实现不同的抽象类。还是登录的例子,我们的登录提供方法应该基于共同的登录抽象类实现(LoginHandler),最终调用方法也基于这个抽象类,而不是在一个高阶登录方法中去依赖多个低阶登录提供方。