javascript 面向对象的演化
面向对象并不是针对javascript,在计算机语言里面,有很多面向对象语言,面向对象主要是一种思想,并不针对语言。
说到面向对象,我们必须理解对象。那么什么是对象,从Ecmascript的规范里来讲,javascript世界里都是对象,比如函数,数组,json等。而从面向对象的思想来说,对象就是属性和方法的结合体。
创建一个对象
下列代码创建了一个person对象,并且为该对象填加了name和age属性,而且还添加了showName和showAge两个方法:
var people=new Object(); |
这里需要解释下,所谓的属性相当于绑定在people对象的变量,而方法则相当于绑定在people对象的函数。同时,在这里的this指的就是people对象。
如果需要再创建另一个相同功能的对象呢?那么:
var people1=new Object(); |
显然,这种object构造函数创建单个对象的方式是有一明显的缺点,即产生大量的重复代码。就好像我们在一页面中写了某一个特效函数,但是在另一页面也需要此特效,于是我们不是在另一页面重写一遍,而是把该特效函数封装成方法作为外部的js文件,来供不同页面引用。于是就出现了工厂模式:
工厂模式
工厂模式在程序设计语言里是一种很普遍的设计模式,这种模式并没有详细介绍内部实现机制,而是用函数封装以特定接口创建对象的细节,如:
function createPerson(name,age){ //构造函数 |
工厂模式虽然解决了为了创建单一对象而重复代码的问题,但是却没有解决对象识别问题,即不知一个对象的类型,于是构造函数模式出现了。
构造函数模式
下列代码是用构造函数对工厂模式进行重写,相对于前面的工厂模式而言,这种模式无需在函数开头就创建一个对象,另外是直接将属性和方法赋给this对象,还有就是没有了return语句了。但是,必须在构造函数外,创建新的对象,这样这些新的对象才能分别保存Person的一个不同实例,并且这两个新对象都有一个constructor属性,该属性指向了Person。
function Person(name,age){ |
判断某个对象是不是某个类的实例,比如你要判断对象,看它是否为数组(或者日期),这时你用typeof是不行的, 因为typeof返回的大部分是对象,太笼统了。所以我们必须用instanceof。
alert(person1 instanceof Object) ; //true |
虽然构造函数模式指明了对象类型,无需再函数里创建对象(系统自己创建),但它也是有问题的,因为每个方法都要在每个实例上重新创建一遍,通过:
alert(person1.showName==person2.showName) //false |
我们可以知道,person1与person2的创建,都要new两个方法出来(showName,showAge),这无疑是浪费系统资源。这就是构造函数的问题,于是,就有了原型模式。
原型模式
给一个数组求和,我们会:
var arr1=[1,2,3,4,5]; |
可是,如果我们希望给数组arr2求和呢?
var arr2=[2,4,6,8,10]; |
这样吗?显然是错的,因为arr2并没有sum这个方法,因此代码执行会报错。
如果我们给数组添加prototype,此时arr2便得到正确结果:
Array.prototype.sum=function(){ |
关于prototype,我们创建的每一个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。因此,prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处就是可以让所有对象实例共享它所包含的属性和方法。例如,上面数组arr2可以使用arr1的求和方法。
prototype就类似于css中的class类名,可以给一组具有相同className的标签添加相同的样式。
此时,我们再来看,person1和person2引用的就是CreatePerson的同一方法:
function CreatePerson(){ |
创建了自定义的构造函数后,它的原型对象会取得constructor属性,而其他方法和属性则是从object继承而来。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针,指向构造函数的原型对象。
也就是说,新对象是由prototype指向原型对象的,而prototype是由constructor指向CreatePerson的。
关于判断一个新对象是否与另一个对象是否存在prototype关系,我们可以通过isPrototype()方法来确定。如果返回的值是true,则表示新对象都有一个指向CreatePerson.prototype的指针。
CreatePerson.prototype.isPrototype(person1) ; //true |
重写实例属性
由于原型里的属性并不一定是新对象想要的,所以我们有时候必须对对象实例的属性进行重写,注意这里不是对原型对象里的属性重写,而是对调用构造函数的新对象。如果该属性与原型对象的属性名相同,则创建的属性会覆盖原型里的属性。
function CreatePerson(){ |
对于读取某个对象的属性,是从该对象实例本身开始的。如果在实例中找到该属性,则返回该属性的值,但是如果没有找到,则继续查找指针指向的原型对象。因为person1有name属性(重写了),所以返回 mol。而person2没有,则返回yix。
当然,这里可以通过删除实例属性,从而使得我们可重新访问原型对象的属性:
delete person1.name; //删除实例重写的属性 |
那么,如何判断一个属性是存在实例对象还是原型对象里呢? 我们可以通过hasOwnProperty()方法来检测。如果属性存在实例对象,则返回true,否则就返回false:
function CreatePerson(){ |
因为每次添加属性和方法都要写CreatePerson.prototype,为了精简代码,我们可以把原型封装如下:
function CreatePerson(){ |
这样,虽然精简了代码。但是constructor属性不再指向CreatePerson,因为每创建一个函数,就会同时生成它的prototype对象,而这个对象会自动获取constructor属性,于是,constructor指向的就是Object。此时,我们需要手动设置constructor的指向:
function CreatePerson(){ |
在我们使用原型对象时,也发现了一些不足的地方。比如,构造函数没有参数,属性只能给固定的值,显然这样局限性很大。另外,由于prototype的属性和方法都是共享的。所以重写person1的某个属性时,person2引用的这个属性也会随着改变。注意这个主要是由于我们把原型封装了才会有的,如果不封装原型,则person2是不会受到影响的。
混合法
为解决原型模式的问题,一般而言,会结合构造函数模式和原型模式的混合使用。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性。这样一来,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用。
function CreatePerson(name,age){ //构造函数 |
由上面的例子,person1和person2共享了CreatePerson原型的属性和方法。当person1重写partner属性时,并不会影响构造函数原型中的partner属性,故person2显示aa,bb。但person1是仍有原来的属性和方法,故显示aa,bb,cc,这就是类似于拓展属性和方法。
继承
什么是继承?继承就是利用原型让一个引用类型继承另一个引用类型的属性和方法。
假如person有name和age属性,还有showName和showAge方法。如果我希望另外一个对象work拥有person的属性和方法,并且还有job属性和showJob方法。在不影响person的属性和方法的情况,那么该如何做?此时,我们就要用到继承:
function person(name,age){ |
例子中用到了构造函数伪装,并且使用了for in对person原型中的属性和方法进行了循环复制给了job,而不是让job与person共同引用一套prototype,从而当job增删属性和方法时,不会对person造成影响。
结语:关于javascript面向对象,主要部分还是在prototype。理解它的由来、缺点、优点,以及特性(封装,继承,多态)尤为重要,我个人在这方面还需要更多的理解与实践。