面向对象之 class(上)

思考题:写入属性时会覆盖共有属性吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype = {
constructor: Person,
sayHi(target) {
console.log(`你好,${target.name},我是 ${this.name}`)
}
}
const p0 = new Person("ClariS", 17)
const p1 = new Person("vivy", 18)
const p2 = new Person("k423", 19)
p1.sayHi = function (target) {
console.log(`我是 ${target.name} `)
}
p2.sayHi(p0)
// 1. 你好,ClariS,我是 k423
// 2. 我是 ClariS

上面代码的打印结果是 1 还是 2 ?

答案是 1,当我们向实例对象上写入属性时,并不会覆盖原型上的共有属性

我们把实例对象的结构打印出来看看

可以看到,读取与写入的规则是不一样的

  • 读取 p0p1p2sayHi 属性时,会先看自身上的独有属性中是否存在 sayHi,再去看原型上的共有属性;
  • 而当向 p0p1p2 上写入属性时,会直接把属性写入到实例对象上,并不会覆盖原型上的 sayHi 方法。

interface 和 class 的区别

interfaceclass 都是用来描述对象的

1
2
3
4
interface PointInterface {}

class PointClass {}
const p = new PointClass();

tsconfig.json 中设置 "strictPropertyInitialization": false

1
2
3
4
5
6
7
8
9
10
11
12
13
interface PointInterface {
x: number;
y: number;
}

// 不报错
class PointClass {
x: number;
y: number;
}
const p = new PointClass();
p.x = 1;
p.y = 1;

tsconfig.json 中设置 "strictPropertyInitialization": true"strict": true

1
2
3
4
5
6
7
8
9
10
interface PointInterface {
x: number;
y: number;
}

// 报错:Property 'x' has no initializer and is not definitely assigned in the constructor
class PointClass {
x: number;
y: number;
}

区别:interface 只有成员的类型没有实现,class 须同时有成员的类型和实现

class 的 4 种初始化方法

① 声明类型并给初始值

1
2
3
4
class PointClass {
x: number = 0;
y: number = 0;
}

② 只给初始值,类型让 TS 自动推断

1
2
3
4
class PointClass {
x = 0;
y = 0;
}

③ 声明类型,并在构造函数中给初始值

1
2
3
4
5
6
7
8
class PointClass {
x: number;
y: number;
constructor() {
this.x = 0;
this.y = 0;
}
}

④ 断言,告诉 TS 别检查,我自己一定能保证初始值存在

1
2
3
4
5
6
7
8
9
10
11
12
class PointClass {
x!: number;
y!: number;
constructor() {
this.init();
}
// 当我们进行初始化操作,但初始化操作不在 constructor 中时,TS 是检测不出来的,因此需要进行断言
init() {
this.x = 0;
this.y = 0;
}
}

class 的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
class Point {
x: number;
y: number;

constructor(x = 0, y = 0) {
// 生成实例对象时才会执行
this.x = x;
this.y = y;
}
}
const p = new Point()
console.log(p.x, p.y)

public 缩写

可以使用以下的缩写语法来代替上述写法,和上述的写法是完全等价的

1
2
3
4
5
6
class Point {
constructor(public x = 0, public y = 0) {
}
}
const p = new Point()
console.log(p.x, p.y)

不写初始值也不报错

1
2
3
4
5
6
class Point {
constructor(public x?: number, public y?: number) {
}
}
const p = new Point()
console.log(p.x, p.y)

结合函数重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Point {
x!: number;
y!: number;

constructor(x: number, y: number);
constructor(s: string);

constructor(xs: number | string, y?: number) {
if (typeof xs === 'number' && typeof y === 'number') {
this.x = xs
this.y = y
} else if (typeof xs === 'string') {
const parts = xs.split(',')
this.x = parseFloat(parts[0])
this.y = parseFloat(parts[1])
}
}
}

const p = new Point('1,2')
console.log(p.x, p.y)

结合索引签名

1
2
3
4
5
6
7
8
9
10
11
class Hash {
[s: string]: unknown

set(key: string, value: unknown) {
this[key] = value
}

get(key: string) {
return this[key]
}
}

class 实现接口

实现一个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Person {
name: string
sayHi: (target: Person) => void
}

class User implements Person {
constructor(public name: string) {
}
sayHi(target: Person) {
console.log(`Hi ${target.name}`)
}
}
// 通过观察不难发现:
// 类 - 类 => 继承
// 类型 - 类型 => 继承
// 类 - 类型 => 实现

实现多个接口

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
interface Person {
name: string
sayHi: (target: Person) => void
}

interface Taggable {
tags: string[]
addTag: (tag: string) => void
removeTag: (tag: string) => void
}

class User implements Person, Taggable {
tags: string[] = []
constructor(public name: string) {
}
sayHi(target: Person) {
console.log(`Hi ${target.name}`)
}
addTag(tag: string) {
this.tags.push(tag)
}
removeTag(tag: string) {
const index = this.tags.indexOf(tag)
this.tags.splice(index, 1)
// this.tags = this.tags.filter(t => t !== tag)
}
}

即使接口中存在可选属性,也必须得在 class 中实现它,不然会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Person {
name: string
age?: number
sayHi: (target: Person) => void
}

class User implements Person {
constructor(public name: string) {
}
sayHi(target: Person) {
console.log(`Hi ${target.name}`)
}
}

const u = new User('ClariS')
u.age // Property 'age' does not exist on type 'User'

class 继承 class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
constructor(public name: string) { }
sayHi() {
console.log(`你好,我是${this.name}`);
}
}

class User extends Person {
constructor(public id: number, name: string) {
super(name)
}
login() { }
}

const u = new User(1, 'ClariS');

u.sayHi()
u.login()

注意:

  • 只能继承一个 class,无法同时继承多个 class
  • 在调用 super() 之前不能使用 this

重写函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person {
constructor(public name: string) {}
sayHi() {
console.log(`你好,我是${this.name}`);
}
}
class User extends Person {
constructor(public id: number, name: string) {
super(name);
}
login() {}
sayHi(target?: User) { // target 只能是可选
if (target === undefined) {
super.sayHi();
} else {
console.log(`你好,${target.name},我是 ${this.name}`);
}
}
}

注意函数参数类型之间的兼容关系,比如此处 PersonsyaHi 的参数为空,那么重写的 sayHi 函数的参数 target 只能是可选的

重写属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
friend?: Person;
constructor(public name: string, friend?: Person) {
this.friend = friend;
}
}
class User extends Person {
declare friend: User;
constructor(public id: number, name: string, friend?: User) {
super(name, friend);
}
}
const u1 = new User(1, 'ClariS');
const u2 = new User(1, 'vivy', u1);
u2.friend;

注意:declare 后面声明 friend 的类型必须是兼容父类中 friend 的类型的,比如此处 User 是兼容 Person


(●'◡'●)ノ♥