类(Class)
ES6 中:基于原型继承的语法糖
JavaScript是一门基于原型
的语言,通过原型继承,我们可以实现类的一些性质
原型与原型链
如何实现方法和属性的继承?
错误的方法 ❎
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
function Person(name, age) {
this.name = name;
this.age = age;
}
function Student(grade, name, age) {
this.name = name;
this.age = age;
this.grade = grade;
}
Person.prototype.sayHello = function () {
console.log(`Hello, my name is ${this.name}`);
};
|
🚫 声明冗余,并且不会继承Person
的原型方法
正确方法 ✅
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
function Person(name, age) {
this.name = name;
this.age = age;
}
function Student(grade, name, age) {
Person.call(this, name, age);
this.grade = grade;
}
Person.prototype.sayHello = function () {
console.log(`Hello, my name is ${this.name}`);
};
// 子类继承父类
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Person;
|
⚠️ 需要让Student
的原型也继承Person
的原型,并让Student
原型对象的构造函数也指向Person
,从而完整构建出一条原型链
使用类(Class)的语法糖会让代码更具可读性和可维护性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(`Hello, my name is ${this.name}`);
}
}
class Student extends Person {
constructor(grade, name, age) {
super(name, age);
this.grade = grade;
}
}
|
上述两种代码的效果是一样的
静态属性(方法)
默认情况下,在类中不加任何关键字声明方法的都是公共方法,会定义在类的原型对象上,被其实例链式继承
使用static
关键字声明一个静态域
和公共域的区别:通过类本身访问和类实例访问
私有属性(方法)
有时候,我们不希望类的属性(方法)暴露给外部使用,这些属性(方法)往往仅仅会被使用在类的内部,此时我们可以通过声明类私有域
来进行对属性(方法)的私有化
ES2019
中增加了对class
私有属性的原生支持
使用#
(哈希前缀)声明一个私有属性(方法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
#_combineFullName() {
return this.firstName + " " + this.lastName;
}
getName() {
return this.#_combineFullName();
}
}
|
Q:不使用哈希前缀如何声明私有属性(方法)?
A:命名规定(自欺欺人法);使用闭包;将私有方法定义在类的外部;使用WeakMap
TypeScript 中:完备的类实现
我们可以直接使用public
、static
和private
来区别声明公共域、静态域和私有域
并且可以声明抽象类、使用readonly
修饰符以及装饰器等,更好地实现面向对象
示例:
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
class Person {
// 需要先声明示例属性的类型
firstName: string;
lastName: string;
age: number;
static readonly description: string = "Person";
constructor(firstName: string, lastName: string, age: number) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
private _combineFullName(): string {
return this.firstName + " " + this.lastName;
}
public getFullName(): string {
return this._combineFullName(); // 只能在类的内部访问到这个方法
}
static getDescription(): string {
return this.description; // 任何试图修改只读属性的操作都会报错
}
static getAge(person: Person): number {
return person.age;
}
}
class Student extends Person {
grade: number;
id: string;
private _info: string = "default info";
constructor(
firstName: string,
lastName: string,
age: number,
grade: number,
id: string
) {
super(firstName, lastName, age);
this.grade = grade;
this.id = id;
}
get info(): string {
return this._info;
}
set info(value: string) {
this._info = value;
}
}
const John = new Student("John", "Doe", 30, 9, "12345678");
/* 正确的操作 */
John.getFullName(); // "John Doe"
Person.getDescription(); // "Person"
Person.getAge(John); // 30
// 只能通过 `getter` 和 `setter` 操作私有属性
John.info = "new info";
console.log(John.info); // "new info"
/**
* John.getAge()
* 报错,因为getAge方法是静态方法,不能在类的实例上调用
*/
/**
* John.combineFullName()
* Person.combineFullName()
* 报错,因为combineFullName方法是私有方法,不能在类的外部调用
*/
|
泛型(generics)
泛型函数(方法)
1
2
3
4
5
6
7
8
|
function identity<T, U>(value: T, message: U): T {
console.log(message);
return value;
}
const res = identity<number, string>(21, "Hello");
console.log(res);
// Hello
// 21
|
在多数场景下,我们调用函数时并不需要手动将类型写出来,ts 的类型系统会自动进行类型的推断,并且不会报错。只有在 ts 无法推断类型时,我们才需要显式声明泛型变量的类型。
1
2
3
4
5
6
7
|
function identity<T, U>(value: T, message: U): T {
console.log(message);
return value;
}
// 写成这样也是完全可以的
const res = identity(21, "Hello");
console.log(res);
|
使用泛型可以防止类型的丢失
1
2
3
4
5
6
7
8
9
|
function identity(value: number | string, message: any) {
console.log(message);
return value;
}
const res = identity(21, "Hello");
// 报错:不能将类型 `string | number` 分配给类型 `number`
// 在这个过程中类型并没有收窄
const value: number = res;
|
既然使用了 ts,就要尽量避免使用 any,因为这样类型系统就没有了意义
1
2
3
4
5
6
7
|
function identity(value: any, message: any) {
console.log(message);
return value;
}
const res = identity(21, "Hello");
// 合法,ts无法判断res的类型,
const value: string = res;
|
泛型接口
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 IResult<T> {
code: number;
message: string;
data: T;
cache: boolean;
}
interface IUserInfo {
name: string;
age: number;
type: number;
}
// 类型别名
type IUserInfoResponse = IResult<IUserInfo>;
/* 等同于 {
code: number;
message: string;
data: {
name: string;
age: number;
type: number;
};
cache: boolean;
} */
|
泛型接口和泛型函数在定义时都可以指定缺省值
1
2
3
4
5
6
7
8
9
10
11
12
|
interface IResult<T = IUserInfo> {
code: number;
message: string;
data: T;
cache: boolean;
}
interface IUserInfo {
name: string;
age: number;
type: number;
}
|
在泛型函数定义中指定缺省值没有什么实际意义,因为 ts 本身就会根据传参来确定参数类型
泛型继承
和类一样,接口也可以进行类型的继承:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
interface IBaseType {
className?: string;
style?: string;
children?: ReactElement | ReactElement[];
}
interface IButtonProps extends IBaseType {
onClick?: () => void;
}
/** 等同于
* interface IButtonProps {
* className?: string;
* style?: string;
* children?: ReactElement | ReactElement[];
* onClick?: () => void;
* }
*/
|
热知识:TypeScript 的类型系统是图灵完备的
拓展阅读:探究 typescript 类型元编程
而泛型变量也可以进行继承:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
interface IPerson {
name: string;
age: number;
}
function sayHi<T extends IPerson>(o: T): void {
console.log("Hi, I'm " + o.name);
// 只能提示 `name` 和 `age` 两个成员
}
const obj = {
name: "Jack",
age: 20,
info: "info",
};
sayHi(obj); // 即使 `obj` 上多了 `info` 成员也是合法的
|
泛型变量继承了IPerson
接口后,就可以对传参obj
进行类型约束和成员提示了
如果已经声明了一个对象,也可以通过typeof
关键字直接获取对象的类型:
1
2
3
4
5
6
7
8
|
const obj = {
name: "Jack",
age: 20,
};
function sayHi<T extends typeof obj>(o: T): void {
console.log("Hi, I'm " + o.name);
}
|
泛型关键字
-
extends
:继承
-
typeof
:获取变量的类型
-
keyof
:获取一个对象接口的所有 key 值
1
2
3
4
5
6
7
|
interface IPerson {
name: string;
age: number;
gender: number;
}
type K = keyof IPerson; // "name" | "age" | "gender"
|
使用keyof
实现pluck
函数
1
2
3
4
5
6
7
8
9
10
11
|
const obj = {
name: "Jack",
age: 20,
info: "info",
};
function pluck<T, K extends keyof T>(obj: T, names: K[]): Array<T[K]> {
return names.map(name => obj[name]);
}
pluck(obj, ["name", "age"]); // ['Jack', 20]
|
in
:遍历枚举类型
1
2
3
4
|
type Keys = "a" | "b";
type Obj = {
[p in Keys]: any;
}; // { a: any, b: any }
|
infer
:推断出最能够匹配extends
左边接受的类型(最接近的类型)
1
2
3
|
type Flatten<T> = T extends Array<infer R> ? R : T;
type strArr = Flatten<Array<string>>; // string 类型
type numArr = Flatten<Array<"123">>; // '123' 类型
|
工具泛型
keyof
可以获取类型中的所有键名,而in
可以对枚举类型进行遍历,因此两者联合使用就可以遍历所有键名了
Partial
:将传入的属性变为可选项
1
2
3
|
type Partial<T> = {
[P in keyof T]?: T[P];
};
|
其中,keyof T
拿到T
中的所有键名,然后in
进行遍历,将值赋给P
,最后T[P]
取得相应属性值。
Required
:将传入的属性变为必选项
1
2
3
|
type Required<T> = {
[P in keyof T]-?: T[P];
};
|
其中,-?
表示将可选项的?
去掉,将其改为+?
则可以将所有属性改为可选项
Readonly
:将传入的属性变为只读选项
1
2
3
|
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
|
此外还有Record
、Pick
、Exclude
等诸多工具泛型,这些都是官方帮我们封装好的,可以开箱即用。
类型断言
有时候你比编译器更了解某个变量的类型,因此 ts 给你提供了覆盖其推断的能力,即断言。在初始化数据的时候,我们通常会使用类型断言。
1
2
3
4
5
6
7
|
interface ICourseInfo {
courseName: string;
courseId: number;
location: string;
}
const courseInfo = {} as ICourseInfo;
|
在Vue3
中,Props
的类型约束就依赖类型断言实现
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
|
import { defineComponent, PropType } from "vue";
interface Book {
title: string;
author: string;
year: number;
}
const Component = defineComponent({
props: {
name: String,
id: [Number, String],
success: { type: String },
callback: {
type: Function as PropType<() => void>,
},
book: {
type: Object as PropType<Book>,
required: true,
},
metadata: {
type: null, // metadata 的类型是 any
},
},
});
|
在下面的例子中,原始数据在经过groupBy
后丢失了类型,因此在下面我们需要使用类型断言,将dayCourses
强制认定为ICourseDetails[]
,这样才能够对其调用forEach
方法,并且循环中的course
也自动推断出了ICourseDetails
的类型,并且会进行提示。
这就使得开发者在繁杂的逻辑中,可以从 IDE 层面规避不必要的 bug,这也是 ts 诞生的一个重要原因
1
2
3
4
5
6
7
8
|
const groupByWeekDay = groupBy(courses, "WeekDay");
// 对每一天进行遍历
for (let [day, dayCourses] of Object.entries(groupByWeekDay)) {
// 用于存放每一天分组后的课程
const groupBySection = [] as ICourseDetails[][];
// 取出 `dayCourses` 中的每一节课,将其与已分组的 `group` 进行判断
(dayCourses as ICourseDetails[]).forEach((course, index) => {});
}
|
可选链与空值合并
在写 js 的时候,访问对象属性是一件有风险的事情,有时我们并不能确定我们一定可以访问到这个值,不然的话就会报熟悉的Cannot Read Property of Undefined
。
在大多数情况下我们可能会使用:
1
|
const result = obj && obj.name;
|
来解决,但当数据层级很深的情况下,我们就需要做一层一层的分支判断了:
1
|
store.user.product.name;
|
1
2
3
|
console.log(
store && store.user && store.user.product && store.user.product.name
);
|
这样的代码逻辑冗余而且十分丑陋,好在我们现在可以使用可选链操作符
来简化操作,只需要在.
前加一个?
即可
1
|
store?.user?.product?.name;
|
这样一来,如果中间的链条在任何一处断开,都不会报错,而只会短路返回一个undefined
,那么结合空值合并运算符
就可以写出这样的代码了
1
|
store?.user?.product?.name ?? "default name";
|
这里的??
表示,当左边的表达式为null
或者undefined
时,整个式子取右边的值,比常规的||
要更加精确。
可选链支持访问对象的所有属性,这意味着你可以对数组和方法都写出这样的判断
1
2
|
props?.products?.[1];
props?.getName?.();
|
题外话:在一次 pr 中,尤雨溪回复了为什么不在框架源码中使用可选链操作符,原因是在 ts 中使用可选链会被编译成更复杂的语句。
在大型框架的设计中,打包体积是不得不考虑的一个问题,但在我们的日常开发中,放心用就行啦。
非空断言
有时,我们可以确定某个对象的属性一定不为空,例如我们常常会在Axios
的拦截器中设置headers
,ts 往往会提示,对象可能未定义
,但事实上我们知道它一定已经定义了,所以我们可以在其后加上!
来断言非空,从而给它赋值
1
2
3
4
|
Axios.interceptors.request.use((config: AxiosRequestConfig) => {
config.headers!.Authorization = `Bearer ${localStorage.token}`;
return config;
});
|
但如果要贯彻函数式编程的思想,这样写也许会更优雅:
1
2
3
4
5
6
7
|
Axios.interceptors.request.use((config: AxiosRequestConfig) => {
config.headers = {
...config.headers,
Authorization: `Bearer ${localStorage.token}`,
};
return config;
});
|