JavaScript面向对象编程
一、引言
随着Ajax等技术的广泛使用,YUI、Prototype等对JavaScript的应用可谓是淋漓尽致、出神入化。人们才发现原来JavaScript可以实现如此强大的功能,具备如此优雅的架构...
二、准备
1、概念
JavaScript是一种弱类型语言。包括:
基本类型:数字Number,字符串String,布尔值Boolean;
复合类型:对象Object,数组Array;
工具类型:全局对象Global,日期Date,数学对象Math,正则表达式RegExp,错误对象Error;
特殊类型:函数Function。
这里我只想说两点:
1)基本数据类型的包装对象
每一个基本数据类型都有一个对应的对象类。可以灵活地实现类型转换。简单地说,JavaScript不仅支持数字、字符串和布尔值这些数据类型,还支持Number、String、Boolean类,这些类是基本数据类型的包装(wrapper)。
例子:
var s="some string";
var len=s.length;
这里,s保存了一个字符串,原始的字符串值是不会改变的。一个新的String对象被创建了,实现了对长度属性的访问,之后它就被销毁了。
其它例子:
var a="some string";
var b=new String("some string");
var c=["a","b","c"];
var d=new Array("a","b","c");
alert(typeof a);//string
alert(a instanceof String);//false
alert(a instanceof Object);//false
alert(typeof b);//object
alert(b instanceof String);//true
alert(b instanceof Object);//true
alert(typeof c);//object
alert(c instanceof Array);//true
alert(c instanceof Object);//true
alert(typeof d);//object
alert(d instanceof Array);//true
alert(d instanceof Object);//true
2)Function类型
做为JavaScript的一种特殊类型,我们将看到函数在面向对象编程中起到了非常关键的作用。
2.值和引用
类型 | 复制 | 传递 | 比较 |
数字 | 值 | 值 | 值 |
布尔值 | 值 | 值 | 值 |
字符串 | 不可变的 | 不可变的 | 值 |
对象 | 引用 | 引用 | 引用 |
函数 | 引用 | 引用 | 引用 |
数组 | 引用 | 引用 | 引用 |
例子:
var s1="hello";
var s2="hell"+"o";
alert(s1==s2);//true
var d1=new Date();
var d2=new Date();
alert(d1==d2);//false
3.this
在构造函数中,指代新创建的对象实例;
在对象的方法被调用时,指代调用该方法的对象实例。
4.arguments
arguments属性由解释器创建,用于访问函数对象的每一个参数。
5.callee,caller
arguments的callee属性获取对正在执行的Function对象的引用;
Function对象的caller属性获取正在调用当前函数的父函数对象。
6.apply,call
两者都是将函数绑定到其它对象上执行的,区别在于调用方式:
apply([thisObj[,argArray]])
call([thisObj[,arg1[,arg2[,[,.argN]]]]])
7.匿名函数
(function(a,b){
return a+b;
})(1,1);
等价于:
function f(a,b){
return a+b;
}
f(1,1);
8.null,undefined
null是JavaScript的关键字,表示空值。可以看作Object类型的一个特殊值。
undefined不是关键字,它是一个全局变量,使用了未定义变量、变量未赋值、void运算符,都会返回“undefined”。
9.constructor
从JavaScript1.1开始,每个对象都具有一个constructor属性,它引用的是用来初始化该对象的构造函数。
10.prototype
JavaScript1.1引入了原型对象的概念,每一个对象都有一个原型对象,对象可以继承它的原型对象的所有属性和方法。
要为一个对象的类制定原型对象,需要将构造函数的prototype属性设置为指定的对象。之后,如果用构造函数初始化对象时,会自动将指定的对象作为新创建对象的原型对象。
注意:
1)使用原型对象可以减少每个继承对象的内存需求量;
2)即使属性是在对象被创建后才加到它的原型对象中的,对象也能够继承这些后定义的属性。
3)当调用一个对象的一个属性时,先在该对象定义内查找该属性,如果没有该属性才到该对象的原型对象中查找,依此类推。
三、实现
在面向对象编程中,我们可以把过程编程中的一个个function看作一个个独立定义的类,函数名即为类名。
1.例子:Circle类
function Circle(radius){
//实例变量
this.r=radius;
}
//静态变量
Circle.PI=3.14159;
//实例方法
Circle.prototype.area=function(){
return Circle.PI*this.r*this.r;
}
//静态方法
Circle.max=function(a,b){
if(a.r>=b.r){
return a;
}
else{
return b;
}
}
//调用
var a=new Circle(2);
var b=new Circle(3);
var c=a.area();
var d=Circle.max(a,b);
2.继承
1)一种继承方式
//这里我们将CircleMore类的prototype指向了一个Circle类实例,
//并增加了circumference方法。
function CircleMore(radius){
this.r=radius;
}
CircleMore.prototype=new Circle(0);
CircleMore.prototype.circumference=function(){
return 2*Circle.PI*this.r;
}
这样能够实现对Circle的继承,但是这里存在一点问题:我们直接把CircleMore类的prototype指向了一个Circle类实例,这样就覆盖了JavaScript提供的原型对象,而且抛弃了给定的constructor属性,这样CircleMore的constructor属性就指向了父类Circle的constructor属性。而且这种方式总是使人感觉怪怪的。
2)Ajax架构Prototype(区别前面提到的prototype哦)的继承方式
//Prototype框架为Object对象定义了extend方法,
//将source的属性和方法复制到了destination。
Object.extend = function(destination, source) {
for (var property in source)
destination[property] = source[property];
return destination;
};
//调用
function ParentClass(){...}
function SubClass(){}
SubClass.prototype=Object.extend({
newMethod:function(){
alert("newMethod");
}
},
ParentClass.prototype
);
3)如果这里对两个参数交换位置,则是对原对象的开展。
例子:通过extend方法对String对象进行了扩展
Object.extend(String.prototype,{
newMethod:function(){
alert("newMethod");
}
});
3.多态
Object.extend = function(destination, source) {
for (var property in source)
destination[property] = source[property];
return destination;
}
//基类
function base(){}
base.prototype={
initialize:function(){
this.oninit();//调用了一个虚方法
}
}
//子类SubClassA
function SubClassA(){}
SubClassA.prototype=Object.extend({
//...其它属性方法
prop:"SubClassA",
oninit:function(){
alert(this.prop);
}},
base.prototype
}
//子类SubClassB
function SubClassB(){}
SubClassB.prototype=Object.extend({
//...其它属性方法
prop:"SubClassB",
oninit:function(){
alert(this.prop);
}},
base.prototype
}
//调用
var a=new SubClassA();
var b=new SubClassB();
a.initialize();//输出"SubClassA"
b.initialize();//输出"SubClassB"
四、JavaScript与设计模式
1.Singleton
function Singleton(){
if(Singleton.caller!=Singleton.getInstance){
throw new Error("Can not new Singleton instance!");
}
this.prop="some string";
//...
}
Singleton._instance=null;
Singleton.getInstance=function(){
if(this._instance==null){
this._instance=new Singleton();
}
return this._instance;
}
var a=Singleton.getInstance();
var b=Singleton.getInstance();
b.prop="another string";
alert(a.prop);//"another string"
alert(b.prop);//"another string"
2.Factory
function XMLHttpFactory(){}
XMLHttpFactory.createXMLHttp=function(){
if(...){
return new XMLHttpRequest();
}
else if(...){
return new ActiveXObject("MSXML2.XMLHttp");
}
...
}
var xmlhttp=XMLHttpFactory.createXMLHttp();
JavaScript的面向对象机理1)-类
JavaScript中的"类",其实就是函数
下面来简单来一个"类"的例子:
//MyClass本质上就是一个函数,但可以当类来使用
function MyClass(name){
this.name = name;
}
var m = new MyClass("fanrong");
alert(m.name);
</script>
这里成功定义和使用了一个自定义的JavaScript类:MyClass.
所谓类的实例化是如何实现的?
在JavaScript中,"类"实例化一个对象,是依靠JavaScript可动态增加属性的特点来实现的,如下示例:
var a = {};
a.name = "fanrong";
alert(a.name);
</script>
当我们定义变量a时,还没有name属性,但是我们在接下来的一行代码中,"直接"对a.name进行了赋值(这一点相当重要,这是JavaScript的函数之所以能成为类的关键所在).
然后再回到我们的MyClass类,它其实是一个函数,当你在new MyClass("fanrong")时,你做了两件事:1>你用new关键字创建了一个object类型的对象,此时这个对象很干净,基本没有任何的属性.2>你开始调用MyClass方法了,而且是以(newObject()).MyClass("fanrong")这样的形式调用的,这样,MyClass函数体内的this指针就指向了新创建的对象,而this.name = name,其实就相当与本例的a.name = "fanrong"了,依靠的,还是JavaScript的"动态添加属性"的特点.
另一种类实现方式—Prototype原型对象
好了,以上您已经了解了在JavaScript中怎么实现类的一种方法了,下面介绍另一种...不要急,JavaScript中一共也就2种实现类的方法,而下面这一种,更优秀且更通用,如prototype.js/aspnetajax等库都是主要基于下面这一种prototype原型对象的方式来实现的.
首先看例子:
function MyClass(){
}
MyClass.prototype.name = "fanrong";
var m = new MyClass();
alert(m.name);
</script>
这里的重点在于MyClass的prototype属性.这是JavaScript的又一个特点,prototype属性是每个JavaScript对象都具备的,从它动态扩展的属性/方法等,能"自动"赋予被new的对象.因此,alert(m.name)会显示"fanrong",而不是undefined.
prototype这个属性好象天生就是为了设计JavaScript的类而出现的...它的优点在于它是"自动"赋予被实例化对象的,这一点很重要.你可以看到,在本例的MyClass函数体内,没一行代码使变量m与name属性发生关系,但是name在MyClass实例化之后,使m具备了name属性.
也许有人疑惑本例中由prototype定义的类属性好象不能初始化,而第一例就可以,其实不然.下面我来演示下:
function MyClass(name){
alert(this.name);
this.name = name;
}
MyClass.prototype.name = "haha";
var m = new MyClass("fanrong");
alert(m.name);
</script>
在实例化时,首先会弹出一个对话框,显示this.name的值,这里会显示"haha".说明当进入到MyClass函数体内部时,this.name已经具备了(由prototype优先赋予了).而如果是第一个例子,而是"undefined",是还未定义的!这一点区别很重要.
如何区别类和实例对象
这个其实很简单,如下:
function MyClass(name){
this.name = name;
}
MyClass.prototype.name = "haha";
var m = new MyClass("fanrong");
//区分类与实例对象
alert(typeof(MyClass)); //显示 function
alert(typeof(m)); //显示 object
</script>
...类就是function,而实例对象就是object...
简单吧~
如何判断对象是否是某一个类的实例
这里的技巧是使用instanceof方法,如下:
function A(){}
function B(){}
var m = new A();
alert(m instanceof(A));//true
alert(m instanceof(B));//false
</script>
应该m是A的实例,所以显示true,而B则不是,所以显示false了.也还是比较简单的.
如何运用这两种类定义方法
以我的看法,设计一个类时,在prototype上定义所有的属性和方法,而在函数体内进行初始化工作,如下示例:
function MyClass(name){
this._name = name; //在函数体内初始化属性
}
MyClass.prototype = { //在prototype上定义属性和方法
_name: null,
showName: function(){
alert(this._name);
}
}
var m = new MyClass("fanrong");
m.showName();
</script>
好了,到这里,基本已经讲JavaScript的"类"实现的两种方法了:一是依靠JavaScript的动态属性扩展能力,二是依靠JavaScript的prototype原型对象.
JavaScript的面向对象机理2)-继承
一个继承机制可以有多简单?
以下是一个最简单的实现继承基类属性和方法的示例:
//声明继承自哪一个基类
Function.prototype.extend = function(baseClass){
this.baseClass = baseClass;
}
//初始化基类的属性(其实这个函数才具体实现"继承"基类属性和方法)
Function.prototype.initializeBase = function(){
if(typeof(this.baseClass) === 'undefined') return;
var self = arguments[0];
if(!self) return;
this.baseClass.apply(self,arguments[1]);
}
//定义一个基类
function MyBase(name){
this.name = name;
this.showName = function(){
alert(this.name);
}
}
//定义一个扩展类
function MyClass(name,age){
MyClass.initializeBase(this,[name]);//初始化基类
this.age = age;
this.showAge = function(){
alert(this.age);
}
}
MyClass.extend(MyBase);//声明继承自MyBase
var m = new MyClass("fanrong",25);
m.showName();
m.showAge();
</script>
这种类的设计方式是依赖于JavaScript的动态属性扩展能力而实现的,因此这种方式的继承机制,不是在于"基类"与"派生类"之间的属性/方法继承,"派生类"其实与"基类"没关系,"派生类"只是通过extend而知道了自己的基类,具体的基类的属性和方法,要由被实例化的对象自己去基类的构造函数那获取.
这种继承实现方式如果非要用一个比喻来形容的话,就仿佛是...应该是你的老板A直接付你工资,但是他不,他不乐意(我靠!).他说某老板B欠他钱,他有凭据(A.extend(B)),他要求你去找B要去(A.initializeBase(this)),幸好,B还算有信,二话不说给了你钱(B.apply(self,[...]).
:)
真正的派生类与基类之间继承
要实现真正的,派生类通过extend方法就完全获得基类的属性和方法,则必须依靠prototype,这也是类定义的两种方法之一.具体请看如下示例:
Function.prototype.extend = function(baseClass){
for(var ptototypeName in baseClass.prototype){
if(typeof(this.prototype[ptototypeName]) === 'undefined'){
this.prototype[ptototypeName] = baseClass.prototype[ptototypeName];
}
}
}
function MyBase(name){
}
MyBase.prototype = {
name: null,
showName: function(){
alert(this.name);
}
}
function MyClass(name,age){
this.name = name;
this.age = age;
}
MyClass.prototype = {
age: null,
showAge: function(){
alert(this.age);
}
}
MyClass.extend(MyBase);
var m = new MyClass("fanrong",25);
m.showName();
m.showAge();
</script>
还是那个比喻...你的老板A幡然悔悟,他强烈要求即时付你工钱,决不让你自己去讨要第三方欠他的债...因此你就不同到B那跑一趟了,尽管B这人还是不错的...
继承机制的完美实现
其实就是把上面的两种方法结合起来...
如下:
Function.prototype.extend = function(baseClass){
this.baseClass = baseClass;
for(var ptototypeName in baseClass.prototype){
if(typeof(this.prototype[ptototypeName]) === 'undefined'){
this.prototype[ptototypeName] = baseClass.prototype[ptototypeName];
}
}
}
Function.prototype.initializeBase = function(){
if(typeof(this.baseClass) === 'undefined') return;
var self = arguments[0];
if(!self) return;
this.baseClass.apply(self,arguments[1]);
}
function MyBase(name,sex){
this.name = name;
this.sex = !!sex;
this.showSex = function(){
if(!!sex){
alert("男");
}else{
alert("女");
}
}
}
MyBase.prototype = {
name: null,
showName: function(){
alert(this.name);
}
}
function MyClass(name,age,sex){
MyClass.initializeBase(this,[name,sex]);
this.age = age;
}
MyClass.prototype = {
age: null,
showAge: function(){
alert(this.age);
}
}
MyClass.extend(MyBase);
var m = new MyClass("fanrong",25,true);
m.showName();
m.showAge();
m.showSex();
</script>