这是「对象」里两个重大的概念,即「实例成员」和「静态成员」,另外还有一个「访问器成员」,可以控制属性的访问和赋值。
1
2
3
4
5
6
7
8
9
10
11
12
13
class Dog {
constructor(name, breed, weight) {
this.name = name;
this.breed = breed;
this.weight = weight;
}
bark() {
console.log("Woof!");
}
run() {
console.log("Running...");
}
}
实例成员
在上述类中,name
、breed
等属性是用来描述对象的特征,但是你能说清楚 dog 是什么名字,什么品种吗?不能,这是对象在创建的时候才确定的,像这种跟对象绑定在一起的成员叫做「实例成员」。
要获得名字、品种等信息,不能通过类去获取,而是通过实例对象去获取。其中,类的作用就是给这些实例对象提供模板,定义实例对象的行为和属性,并赋值给实例对象。
在 Js 中,实现面向对象的时候,使用的是原型的方式。通过 new 创建实例对象,运行构造函数,给创建的实例对象属性赋值,方法是挂在原型上的。
这也是原型链的特点,如果某个对象没有某个方法,就通过
__proto__
到原型上找。
因此,实例成员一部分在对象上,一部分在原型上。
静态成员
静态成员就是直接属于某一类的成员,不属于任何一个对象。比如说,这个 Dog 类别里有多少个 dog,那这个概念如果和实例对象绑在一起,就不太合适。而是应该直接属于这个类,在类里说明 dog 的总数。
在 ES5 中,静态成员就不用放到 prototype
中,而是直接定义在类上。比如 Dog.totalDogs = 0;
这样定义。
在 ES6 中,有更加语义化的方式,即不声明在 constructor
里,而是直接定义在类上 static totalDogs = 0
。访问的话,直接通过类名去访问 Dog.totalDogs
。静态成员不仅有属性,还有方法。
至于静态成员,我们是很常见的,也用过的。比如, Math
类,里面有一些常用的数学函数,这些函数不依赖于某个实例对象,而是直接属于这个类。
访问器成员
访问器成员是用来控制属性的访问和赋值的。不仅如此,我们可以从概念出发,让某些方法更加语义化。
如类中有一个方法,计算商品总价,是通过 price * quantity
得到的,但是我们希望这个方法更加语义化,比如 totalPrice
,这样更加直观。但是如果将 totalPrice
设为一个属性,并在开始时赋值为 price * quantity
,那么这在语义上并不明确,意思就是这明明就是这个计算,但却声明成属性。
换句话说,我要这个 totalPrice
是属性,但又有计算的功能,符合方法的特点。这时候,我们就可以用访问器成员来实现。
在 ES5 中,是通过 Object.defineProperty() 来定义访问器成员。
1
2
3
4
5
Object.defineProperty(Dog.prototype, "totalPrice", {
get: function () {
return this.price * this.quantity;
},
});
在 ES6 中,使用更为简单,使用语法糖 「get」来实现。
1
2
3
get totalPrice () {
return this.price * this.quantity;
},
我们还可以通过 「访问器」来对属性进行控制,比如说,我们只允许 weight
被修改一次,那么可以这样定义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Dog {
constructor(name, breed, weight) {
this.name = name;
this.breed = breed;
this._weight = weight;
}
get weight() {
return this._weight;
}
set weight(value) {
if (!this._weight) {
this._weight = value;
} else {
console.log("Cannot modify weight after it has been set.");
}
}
}