在JavaScript中使用面向对象
前言
OO(面向对象)概念的提出是软件开发工程发展的一次革命,多年来我们借助它使得很多大型应用程序得以顺利实现。如果您还没有掌握并使用OO进行程序设计和开发,那么您无疑还停留在软件开发的石器时代。大多数编程语言,尤其是近年问世的一些语言,都很好的支持了面向对象,您可能对此了如执掌,但是一些语言在OO方面却无法与其它高级语言相比,在这些语言上进行面向对象程序设计和开发会有些困难,例如本文要讨论的JavaScript。JavaScript是一门古老的语言,但是随着近期Web2.0 技术的热捧,这门语言又重新焕发出青春的光辉,借助于JavaScript客户端技术,我们的Web体验变得丰富而又精彩,为了设计和开发更加完善、复杂的客户端应用,我们必须掌握JavaScript上的OO方法,这正是本文要讨论的。 前几天阅读了MSDN的《使用面向对象的技术创建高级 Web 应用程序》一文,觉得还有些东西有必要继续探讨补充一下,就有了本文。
JavaScript是一门相当灵活的语言,语法也相当宽松,并且入门门槛很低,您可以不费什么力气就编写出一大堆可以运行的代码,但是根据我在实际工作中的经验,多数人还是对之核心技术知之甚少。同样一个功能,简简单单几行代码,就可看出一个人的技术功底。正如天龙八部中的萧峰使用的一招“太祖长拳”,这是一种武术中的入门的招法,虽然它看上去很简单,但是在高手的使用下,却是威力无穷。其实越是简单的东西,要把它变得完美就越是困难。所以作为能工巧匠的您怎能错过这篇文章?切听我一一道来。
在JavaScript我们可以使用下面几种代码进行对象声明:
{
blablabla
}
对于后两种方法,我们还可以增加参数,这样就类似于一个带参数的构造器了,例如:
{
alert(msg);
}
var o = new MyObject('Hello world!');
var MyObject = function(msg)
{
alert(msg + ' again');
};
var o = new MyObject('Hello world!');
甚至我们可以使用字符串来声明函数,这使得我们的程序更加灵活:
var o = new MyObject('Hello world!');
成员的声明
在JavaScript中,要声明一个对象的成员也非常简单,但是跟其它的高级程序仍然略有不同,请看下面的示例:
FirstName:"Mary",
LastName:"Cook",
Age:18,
ShowFullName : function(){
alert(this.FirstName + ' ' + this.LastName);
}
}
MyObject.ShowFullName();
或者使用字符串来声明:
"FirstName":"Mary",
"LastName":"Cook",
"Age":18,
"ShowFullName" : function(){
alert(this.FirstName + ' ' + this.LastName);
}
}
MyObject.ShowFullName();
用字符串的声明方式有诸多好处,这也是JavaScript中表示对象的一种特殊方式,像近年JSON概念的提出,将这种特殊方式提示到了一个新的高度,更多JSON的介绍请参加我以前的大作《深入浅出JSON》。
而在实际的程序设计中,这种方式在JavaScript的面向对象程序设计中我们通常用来映射数据类型,定义类似高级语言中的结构,集合,实体等,还常常用作定义静态帮助器类,无需构造而可以直接访问成员方法。例如上面代码中的MyObject.ShowFullName();
前面我们介绍了成员的定义,在JavaScript中另一个面向对象特点是我们可以像高级编程语言一样使用.和[]引用成员,如:
alert(DateTime.Now);
// 等价于:
alert(DateTime["Now"]);
DateTime.Show()
// 等价于:
DateTime["Show"]();
提到方法调用,这里有一些知识需要知道,在JavaScript中,所有的对象的基类是Object,基类通过prototype定义了很多成员和方法,例如toString, toLocaleString, valueOf等
这里我以toString()来做一介绍,请看下面示例:
alert(obj);
我们注意到当alert的时候,toString()方法被调用了,事实上,当javascript需要将一个对象转换成字符串时就隐式调用这个对象的toString()方法,例如alert或者document.write或者字符串需要进行+运算等等。参加下面示例代码:
var dt= new Date(new Date());
Date.prototype.toString = function(){ alert('This is called');}
var dt= new Date() + 1;
通过这个例子我们验证了这一点,即使一个对象作为入口参数也可能会调用其toString方法。除了这一点外,该示例同时演示了如何覆盖基类中定义的方法。
在JavaScript中,在全局上下文中声明的变量作为全局变量,而在对象或方法内部声明的对象则作为本地变量。请参见下面的代码:
function mm()
{
var global = 2; // 声明本地变量
alert(this.global); // 等价于alert(global);
}
mm();
alert(this.global); // 等价于alert(global);
上面例子我们可以看出本地变量和全局变量即使同名也不会出现冲突。
另外Javascript有一个特性就是变量不用声明就可以使用,在第一次使用一个未声明的变量时,系统会自动声明该变量,并将其作为全局变量。但是在构建大型应用程序的时候,这一点是非常具有破坏性的,如果该变量名在多个脚本块中出现,引起变量名冲突,导致严重的程序错误。因此,我们应该尽量避免使用全局变量,并且保持先声明后使用的良好习惯。
在JavaScript中this关键字是比较重要的一个特点,它会随调用对象而发生改变,始终与当前对象的上下文保持一致,这里一个例子让我们演示this并且同时继续深入研究toString,首先我们使用构造器方式创建一个对象,代码如下:
toString = function() { return 'This is an object.'; }
}
alert(new obj());
你会发现当运行这段代码的时候,浏览器将会抛出一个错误。
下面我们再看另外两段代码:
aMethod = function() { return 'This is global method.'; }
}
alert(new obj()); // 正常执行
function obj(params){
this.toString = function() { return 'This is local method.'; }
}
alert(new obj()); // 正常执行
第一个函数声明虽没有使用this关键字,这时如果初始化对象那么将声明一个全局方法aMethod。第二个函数声明则为对象定义了一个自己的toString()方法。
当分析这两个函数的时候,你会注意到JavaScript的另一个特性,解释执行,所以
aMethod = function() { return 'This is global method.'; }
}
alert(aMethod()); // 此语句会报错
function obj(params){
aMethod = function() { return 'This is global method.'; }
}
new obj(); // 实例化的时候,声明了全局的aMethod()方法
alert(aMethod()); // 正常执行
通过上面的例子我们知道通过this非常重要,如果使用不当,可能造成全局函数的改变。有一点需要记住,绝不要调用包含“this”(却没有所属对象)的函数。否则,将违反全局命名空间,因为在该调用中,“this”将引用全局对象,而这必然会给您的应用程序带来灾难。
如下面的例子,当对象没有定义this指定的函数(isNaN)时,那么可能覆盖全局的同名函数, 看一些代码示例:
正确使用this的例子:
function obj(params){
this.toString = function() { return 'This is an object.'; }
this.isNaN = function() {
return "not anymore!";
};
}
var o = new obj(); // 正确的使用方式,调用构造函数
alert(o.isNaN(1)); // 此时obj定义中的this指向o这个实例而不是全局上下文
alert(isNaN(1)); // 全局函数未发生改变
错误的例子:
function obj(params){
isNaN = function() {
return "not anymore!";
};
}
obj(); // 错误的使用方式,this指向全局上下文,全局函数isNaN被覆盖
alert(isNaN(1)); // 全局函数发生改变
同时我们还注意到有一些全局函数则无法覆盖,例如toString()
下面我们看JavaScript的一个很好用的方法: call
关于call的解释:
call 方法可以用来代替另一个对象调用一个方法。call 方法可将一个函数的对象上下文从初始的上下文改变为由 thisObj 指定的新对象。
可以这样来理解:
我们定义了一个函数abc:
{
alert(this.member1);
}
var obj = { member1:"Hello world!", show:abc};
var obj2 = { member1:"Hello world again!", show:abc};
obj.show();
//也可以使用
abc.call(obj);
abc.call(obj2);
修改后的另一个版本:
function abc()
{
alert(this.member1);
}
var obj = { member1:"Hello world", show:abc};
var obj2 = { member1:"Hello world again", show:abc};
obj.show();
//也可以使用
abc.call(obj);
abc.call(obj2);
abc(); // 此时abc中的this指向了当前上下文
每个函数都有call方法,上面的过程中我们看到用另一个对象代替调用显示方法,并注意到this在对象上下文中的改变。
通过上面基础知识的研究,让我们再向前跨出一步,使用call的特性来实现类继承,参见下面示例:
function MyClassInitor()
{
this.base();
if(!this.mm)
alert('未定义成员函数: mm()');
return this;
}
// 定义一个基类
function baseClass()
{
if(!this.tt) // 判断该成员是否被继承类覆盖
this.tt = '基类成员';
}
// 从基类继承
var obj = { member1:"Hello world", base:baseClass, gg:function(){ alert('I am an GG');}};
var obj2 = { member1:"Hello world again", base:baseClass,mm:function(name){alert('I am MM '+name + '.');}, tt:"覆盖基类的tt成员"};
var o = MyClassInitor.call(obj);
var o2 = MyClassInitor.call(obj2);
alert(o.tt);
alert(o2.tt);
o2.mm('Mary');
虽然跟高级编程语言的语法有点不同,但是你必须了解JavaScript的语法特点。通过这个例子,我们什么分析了this和call的配合,但是实际进行类继承设计时往往不会采用此方法进行实现,后面我们介绍Prototype时再做详细介绍。
前面我们了解完类、对象的声明,下面看一下Javascript中命名空间的处理,大家知道,在高级编程语言中我们非常熟练的使用命名空间来避免变量或方法名的冲突,那么同样我们也可以在JavaScript中使用命名空间来为我们的类和方法进行界定。在JavaScript中命名空间的声明与其它高级语言略有不同,下面是一个命名空间声明的示例:
var System.Web = {};
通过这两行代码我们就有了System和System.Web两个命名空间,回想一下前面我们介绍的知识,你很快可以发现,这是两个对象声明语句。在JavaScript中,我们正是使用对象来表示命名空间的。但是你必须清楚一点,由于JavaScript的特性,在实际应用中,我们不能这么简单的来处理命名空间,因为声明语句可能同时出现在多个地方或者多个js文件中,我们知道,在JavaScript中,最后声明的变量会覆盖前面同名的变量,因此通常我们要加一些判断代码来防止重复声明,例如:
这样即使这段代码在程序中重复出现多次我们也可以保证System对象只声明一次。关于这一点,大家如果深入研究过AjaxPro和其它很多大型JavaScript框架,会发现当配合后端应用程序的时候,它是多么的有用。例如AjaxPro的类型注册,关于AjaxPro可参见我另一篇文章《AjaxPro框架剖析》。