# JavaScript教程 - 9 面向对象
面向对象编程(OOP,Object-Oriented Programming),首先涉及到的两个概念就是:类 和 对象。
什么是类?
类就是对现实事物的抽象设计。
例如设计学生的类,可能包括属性:学号,姓名、年龄、性别等。
设计狗的类,可能包括属性:名字、年龄、品种。
类表示的是对一个事物的抽象,不是具体的某个事物。
什么是对象?
对象就是一个具体的实例,这个实例是从类派生出来的。
我们将类的属性赋值,就产生了一个个实例,也就是对象。
例如通过学生的类,我们赋值:学号=001,姓名=张三,年龄=16,性别=男,班级=三年二班,产生了就是一个名字叫张三的具体的学生,这样我们通过给类可以派生出一个个不同属性值的对象,李四、王五、赵六...。
所以说面向对象编程,就是先设计类,然后根据类来生成一个个对象。
# 9.1 类和对象
JavaScript 和其他的面向对象的变成语言有一些区别,在一些其他的语言中,我们需要先创建类,然后根据类来创建对象。但是 在 JavaScript 中可以直接创建对象,就像我们前面讲解对象时一样。这主要是 JavaScript 面向对象实现的原理不同,是基于原型,而非基于类,class
只是语法糖(更简洁更易读的语法),底层仍是原型(ES6 未改变 JavaScript 的 OOP 本质),当然这个后面细说,有个印象。
前面不通过类来创建对象的时候,存在如下问题:
无法区分对象的类型
例如创建多个学生和多个老师对象,没办法区分哪个对象是什么类型的,所有的对象只是有一些属性和方法,没什么本质区别。
批量创建对象很不方便
每次创建对象都需要使用字面量的形式创建,每次都要写各种属性和方法。
下面我们先来定义类,然后通过类来创建对象,看看有什么不同。
# 1 类的定义
类中可以定义属性和行为,属性也就是成员变量,表示这个类有哪些数据信息,行为也叫方法,表示这个类能干什么。
例如,对于学生类而言,学号、姓名、年级就是属性,学习这个行为,可以定义为方法。
那么我们可以定义以下学生类:
/**
*定义类使用class关键字,后面跟类名
*/
class Student {
sid = '001';
name = 'Doubi';
age = 18;
study() {
console.log(`我是${this.name}, 我在学习`);
}
}
2
3
4
5
6
7
8
9
10
11
12
- 上面的类定义了三个成员变量(sid、name、age分别表示学号、姓名、年龄),并赋值了。还定义了一个成员方法
study
,和之前定义对象很相似。
# 2 类的使用
上面我们定义好类了,现在可以使用类来创建对象了。
/**
*定义类使用class关键字,后面跟类名
*/
class Student {
sid = '001';
name = 'Doubi';
age = 18;
study() {
console.log(`我是${this.name}, 我在学习`);
}
}
// 使用类来创建对象
let s1 = new Student(); // 创建一个对象
let s2 = new Student(); // 再创建一个对象
console.log(s1);
console.log(s2);
s1.study();
s2.study();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 通过
new 类名()
可以创建对象,如果要创建多个对象,new
多次就可以了,上面就创建了两个对象。 - 创建对象后,就像之前操作对象一样了,可以给属性赋值和调用方法。
执行结果:
Student {sid: '001', name: 'Doubi', age: 18}
Student {sid: '001', name: 'Doubi', age: 18}
我是Doubi, 我在学习
我是Doubi, 我在学习
2
3
4
但是上面两个对象的值都是一样的呀,但需要注意,这是两个不同的对象!
因为我们直接在类中给各个属性赋值,所以这些值各个对象都会会拥有,注意每次使用 new 创建一个对象,相当于复制了一份数据,我们可以单独再修改每个对象的属性值就可以了。
但是再修改每个对象的值也太麻烦了,能不能在创建对象的时候指定属性值呢?
这就用到构造函数。
# 3 构造函数
我们可以在创建对象的时候,直接传递参数,给属性赋值,这样每个对象就直接拥有不同的值了。
而构造函数会在创建对象的时候自动执行,通过传递的属性值,给属性进行初始化。
举个栗子:
/**
*定义类使用class关键字,后面跟类名
*/
class Student {
sid;
name;
age;
constructor(sid, name, age) {
console.log("构造方法: ", sid, name, age);
this.sid = sid;
this.name = name;
this.age = age;
}
study() {
console.log(`我是${this.name}, 我在学习`);
}
}
let s1 = new Student("001", "Doubi", 18);
let s2 = new Student("002", "Niubi", 13);
console.log(s1); // Student {sid: '001', name: 'Doubi', age: 18}
console.log(s2); // Student {sid: '002', name: 'Niubi', age: 13}
s1.study(); // 我是Doubi, 我在学习
s2.study(); // 我是Niubi, 我在学习
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
- 在上面的代码中,
constructor
就是构造方法,名称是固定的,接收三个参数(sid, name, age)
,在函数方法中,分别将三个参数赋值给类中的成员变量;构造函数会在执行 new 创建对象的时候执行。 this.name = name;
是将参数赋值给 当前被创建的对象 的 name 属性;- 在调用
new Student("001", "Doubi", 18)
创建对象的时候,就是在调用构造方法,并将参数传递给构造方法;
执行结果:
构造方法: 001 Doubi 18
构造方法: 002 Niubi 13
Student {sid: '001', name: 'Doubi', age: 18}
Student {sid: '002', name: 'Niubi', age: 13}
我是Doubi, 我在学习
我是Niubi, 我在学习
2
3
4
5
6
在定义类的时候,可以不用在类上显式声明属性,可以直接在构造函数中创建并初始化属性:
/**
*定义类使用class关键字,后面跟类名
*/
class Student {
// 构造函数
constructor(sid, name, age) {
this.sid = sid;
this.name = name;
this.age = age;
}
// 标准类方法形式定义函数
study() {
console.log(`我是${this.name}, 我在学习`);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
上面定义函数是标准类方法定义函数,推荐使用上面这种。
还可以使用匿名函数方式定义:
class Student {
// 构造函数
constructor(sid, name, age) {
this.sid = sid;
this.name = name;
this.age = age;
}
// 匿名函数
study = function () {
console.log(`我是${this.name}, 我在学习`);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
- 当然上面两种定义函数的方式是有区别的,我们还可以使用箭头函数,这个后面再讲。
# 4 实例判断
上面讲的对象是通过类来创建的,也就是说通过一个类创建的对象,都是这个类的实例,通过类创建的对象可以等价于实例。
我们可以通过 instanceof
关键字来判断一个实例是否是一个类的实例。
举个栗子:
class Student { // 创建一个学生类
}
class Teacher { // 创建一个老师类
}
let s1 = new Student(); // 创建学生类的实例
let s2 = new Student();
let t1 = new Teacher(); // 创建老师类的实例
// 判断对象是否是类的实例
console.log(s1 instanceof Student); // true, s1是Student的实例
console.log(s2 instanceof Student); // true, s2也是Student的实例
console.log(s1 instanceof Teacher); // false, s1不是Teacher的实例
console.log(t1 instanceof Student); // false, t1也不是Student的实例
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 上面创建了两个类,通过这两个类来创建对象;
- 可以看到不同类的对象与类的关系。
# 5 静态属性和方法
上面我们定义的属性和方法,是实例变量和实例方法,也叫成员变量和成员方法。
我们还可以在类中定义静态属性和方法:
- 静态属性:属于类本身,而不是某个实例,所以只能通过类来访问。
- 静态方法:也是属于类本身的方法,不能通过实例调用,只能通过类来调用。
举个例子:
/**
*定义类使用class关键字,后面跟类名
*/
class Student {
// 定义静态属性,使用 static 关键字修饰
static planet = "Earth";
constructor(sid, name, age) {
this.sid = sid;
this.name = name;
this.age = age;
}
// 普通方法
study() {
console.log(`我是${this.name}, 我在学习`);
}
// 静态方法,使用 static 关键字修饰
static species() {
console.log("Homo sapiens");
}
}
let stu = new Student("001", "张三", 18); // 创建张三对象
console.log(Student.planet); // 输出:Earth
// 错误:实例不能直接访问静态属性
console.log(stu.planet); // undefined
// 调用实例方法
stu.study(); // 输出:我是张三, 我在学习
// 调用静态方法
Student.species(); // 输出:Homo sapiens
// 错误:实例不能调用静态方法
// stu.species(); // 会报错:TypeError: stu.species is not a function
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
- 上面使用
static
修饰,分别定义静态属性和静态方法; - 静态属性和静态方法不能通过实例对象来访问,只能通过类来访问;
为什么需要用到静态属性和静态方法?
对于类的所有实例共享的数据,我们可以定义为静态属性。
如果一个方法不依赖实例的属性,也就是说在方法中不会访问到实例属性,那么就可以定义为静态方法,一般情况下,对于一些工具类,可以用静态方法来写。
需要注意:在实例方法中,可以访问静态属性,这没问题,在哪里通过类都可以访问。在静态方法中 可以访问其他静态属性/静态方法。但是不能直接访问实例属性,因为实例属性值都是属于某个具体的对象的,而静态方法是属于类的不属于某个对象,所以如果在静态方法中访问实例属性,根本不知道哪个实例。
下面演示一下静态属性的使用。
我们定义了一个 Student 类,然后通过 Student 类来创建对象,我们想记录一共创建了多少个 Student 对象,应该如何操作呢?
通过定义 静态属性 来实现,代码如下:
/**
*定义类使用class关键字,后面跟类名
*/
class Student {
// 定义一个静态属性,用于记录创建的对象个数
static stuCount = 0;
constructor(sid, name, age) {
Student.stuCount++; // 创建对象会调用构造方法,调用一次就+1
this.sid = sid;
this.name = name;
this.age = age;
}
}
let stu1 = new Student("001", "张三", 18);
let stu2 = new Student("002", "李四", 19);
let stu3 = new Student("003", "王五", 20);
// 通过类名访问
console.log(Student.stuCount); // 输出: 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 在上面的代码中,我们定义了一个类,然后在类中定义了一个
stuCount
静态变量。当创建对象的时候,会调用构造方法,我们在构造方法中将stuCount++
,这样就可以记录调用构造方法的次数。