Javascript类与继承的总结

学习Js也有很长的一段时间了。但是学习过程中也是走走停停。对很多概念的感觉也一直是模模糊糊。今天花了一点时间。重新研究了一下js里面的类和继承的机制。顺便总结下。


1.Js中的类式继承。

先看代码:

1
2
3
4
5
6
7
8
9
10
function Person(name){
this.name = name;
}
Person.prototype.showName = function(){
console.log(this.name);
}

/* 实例化一个对象 */
var xiaoming = new Person("Xiao Ming");
xiaoming.showName();//打印出"Xiao Ming"

这是用js实现的最简单的一个Person类,其中包含了一个showNmae的方法,能够打印出name,然后实例化了一个叫xiaoming的对象,打印并且调用showName方法打印出name(也就是”Xiao Ming”)。

那么我们可能还会想,小明是一个人,而且他同样也是一个学生呀,这才是小明扮演的更加重要的角色。于是我们可以这样做。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*Person类*/
function Person(name){
this.name = name;
}
Person.prototype.showName = function(){
console.log(this.name);
}
/*Student类*/
function Student(name,grade){
Person.call(this,name);
this.grade = grade;
}
Student.prototype= new Person();//建立原型链
Student.prototype.constructor = Student;
Student.prototype.sayGrade = function(){
console.log(this.grade);
}

/* 实例化一个新的学生对象 */
var xiaoming = new Student("Xiao Ming","Grade 1");
xiaoming.showName();//打印出"Xiao Ming"
xiaoming.sayGrade();//打印出"Grade 1"

Student类继承了Person类,并拥有Person类的showName方法。在Student类中,我们还加入了Student所特有的属性和方法(grade和sayGrade方法)。

回过头来看看Student类。它和之前的Person类一样,先创建它的构造方法,这里我们看到了Person.call(this,name)这个语句。它的作用是调用父类Person的构造方法,并用this处于作用域链的最顶端,然后name作为参数被传入。

之后便是原型链的建立。

Javascript中的每个函数对象都有prototype属性,这也是Javascript不同与其他语言的重要因素之一。这个属性要么指向另外一个对象,要么就为null。当访问对象的某个成员时,如xiaoming.showName()。如果这个成员未见于当前对象,那么js会在当前prototype属性所指的对象中去查找它。如果在那个对象中也没有找到,那么js会沿着原型链向上逐一访问每一个原型对象,直到找到这个成员。——《Javascript设计模式》

而且我们还需要在原型链中将Student这个类的赋给Student.prototype.constructor函数。Student在设置为Person的实例时,其constructor属性已经被抹去(但是在这个地方,就算我们不写”Student.prototype.constructor = Student;”这句话整个程序还是照样能运行,但是是存在问题的,因为此时的Student.prototype.constructor的值是指向Person的,我们必须要将其指回他自己来)。

这里关于Student.prototype.constructor的问题,我们可以用下面的代码来测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*Person类*/
function Person(name){
this.name = name;
}
Person.prototype.showName = function(){
console.log(this.name);
}
/*Student类*/
function Student(name,grade){
Person.call(this,name);
this.grade = grade;
}
Student.prototype= new Person();//建立原型链
console.log(Student.prototype.constructor);//[Function: Person]结果指向了Person
Student.prototype.constructor = Student;
console.log(Student.prototype.constructor);//[Function: Student]结果指向了Student
Student.prototype.sayGrade = function(){
console.log(this.grade);
}
/* 实例化一个新的学生对象 */
var xiaoming = new Student("Xiao Ming","Grade 1");
xiaoming.showName();//打印出"Xiao Ming"
xiaoming.sayGrade();//打印出"Grade 1"

这就是js中一个最简单的类式继承了。虽然和常规的oop式的继承还是有很大的不同,还是能够接受。但是还是感觉很不舒服,习惯了c#或者java的人看到js继承中出现的一大堆乱七八糟的prototype,估计头都大了。要是能用像extends这样的关键字来继承该多好。(作为一个小彩蛋,我把js里面稍微简化的继承放在文字后面。)

