在本系列的前两篇《javascript面向对象(一):object基础以及构造函数详解》和《javascript面向对象(二):prototype以及继承详解》中,我们已经对JS中面向对象的基础知识了解的八九不离十了,但是其实都是为了给本系列的最后一篇做铺垫。这一篇就让我们一起学习class语法,对前面的知识进行一次升华。
class语法
前面说的用构造函数和new
关键字来创建对象的方式是可行的,不过为了和大多数OOP语言保持对齐,JS中引入了class语法。语法如下
class MyClass {
// class methods
constructor() { ... }
method1() { ... }
method2() { ... }
method3() { ... }
...
}
这里的格式要注意,方法之间没有逗号也没有分号,方法的声明前面也没有function
关键字。
在创建对象时,利用new MyClass()
语法,之后构造函数被自动运行执行初始化。
例如
class People{
constructor(name,age){
this.name=name;
this.age=age;
}
sayHello(){
console.log(`Hello, my name is ${this.name}, I am ${this.age} years old!`);
}
}
let xiaofu=new People('xiaofu',99);
xiaofu.sayHello(); //Hello, my name is xiaofu, I am 99 years old!
class的本质还是一个函数,类似于构造函数。在定义class People{}
的时候,相当于定义了一个叫People
的构造函数,函数的内容从constructor
中拿取。当执行new关键字时,执行该函数,同时将剩下的方法放进People.prototype
中。
下面的这些代码可以验证
console.log(typeof People); //function
console.log(Object.getOwnPropertyNames(xiaofu)); //["name", "age"]
console.log(xiaofu.__proto__); //{constructor: ƒ, sayHello: ƒ}
所以正因为如此,很多人说JS中的class只是一个语法糖而已。
但是class语法还是有很多新的特性,例如for...in
循环的时候prototype中的内容不会被包括,还有class中自动会采用use strict
模式等等,下面我们会慢慢了解。
匿名class
和匿名函数一样,可以直接将匿名class赋值给变量
let Student=class{
intro(){console.log('Hello')}
}
new Student().intro(); //Hello
注意在使用的时候不能漏掉new
关键字,同时不能省略Student
后面的小括号。
Getter/Setter
想要对某个属性在赋值和取值的时候进行适当的逻辑处理,就需要用到Getter方法和Setter方法
class People{
constructor(name,age){
this.name=name;
this.__age=age;
}
get age(){
return this.__age;
}
set age(value){
if(value<1 || value>150){console.log('oops!')}else{
this.__age=value;
}
}
sayHello(){
console.log(`Hello, my name is ${this.name}, I am ${this.age} years old!`);
}
}
let xiaofu=new People('xiaofu',99);
xiaofu.sayHello(); //Hello, my name is xiaofu, I am 99 years old!
xiaofu.age=151; //oops!
xiaofu.sayHello(); //Hello, my name is xiaofu, I am 99 years old!
console.log(Object.getOwnPropertyNames(xiaofu)); //["name", "__age"]
这里处于约定俗成的习惯,将不希望被外界访问的属性前面加上了下划线,但是从下面打印的结果可以看到,JS并不会像python那样对双前下划线的属性进行修改,用户还是可以通过带下划线的属性名进行访问。后面我们还会介绍一种完全私有的属性,但是不能在构造函数里面进行声明。同时要注意Getter和Setter的名字不能和属性名相同,不然会造成循环调用,导致栈溢出。
类属性
当想定义一个所有类实例都共享的属性,就可以定义类属性
class People{
sex='male';
constructor(name,age){
this.name=name;
this.age=age;
}
sayHello(){
console.log(
`Hello, my name is ${this.name}, I am ${this.age} years old, I am ${this.sex}!`
);
}
}
let xiaofu=new People('xiaofu',99);
console.log(xiaofu.sex); //male
xiaofu.sayHello(); //Hello, my name is xiaofu, I am 99 years old, I am male!
这里的sex='male'
就是定义类属性,同样可以在方法中用this
来访问,也可以在实例对象中用点号获取。
this丢失
我在另一篇博客《Javascript中this指向丢失原因及解决办法详解》中,我介绍了两种解决this丢失的方式,其实还有一种方式,就是通过类属性
class People{
sex='male';
constructor(name,age){
this.name=name;
this.age=age;
}
sayHello=()=>{
console.log(
`Hello, my name is ${this.name}, I am ${this.age} years old, I am ${this.sex}!`
);
}
}
let xiaofu=new People('xiaofu',99);
setTimeout(xiaofu.sayHello,2000);
这里将sayHello
变成了一个类属性,因为类属性并不会被放进prototype中,所以每个实例对象有一个单独的sayHello
,所以不会有问题。但是经过测试发现这里只能用箭头函数,不能用匿名函数。
class的继承
一个类继承另一个类用extends
关键字,继承的本质就是将子类的prototype指向父类的prototype
class People{
sex='male';
constructor(name,age){
this.name=name;
this.age=age;
}
sayHello=()=>{
console.log(
`Hello, my name is ${this.name}, I am ${this.age} years old!`
);
}
}
class Employee extends People{
work(){
console.log('Coding like crazy!')
}
}
let xiaofu=new Employee('xiaofu',99);
xiaofu.sayHello(); //Hello, my name is xiaofu, I am 99 years old!
xiaofu.work(); //Coding like crazy!
console.log(Object.getOwnPropertyNames(xiaofu)); //["sex", "sayHello", "name", "age"]
console.log(xiaofu.__proto__); //People {constructor: ƒ, work: ƒ}
可以看到,进行初始化的时候还是需要传递两个参数,说明虽然没有直接在Employee
中定义构造函数,父类中的构造函数会被执行。
如果在不清楚类名的情况下,想构造和xiaofu
同样的对象zhangsan
,就可以像下面这样
let zhangsan=new xiaofu.constructor('zhangsan',18);
zhangsan.sayHello(); //Hello, my name is zhangsan, I am 18 years old!
zhangsan.work(); //Coding like crazy!
重写方法
和其他语言中类继承一样,子类中定义和父类中相同名称的方法时,优先执行子类中的
class People{
sex='male';
constructor(name,age){
this.name=name;
this.age=age;
}
sayHello(){
console.log(
`Hello, my name is ${this.name}, I am ${this.age} years old!`
);
}
}
class Employee extends People{
sayHello(){
console.log('I am from Employee.')
}
work(){
console.log('Coding like crazy!')
}
}
let xiaofu=new Employee('xiaofu',99);
xiaofu.sayHello(); //I am from Employee.
xiaofu.work(); //Coding like crazy!
这里我将父类中原本的类属性
sayHello
还原成了方法,不然的话因为类属性在实例中,比prototype中的方法优先级高,还是会优先执行类属性
但是通常子类都不会完全重写父类的方法,而是会在原有的基础上进行添加,所以就需要在子类中运行父类的方法,这时候就需要用到super
关键字。super()
用来执行父类的构造函数,super.method()
用来执行父类的普通方法
class People{
sex='male';
constructor(name,age){
this.name=name;
this.age=age;
}
sayHello(){
console.log(
`Hello, my name is ${this.name}, I am ${this.age} years old!`
);
}
}
class Employee extends People{
sayHello(){
super.sayHello();
console.log('I am from Employee.')
}
work(){
console.log('Coding like crazy!')
}
}
let xiaofu=new Employee('xiaofu',99);
xiaofu.sayHello();
xiaofu.work();
这时候打印的结果如下
Hello, my name is xiaofu, I am 99 years old!
I am from Employee.
Coding like crazy!
如同箭头函数中没有this
关键字一样,同样也没有super
关键字,使用的时候要注意,这里就不展开了。
重写构造函数
但是构造函数的重写会有一点麻烦,先看下面的例子
class Employee extends People{
constructor(name,age,sex){
this.name=name;
this.age=age;
this.sex=sex;
}
sayHello(){
super.sayHello();
console.log('I am from Employee.')
}
work(){
console.log('Coding like crazy!')
}
}
这里的本意是不要父类中的构造函数,完全重写一个之类自己的,但是发现运行的时候会报错
Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor at new Employee
意思就是说,在子类的构造函数中,必须要先运行super()
,然后才能使用this
关键字。
所以正确方式应该是像下面这样
class Employee extends People{
constructor(name,age,sex){
super(name,age);
this.sex=sex;
}
sayHello(){
super.sayHello();
console.log('I am from Employee.')
}
work(){
console.log('Coding like crazy!')
}
}
同时要特别注意,父类的构造函数永远使用父类中的类属性
class Animal {
name = 'animal'
constructor() {
alert(this.name);
}
}
class Rabbit extends Animal {
name = 'rabbit';
}
new Animal(); // animal
new Rabbit(); // animal
这里父类和之类中同时有name
这个类属性,但是构造函数总是会使用父类中的类属性。
如果是普通方法就不会有这个问题
class Animal {
showName() { // instead of this.name = 'animal'
alert('animal');
}
constructor() {
this.showName(); // instead of alert(this.name);
}
}
class Rabbit extends Animal {
showName() {
alert('rabbit');
}
}
new Animal(); // animal
new Rabbit(); // rabbit
这里子类和父类都有方法showName()
,但是父类的构造函数调用的确是子类中的方法。
这种区别和JS内部的运行机制有关,想了解更多的朋友可以参考这里。
静态属性和方法
熟悉python的朋友应该知道python中的@classmethod
装饰器可以去定义一个类属性,用来做类级别的操作或者是做工厂方法。JS中的静态方法也是起到一样的作用。
静态方法和普通方式定义唯一的区别就是前面要加上static
关键字
class People{
// sex='male';
constructor(name,age){
this.name=name;
this.age=age;
}
sayHello(){
console.log(
`Hello, my name is ${this.name}, I am ${this.age}!`
);
}
static Young(name){
return new this(name,18);
}
}
这里的Young
就是一个静态方法,里面的this
关键字指向的是People
这个构造函数,通过将第二个参数锁定为18,实现一个工厂方法
let xiaofu=People.Young('xiaofu');
xiaofu.sayHello(); //Hello, my name is xiaofu, I am 18!
xiaofu.Young('test'); //error
静态方法也是可以被继承的
class Employee extends People{
sex='female';
constructor(name,age){
super(name,age);
}
sayHello(){
super.sayHello();
console.log('I am from Employee.')
}
work(){
console.log('Coding like crazy!')
}
}
let xiaofu=Employee.Young('xiaofu'); //Hello, my name is xiaofu, I am 18!
xiaofu.sayHello(); //I am from Employee.
xiaofu.Young('test'); //error
注意,内建的一些类也有一些静态方法,例如
Object.keys()
,Array.isArray()
。这些内建类的静态方法是不会被继承的,所以虽然Array继承自Object,却没有Array.keys()
这个静态方法
静态属性和前面直接赋值的类属性没有太大区别,只是在声明时前面加了一个static关键字,这里就不展示了。
私有和受保护的属性及方法
所谓受保护的属性在前面的Getter/Setter部分我们已经见识过了,在希望受保护的变量前面约定俗成的加上下划线,然后用方法去对其进行赋值和取值。
受保护只是约定俗成的套路,并没有在语言层次进行强制约束。而私有的属性和方法则不一样。
私有的属性或方法名称以#
开头,在语言层次限制其只能被类定义内部访问。
class People{
#name='cc';
constructor(name,age){
this.#name=name;
this.age=age;
}
#sayHello(){
console.log(
`Hello, my name is ${this.#name}, I am ${this.age}!`
);
}
sayHi(){
console.log(
`Hello, my name is ${this.#name}, I am ${this.age}!`
);
}
static Young(name){
return new this(name,18);
}
}
需要注意的是私有属性的声明一定要在方法外,例如这里如果直接在构造方法内对#name
进行赋值是不行的,必须要现在外面进行声明。
这里定义了一个私有属性#name
和私有方法#sayHello()
,下面验证下
let xiaofu=new People('xiaofu',99);
console.log(Object.getOwnPropertyNames(xiaofu)); //["age"]
xiaofu.sayHi(); //Hello, my name is xiaofu, I am 99!
console.log(xiaofu.__proto__); //{constructor: ƒ, sayHi: ƒ}
从打印的结果可以看到,私有属性和方法既不在对象的属性内,也不在prototype内,所以不能被外部对象访问。但是#name
却可以被内部的sayHi()
方法调用。
这样对于一些在class定义时候使用的一些临时内部变量或者函数就可以设置为私有的防止被外部访问,如果想要对私有属性进行访问,可以是用Getter/Setter方式。
instanceof
最后介绍一个符号instanceof
,用于判断一个对象是否是一个class的实例,继承的父类也包括在内。格式如下
obj instanceof Class
检查的原理也非常简单,就是检查obj.__proto__
是否等于Class.prototype
,不满足就从prototype的继承链依次检查obje.__proto__.__proto__
是否等于,等等。
例如
class People{
#name='cc';
constructor(name,age){
this.#name=name;
this.age=age;
}
}
class Employee extends People{
constructor(name,age){
super(name,age);
}
}
let xiaofu=Employee.Young('xiaofu');
console.log(xiaofu instanceof People); //true
console.log(xiaofu instanceof Employee); //true
既然可以用来判断class,当然也可以用来判断构造函数,这里就不举例了。也可以手动在class里面定义[Symbol.hasInstance]
静态方法,可以参考这里。
Mixin
一个JS中的对象只能指向单个prototype,所以如果想让一个子类同时继承两个父类就变得不可能。但是我们却可以将其中一个父类我们想要的一些方法整理出来人为添加到另一个父类的prototype中,实现两父类功能的合并。
这个被整理出来的方法集合就叫做Mixin。
// mixin
let sayHiMixin = {
sayHi() {
alert(`Hello ${this.name}`);
},
sayBye() {
alert(`Bye ${this.name}`);
}
};
// usage:
class User {
constructor(name) {
this.name = name;
}
}
// copy the methods
Object.assign(User.prototype, sayHiMixin);
// now User can say hi
new User("Dude").sayHi(); // Hello Dude!
利用Object.assign
将一个对象添加进另一个对象。
下面通过利用Mixin实现一个事件响应
let eventMixin={
on(eventName,handler){
if(!this._eventList){this._eventList={};}
if(!this._eventList[eventName]){
this._eventList[eventName]=[];
}
this._eventList[eventName].push(handler);
},
off(eventName,handler){
if(this._eventList[eventName]){
let fs=this._eventList[eventName];
for(let i=0;i<fs.length;i++){
if(fs[i]==handler){
fs.splice(i,1);
i-=1;
}
}
}else{
return
}
console.log(this._eventList);
},
trigger(eventName,value){
if(this._eventList[eventName]){
this._eventList[eventName].forEach(handler=>{handler(value)})
}else{
return
}
}
}
class testInput{
input(value){this.trigger('input',value);}
}
Object.assign(testInput.prototype,eventMixin);
let input_=new testInput;
f=function(value){console.log(value+' is logged')}
input_.on('input',f);
input_.input(33); //33 is logged
input_.off('input',f);
input_.input(34); //
这里为对象提供一个on
方法添加一个事件的处理回调函数,因为一个事件可能有多个回调函数,所以要采用列表的方式来存储。同时提供一个off
方法从列表中删除回调函数,注意这里的i-=1
是因为可能有重复的添加,在删除了一个元素后为了不影响到接下来的循环,必须先减去1。trigger
方法用于模拟浏览器去手动触发事件响应,并提供一个value做为事件的反馈参数,这里严谨一点可以用...args
来代替,因为可能有多个参数进行返回。
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。