在JavaScript面向对象编程(一)原型与继承和JavaScript面向对象编程(二)构造函数和类中,我们分别讨论了JavaScript中面向对象的原型和类的概念。基于这两点理论,本篇文章用一个简单的例子来阐述如何在JavaScript中写类与子类。
几个面向对象的概念
实例属性:是每个对象所拥有的属性。比如对于一个Person类的对象而言,name、age等属性是每一个person所拥有的。而且,不同person的age和name可能不同。所以,在JavaScript中我们必须把实例属性加在对象上面。
实例方法:是类的实例所共享的方法。这些方法通过实例进行调用。比如所有的Person类的对象共享一个getName()方法,通过person对象调用这个方法获取对象的name属性的值。这个方法是实例对象共享的,所以把实例方法加在类的prototype上。
类属性与方法:这与传统面向对象语言中静态属性和方法类似。类属性与方法是类所拥有的,而非对象所拥有。在JavaScript所实现的类属性和类方法是通过“构造函数对象”获取。
基类方法调用:在子类的构造函数或者是子类所重载的方法中,调用基类相应方法。
现在用Person类和其子类Student类来阐述上面的概念。
定义一个类
1 var Person = function(name,age){ 2 //instance property, which owns by the instance. 3 this.name = name; 4 this.age = age; 5 }; 6 //Class methods and properties, owned by Class not by the instances. They are "static" 7 Person.YOUNG = 18; 8 //instance methods, which are shared by all the instances 9 Person.prototype.sayHello = function(){ 10 console.log("Hello!"); 11 console.log("I am " + this.name); 12 }; 13 Person.prototype.isAdult = function() { 14 if(this.age >= Person.YOUNG){ 15 return true; 16 } 17 else{ 18 return false; 19 } 20 }; 21 Person.prototype.grow = function(){ 22 this.age = this.age + 1; 23 };
在Person类中,name和age是实例属性,它们是每个实例对象所拥有的。所以将name和age放在构造函数中。前一篇文章已经讨论过,new关键字生成一个对象,并且使用new后面的函数来初始化这个对象。所以name和id这两个实例属性已经通过this.name = name 和this.age = age这两行代码加在了实例对象上面了。
Person.YOUNG是类属性,它是由Person类所拥有的。isAdult中通过与它比较来判断一个Person是否为成年人。如果YOUNG的值改变了,那么所有isAdult的行为也会发生变化。
sayHello,isAdult和grow是实例方法。通过对象进行调用,实例方法中经常会带有this关键字,用来指代调用方法的对象。前一篇文章已经讨论过,通过new关键字的对象以构造函数的prototype属性为原型。所以根据原型链的概念,所有对Person类的对象都能调用这些方法。对象可以通过调用grow方法来增长其age的值,grow方法的this指代的是调用它的对象。
那么就能使用这个类了:
1 var p = new Person("Jack", 17); 2 p.sayHello();//Hello!I am Jack 3 p.isAdult();//false 4 p.grow(); 5 p.isAdult();//true
定义子类
这里介绍定义子类的基本方法。定义子类的关键是,将基类(父类)的prototype属性作为子类的prototype属性的原型。这样就能构成一个原型链:实例->子类.prototype->父类.prototype。那么根据原型链的概念,就能通过实例对象访问父类定义的方法。同样,子类也可以覆盖父类中的方法。
在传统面向对象编程语言中,子类(基类)在构造函数中会调用父类的构造函数。子类在覆盖父类方法中也能调用父类的方法。因为在定义子类时,我们往往希望对父类的行为进行修改或补充,而不一定是完全替换它们。
1 //define a sub class 2 var Student = function(major,name,age){ 3 this.major = major; 4 Person.prototype.constructor.call(this,name,age); 5 }; 6 Student.prototype = Object.create(Person.prototype); 7 Student.prototype.constructor = Student; 8 Student.baseClass = Person; 9 //override the sayHello method 10 Student.prototype.sayHello = function(){ 11 //call the method in the base class 12 Student.baseClass.prototype.sayHello.call(this); 13 console.log("I am a " + this.major + " student!"); 14 }; 15 //add a method 16 Student.prototype.doHomework = function(){ 17 console.log("I am doing " + this.major + " homework!"); 18 };
Student类是Person类的子类。它的每个实例对象除了拥有name和age之外还拥有一个major属性,用来表示每个student的专业。因为Person类的构造函数已经对name和age进行初始化了,所以在Student类的构造函数中可以调用其父类的构造函数(第4行)。
为了让Student的实例也能访问Person类的实例方法,就要将Person.prototype作为Student.prototype的原型。这样就构成了原型链:实例->Student.prototype->Person.prototype。代码的第6行实现了这一点。需要补充的是:Student.prototype = new Person() 同样可以将Person.prototype作为Student.prototype的原型,但是此时Student.prototype却拥有了两个实例属性name和age,而这两个实例属性应该是实例所拥有而非类的prototype拥有。所以在这里使用Object.create(Person.prototype)比较好。(具体使用哪种方法要根据实现类的步骤决定)
第7行的作用是将Student类的构造函数纠正为Student.
第8行的作用是将Student类的基类(父类)赋值为Person,方便之后重定义父类方法时调用。
Student类重定义了父类中的sayHello方法,不仅说出了自己的name而且说出了自己的major是什么。因为sayHello中只是补充了说出major的行为,所以调用了父类的方法。
最后Student类增加了一个doHomework的方法(因为学生都得做作业)。
这样就能使用这个类了:
1 var s = new Student("Computer Science","Mike",17); 2 s.sayHello();//Hello!I am Mike 3 //I am a Computer Science student! 4 s.isAdult();//false 5 s.grow(); 6 s.isAdult(); //true 7 s.doHomework();//I am doing Computer Science homework!
面向对象编程的作用是:封装,继承和多态。我会在相关日志中不断从这三个角度更新日志。
参考文章:
1)《JavaScript The Definitive Guide》第九章
2)CoolShell:http://coolshell.cn/articles/6441.html
3)MDN:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Introduction_to_Object-Oriented_JavaScript