# JavaScript教程 - 9 面向对象
面向对象编程,是一种编程思想,简单的理解就是:先有模板,也就是类,根据模板去创建实例,也就是对象,然后使用对象来完成功能开发。
我们经常说面向过程编程和面向对象编程,面向过程编程关注的是实现功能的步骤,面向对象编程更关注的是谁来实现功能。
面向对象编程有3大特性:
- 封装
- 继承
- 多态
下面依次开始讲起。
# 9.2 封装
在前面我们创建类,在类中定义了属性和方法,通过属性和方法来对现实世界的事物进行抽象的描述。
一个事物有很多的属性和方法,但是并不是所有属性和方法都需要开放出来。例如我们定义了一个手机类,我们可以使用手机打电话、拍照等,但是我们并不关心手机电压,驱动信息,也不关心内存的分配,CPU的调度等,虽然这些都属于手机的属性和行为。
我们可以将用户不关心的属性和方法封装并隐藏起来,只给类内部的方法调用,例如上网会用到4G模块,但是不是由用户来使用4G模块,而是由手机上网的功能来调用4G模块,只开放用户直接使用的信息和功能。
那么回过头来看,什么是封装?
它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象的内部信息。如果要访问这些信息,需要通过该类所提供的方法来实现对内部信息的操作和访问。
# 1 私有属性和方法
那么怎么将不想暴露的属性和方法隐藏起来呢,就需要用到私有属性和私有方法。
定义私有属性和方法,使用 #
开头来定义。
举个栗子:
class Phone {
#voltage; // 私有属性需要显式声明
constructor(producer, voltage) {
this.producer = producer; // 生产商
this.#voltage = voltage; // 电压
}
// 定义公共方法
call() {
console.log("打电话");
this.#getRunVoltage(); // 在类内部可以调用私有方法
}
// 定义一个私有方法
#getRunVoltage() {
console.log("当前电压:" + this.#voltage);
}
}
let phone = new Phone("小米", 12);
console.log(phone.producer); // 小米,调用公有属性
// console.log(phone.#voltage); // 报错:私有变量,类外部无法访问
phone.call(); // 调用公有方法
// phone.#getRunVoltage(); // 报错,私有方法,类外部无法访问
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
- 上面定义了私有属性
#voltage
和私有方法#getRunVoltage()
,私有属性和私有方法只能在类内部的方法访问,在类的外部不能通过对象来调用。 - 需要注意,私有方法需要显式声明。
- 静态属性和方法,同样在属性和方法前添加
#
即可。
如果不想暴露的属性和方法可以定义为私有成员。私有属性只能在类内部的方法中调用,不能通过对象来调用。
# 2 getter和setter
我们前面说到,封装是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象的内部信息。如果要访问这些信息,需要通过该类所提供的方法来实现对内部信息的操作和访问。
我们一般会针对私有属性提供两个对应的方法,分别用来获取和设置对应的属性,这两个方法称为 getter 和 setter 方法。
举个栗子:
class Phone {
#voltage; // 私有属性需要显式声明
constructor(producer, voltage) {
this.producer = producer;
this.#voltage = voltage;
}
// voltage属性的getter
getVoltage() {
return this.#voltage;
}
// voltage属性的setter
setVoltage(voltage) {
// 还可以在设置的时候进行判断
if (voltage > 36 || voltage < 0) {
return;
}
this.#voltage = voltage;
}
}
let phone = new Phone('小米', 12);
phone.setVoltage(20); // 调用 setter 设置值
console.log(phone.getVoltage()); // 调用 getter获取值
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
- 在上面为私有属性
voltage
定义了 get 和 set 方法,get
方法以get
开头,后跟属性名,遵循方法命名规则。set
方法类似,get 方法有返回值,没有参数,set 方法接收参数,没有返回值。 - 我们还可以根据需求,在设置或获取属性值的时候,对属性值进行额外的处理,例如格式验证。
- 如果有多个私有属性,根据需要,可以为每个私有属性添加 getter 和 setter 。
通过 getter 和 setter 来访问私有属性和为私有属性赋值。这样做的话,严格控制了属性获取和设置的入口,如果通过 对象.属性
来修改,代码很多的时候完全会不知道在哪里修改了属性导致出现了问题。
上面的 getter 和 setter 在进行属性访问的时候,需要调用方法,有点麻烦。JavaScript 还提供了访问器属性方式的 getter 和 setter。
举个栗子:
class Phone {
#voltage; // 私有属性需要显式声明
constructor(producer, voltage) {
this.producer = producer;
this.#voltage = voltage;
}
// voltage属性的getter
get voltage() {
console.log('调用了getter');
return this.#voltage;
}
// voltage属性的setter
set voltage(voltage) {
console.log('调用了setter', voltage);
this.#voltage = voltage;
}
}
let phone = new Phone('小米', 12);
phone.voltage = 20; // 以属性的方式访问,其实调用的是voltage的setter方法
console.log(phone.voltage); // 调用voltage的getter方法
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- 在上面的代码中,通过
get或set 关键字 + 属性名
构成的 getter 和 setter,可以以属性访问的方式来调用方法。 phone.voltage = 20;
调用的是set voltage()
方法。
# 9.3 继承
在现实世界,有麻雀和鸽子,它们都属于鸟类,麻雀、鸽子和鸟类的关系是父类和子类的关系。
麻雀和鸽子都会飞,我们可以在麻雀类中定义一个飞的方法,在鸽子类中定义一个飞的方法,但是这两个飞的方法是一样的,都是用翅膀飞。那么我们可以直接在鸟类中定义一个飞的方法,让麻雀和鸽子类都继承这个鸟类,那么它们就拥有了飞的方法,就不用重复再定义了。
# 1 继承的语法
在 JS 中,使用extends
关键字实现继承。
class 子类名 extends 父类名 {
类内容
}
2
3
举个栗子:
麻雀继承鸟类,鸽子继承鸟类:
class Bird {
constructor(age) {
this.age = age;
}
fly() {
// 定义了一个飞的方法
console.log(`我${this.age}岁了,我会飞`);
}
tweet() {
// 定义了一个叫的方法
console.log("我会叫");
}
}
class Sparrow extends Bird {
// 定义一个麻雀类,继承自鸟类
}
class Pigeon extends Bird {
// 定义一个鸽子类,继承自鸟类
}
let sparrow = new Sparrow(1); // 创建一个麻雀对象
sparrow.fly();
sparrow.tweet();
let pigeon = new Pigeon(2); // 创建一个鸽子对象
pigeon.fly();
pigeon.tweet();
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
在上面的代码中,我们先定义了一个鸟类,然后在鸟类中定义了
age
属性 ,并定义了两个方法fly方法
和tweet方法
;然后定义类麻雀类,类中没有代码,但是继承鸟类,那么就拥有了鸟类的属性和方法。鸽子类也同样。
执行结果:
我1岁了,我会飞 我会叫 我2岁了,我会飞 我会叫
子类继承父类后,就拥有了父类中声明的属性和方法。但是注意,继承不能继承父类私有的成员变量和方法。如果父类的成员变量和属性不想被子类继承,可以设置为私有成员。
子类也会继承父类的静态属性和方法,父类的静态属性和方法
# 2 重写父类方法
所以继承父类就拥有了父类非私有的成员变量和方法,如果父类不是我想要的方法,我还可以进行覆盖。
例如,鸟类提供了叫的方法,但是我麻雀有自己的叫法,我要啾啾叫,那么我们可以重写父类的方法,实现自己的个性化。
class Bird {
constructor(age) {
this.age = age; // 鸟都有年龄
}
fly() {
console.log(`我${this.age}岁了,我会飞`);
}
tweet() {
console.log("我会叫");
}
}
// 麻雀类
class Sparrow extends Bird {
tweet() {
console.log("我会啾啾叫");
}
}
// 鸽子类
class Pigeon extends Bird {
// 不重写父类方法,继承原样即可
}
// 测试代码
const sparrow = new Sparrow(1);
sparrow.fly(); // 输出: 我1岁了,我会飞
sparrow.tweet(); // 输出: 我会啾啾叫
const pigeon = new Pigeon(2);
pigeon.fly(); // 输出: 我2岁了,我会飞
pigeon.tweet(); // 输出: 我会叫
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
- 在上面的 Sparrow 类中重写了
tweet()
方法(需要和父类中的方法同名),那么子类就有了自己定义的方法,可以实现个性化的功能。
那么如果我在父类的 fly()
方法中调用 tweet()
方法,然后子类对象调用 fly()
方法,那么会如何执行呢?
class Bird {
fly() {
console.log("我会飞");
this.tweet(); // 这里调用方法
}
tweet() {
console.log("我会叫");
}
}
class Sparrow extends Bird {
tweet() {
console.log("我会啾啾叫");
}
}
let sparrow = new Sparrow(); // 创建一个麻雀对象
sparrow.fly(); // 这里调用fly()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sparrow.fly()
调用的是继承自父类的方法,在fly()
方法中调用了tweet()
方法,那么调用的是父类的tweet()
方法还是子类的tweet()
方法呢?
是调用子类的方法,因为子类继承父类,子类拥有了父类的方法,所以是子类的 fly()
调用了子类的 tweet()
。
执行结果:
我会飞 我会啾啾叫
# 3 重写构造方法
如果一个类没有写构造方法,那么其实它是有一个默认的隐式构造方法的。
constructor() { }
当你显式的写了构造方法以后,默认的隐式构造方法就不会添加了。
另外,子类的隐式构造方法会调用父类的隐式的构造方法,但是如果重写了子类的构造方法,那子类构造方法的第一句代码必须手动调用父类的构造方法,否则会报错,调用父类的构造方法使用 super()
来调用。
举个栗子:
class Bird {
constructor(age) {
this.age = age;
}
}
class Pigeon extends Bird {
constructor(name, age) {
super(age) // 重写构造方法,第一句代码必须调用父类的构造方法,可以传递参数
this.name = name;
}
}
const p = new Pigeon('Niubi', 5);
console.log(p); // 输出:Pigeon {age: 5, name: 'Niubi'}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 在上面的代码中,子类重写了构造方法,第一句代码必须通过 super 来调用父类的构造方法;在调用父类构造方法的时候,可以传递参数。
需要注意,父类没有构造方法,子类第一句也是要调用父类的隐式构造方法的:
class Bird {
}
class Pigeon extends Bird {
constructor(name) {
super() // 重写构造方法,需要调用父类的构造方法
this.name = name;
}
}
const p = new Pigeon('Niubi', 5);
console.log(p); // 输出:Pigeon {name: 'Niubi'}
2
3
4
5
6
7
8
9
10
11
12
13
- 在上面的代码中,子类定义了构造方法,那构造方法的第一句就是要调用父类的构造方法。
# 4 调用父类方法
我们在重写父类方法的时候,应该尽量做到在父类方法的基础上进行扩展。所以很有可能需要调用父类被重写的方法,然后在父类原来的方法上进行扩展。
可以使用 super
关键字来调用父类的方法。
举个栗子:
class Bird {
tweet() {
console.log("我会叫");
}
}
class Pigeon extends Bird {
tweet() {
super.tweet(); // 调用父类的方法
console.log("我会咕咕叫");
}
}
let pigeon = new Pigeon();
pigeon.tweet();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 如果要在子类中调用父类的方法,可以通过
super.父类方法()
来调用。
执行结果:
我会叫 我会咕咕叫
# 9.4 多态
什么是多态?
多态就是多种状态,相同的类型或方法,因为指向的是不同的对象,而表现出的不同的状态。
举个栗子:
class Bird {
tweet() {
console.log("我会叫");
}
}
class Sparrow extends Bird {
tweet() {
console.log("我会啾啾叫");
}
}
class Pigeon extends Bird {
tweet() {
console.log("我会咕咕叫");
}
}
function makeItTweet(bird) {
bird.tweet(); // 不关心具体类型,统一调用接口
}
let bird1 = new Sparrow();
let bird2 = new Pigeon();
makeItTweet(bird1); // 我会啾啾叫
makeItTweet(bird2); // 我会咕咕叫
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
- 上面麻雀类和鸽子类都继承自鸟类,然后创建了一个麻雀对象和鸽子对象,通过
makeItTweet
函数调用变量的tweet()
方法 - 虽然两个都是鸟类的变量,执行的都是
tweet()
方法,但是因为是不同的子类对象,却得到不同的结果。
变量因为只想不同的对象,执行相同的方法,却表现不同的行为,这就是多态。
多态的作用:
- 提高代码的维护性
- 提高代码的扩展性
- 把不同的子类对象当做父类来看待,可以屏蔽不同子类对象之间的差异,写出通用的代码,以适应需求的不断变化。
在其他语言中,一般通过继承或接口实现来实现多态,但是 JavaScript 是弱类型语言,没有类型检查,只要对象“看起来像”就可以用。
所以不一定要通过继承来实现,只要有相同的方法就可以实现多态。
举个栗子:
const bird = {
speak() {
console.log("鸟在叫");
}
};
const robot = {
speak() {
console.log("机器人在说话");
}
};
function letItSpeak(entity) {
entity.speak();
}
letItSpeak(bird); // 鸟在叫
letItSpeak(robot); // 机器人在说话
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 在上面的代码中,两个对象没有任何关系,只是有相同的方法,就可以实现多态。
- 所以如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。