2.Js中的原型式继承。

接下来我们要看到的就是js中最酷的继承方式——原型继承。

原型继承与类式继承截然不同,我们发现在谈论他的时候,最好忘掉类和实例的一切知识,只从对象的角度来思考。——《Javascript设计模式》

还是先看代码,同样是实现之前的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13

/**Person类 */
var Person = {
name:null,
showName:function(){
console.log(this.name);
}

};
/**实例化对象 */
var xiaoMing = Object.create(Person);
xiaoMing.name="Xiao Ming";
xiaoMing.showName();//console输出的结果为:Xiao Ming

可以看到,这里我们首先是创建了一个Person的对象,其中,他有name的属性,还有showName这样一个方法。name属性首先赋值为null,如果在我们后面的xiaoMing对象中,如果我们不对xiaoMing.name进行赋值,那么调用showName方法所打印出来的值就是Person初始化的null。

这里我们使用了一个叫Object.create(someObject)的方法。这个方法的作用我们可以简单的理解,就是创建了一个对象,并将这个创建的新对象的原型链指向someObject。

原型继承的方式比之前的类式继承要清晰简明多了。我们接下来在实现之前代码中的Student类,然后用看看我么的结果是否和之前相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

/**Person类 */
var Person = {
name:null,
showName:function(){
console.log(this.name);
}

};
/**Student类 */
var Student = Object.create(Person);
Student.grade = null;
Student.showGrade = function(){
console.log(this.grade);
}

/**实例化对象 */
var xiaoMing = Object.create(Student);
xiaoMing.name="Xiao Ming";
xiaoMing.grade = "grade 1";
xiaoMing.showName();//console输出的结果为:Xiao Ming
xiaoMing.showGrade();//console输出的结果为:grade 1

最后的结果和我们所期待的是完全相同。但是对比之前的类式继承,两者所用的方法是完全不同,首先,原型继承并没有涉及到constructor方法,而且在实例化(其实也就是创建一个新对象)的时候,也没有使用new关键字来开辟一个新的空间。这里面就有一个关于原型链的概念了,简单来说,就是:

1
2
3
graph LR
xiaoMIng-->Student
Student-->Person

那照这样看来Person就是原型链的顶端了。其实不是这样,在Peroson上面还有Object,这才是原型链的顶端。所以说,完整的原型继承图应该是这样

1
2
3
4
5
graph LR
xiaoMIng-->Student
Student-->Person
Person-->Object.prototype
Object.prototype-->null

关于原型链的话题我们还能学习的东西还有很多,这里就先不展开讨论了。以上就是我对js的两种不同的继承方式的简单理解,至于在什么情况下用哪种方式来进行js开发完全遵循个人的喜好。喜欢传统oop的可能中意类式继承一点,觉得用原型更方便直白的,那就用后者。没什么要求。两者都可以。


小彩蛋

js中的简化类式继承的方式,因为大家可能会觉得,类式继承在初始化构建原型链的时候,实在是太太太麻烦了,这里给介绍一个十分简单的小代码,可以稍微简化下。

1
2
3
4
5
6
function extend(subClass, superClass) {
var F = function () {}
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;
}

就是这个extend函数。接下来我们用第一个Person的案例来说明:

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
28
function extend(subClass, superClass) {
var F = function () {}
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;
}

/*Person类*/
function Person(name){
this.name = name;
}
Person.prototype.showName = function(){
console.log(this.name);
}
/*Student类*/
function Student(name,grade){
Person.call(this,name);
this.grade = grade;
}
extend(Student, Person);
Student.prototype.sayGrade = function(){
console.log(this.grade);
}

/* 实例化一个新的学生对象 */
var xiaoming = new Student("Xiao Ming","Grade 1");
xiaoming.showName();//打印出"Xiao Ming"
xiaoming.sayGrade();//打印出"Grade 1"

用extend的函数来实现原型链的建立,这样就稍微比之前的代码简洁了一点。