标准化面象对象代码
编写可重用代码的第一个也是最重要的步骤就是以一种贯穿整个应用程序的标准方式编写你的代码,尤其是面向对象的代码。通过上一章的面向对象的JavaScript的运作方式,你可以看到JavaScript语言相当灵活,允许你模拟许多种不同的编程风格。
作为开端,设计出一种最符合你需要的编写面向对象代码并实现对象继承(把对象的属性克隆到新的对象里)的体制是很重要的。然而表面看来,每一个写过一些面向对象的JavaScript代码的人都已经建立起了各自的实现方案,这可能相当令人困惑的。在这一节中,我们将弄清JavaScript的中继承是怎样工作的,随后了解几种不同的供选择的辅助方法的原理以及怎样将它们应用于你的程序当中。
原型继承
JavaScript使用了一种独特的对象创建和继承的方式,称为原型继承(prototypal inheritance)。这一方法背后的前提(相对大多数程序员所熟悉的传统的类/对象方案而言)是,一个对象的构造器能够从另一个对象中继承方法,建立起一个原型对象,所有的新的对象都将从这个原型创建。
这整个过程由prototype属性(存在于每一个函数中,因为任何函数都可以是一个构造器)促成。原型继承是为单继承设计的;尽管如此,仍然存在可以实现多继承的手段,我将在下一节中讨论。
使得这种形式的继承特别难以掌握的是,原型并不从其它的原型或者其它的构造器继承属性,而是从实际的对象中继承。程序3-1展示了prototype属性怎样被用于简单继承的几个例子。
程序3-1. 原型继承的例子
//创建Person对象的构造器
function Person( name ) {
this.name = name;
}
//为Person对象加入一个新方法
Person.prototype.getName = function() {
return this.name;
};
//创建一个新的User对象构造器
function User( name, password ) {
//注意这并不支持优雅的重载/继承,
//如能够调用超类的构造器
this.name = name;
this.password = password;
};
//User对象继承Person对象的全部方法
User.prototype = new Person();
//我们添加一个自己的方法给User对象
User.prototype.getPassword = function() {
return this.password;
};
上例中最重要的一行是User.prototype = new Person();。我们来深入地看看这到底意味着什么。User是对User对象的函数构造器的引用。new Person()建创一个新的Person对象,使用Person构造器。将这一结果设为User构造器的prototype的值,这意味着不论任何时候你使用new User()的时候,新建的User类对象也将拥有你使用new Person()时创建的Person类对象的所有方法。
带着这一特殊的技巧,我们来看一些不同的开发者所编写的使得JavaScript中继承的过程简单化的封装。
类继承
类继承(classical inheritance)是多数开发者所熟悉的一种形式,拥有带方法的可被实例化为对象的类。对初学面向对象JavaScript的程序员来说这种情况是非常典型的:试图模拟这种程序构思,却很少真正悟出怎样正确地实现。
值得感激的是,JavaScript大师之一,Douglas Crockford,把开发一套能用于JavaScript模拟类式继承的简单方法做为了他的目标,如他在网站上所解释的那样( http://javascript.crockford.com/inheritance.html)。
程序3-2展示了他所编写的三个函数,用来建立起一种类风格的JavaScript继承的综合形式。每个函数实现了继承的一个方面:继承单个函数,继承单个父类的全部,和从多个父类中继承独立的方法。
程序3-2. Douglas Crockford的使用JavaScript模拟类形式继承的三个函数
//一个简单的辅助函数,允许你为对象的原型绑定新的函数
Function.prototype.method = function(name, func) {
this.prototype[name] = func;
return this;
};
//一个(相当复杂的)函数,允许你优雅地从其它对象中继承函数,
//同时仍能调用"父"对象的函数
Function.method('inherits', function(parent) {
//追踪所处的父级深度
//Keep track of how many parent-levels deep we are
var depth = 0;
//继承parent的方法
//Inhert the parent's methods
var proto = this.prototype = new parent();
//创建一个名为uber的新的特权方法,
//调用它可以执行在继承中被覆盖的任何函数
//Create a new 'priveledged' function called 'uber', that when called
//executes any function that has been written over in the inheritance
this.method('uber', function uber(name) {
var func; //将被执行的函数(The function to be execute)
var ret; // 该函数的返回值(the return value of then function)
var v = parent.prototype; //父类的prototype(The parent's prototype)
//如果已经位于另一"uber"函数内
//If we're already within another 'uber' function
if (depth) {
//越过必要的深度以找到最初的prototype
//Go the necessary depth to function the orignal prototype
for ( var i = d; i > 0; i += 1 ) {
v = v.constructor.prototype;
}
//并从该prototype取得函数
//and get the functin from that prototype
func = v[name];
//否则,这是第一级的uber调用
//Otherwise, this is the first 'uber' call
} else {
//从prototype中取得函数
//Get the function to execute from the prototype
func = proto[name];
//如果该函数属于当前的prototype
//If the function was a part of this prototype
if ( func == this[name] ) {
//则转入parent的prototype替代之
//Go to the parent's prototype instead
func = v[name];
}
}
//记录我们位于继承栈中的'深度'
//Keep track of how 'deep' we are in the inheritance stack
depth += 1;
//使用用第一个参数后面的所有参数调用该函数
//(第一个参数保有我们正在执行的函数的名称)
//Call the function to execute with all the arguments but the first
//(whick holds the name of the function that we're executing)
ret = func.apply(this, Array.prototype.slice.apply(arguments, [1]));
//重置栈深度
//Reset the stack depth
depth -= 1;
//返回执行函数的返回值
//Return the return value of the execute function
return ret;
});
return this;
});
//一个用来仅继承父对象中的几个函数的函数,
//而不是使用new parent()继承每一个函数
Function.method('swiss', function(parent) {
//遍历所有要继承的方法
for (var i = 1; i < arguments.length; i += 1) {
//要导入的方法名
var name = arguments[i];
//将方法导入这个对象的prototype
this.prototype[name] = parent.prototype[name];
}
return this;
});
我们来看看这三个函数到底提供给我们些什么,以及为什么我们应该使用它们而不去试图写出我们自己的原型继承模型。这三个函数的前提是简单的:
Function.prototype.method:此函数是为构造器的prototype附加函数的简单方式。这一特殊的子句能够工作是因为所有的构造器都是函数,故能获得新的方法"method"。
Function.prototype.inherits:这一函数能用来提供简单的单父继承。函数代码的主体围绕着在你的对象的任何方法中调用this.uber("方法名")使之执行它所重写了的父对象的方法的能力。这是JavaScript继承模型本身不具备的一个方面。
Function.prototype.swiss:这是.method()函数的一个高级版本,能用来从一个父对象中抓取多个方法。当将它分别用于多个父对象时,你将得到一种实用的多父继承的形式。
对上面三个函数提供给我们什么有了一个大致的了解之后,程序3-3重拾你在3-1中所见的Person/User的例子,不过这次使用了新的类风格的继承。另外,你可以看看在改善程序清晰性方面,这个库能够提供怎样的额外功能。
程序3-3. Douglas Crockford的类继承式JavaScript函数的例子。
//创建一个新的Person对象构造器
function Person( name ) {
this.name = name;
}
//给Person对象添加方法
Person.method( 'getName', function(){
return name;
});
//创建新一个新的User对象构造器
function User( name, password ) {
this.name = name;
this.password = password;
},
//从Person对象继承所有方法
User.inherits( Person );
//给User对象添加一个新方法
User.method( 'getPassword', function(){
return this.password;
});
//重写新Person对象创建的方法,
//但又使用uber函数再次调用它
User.method( 'getName', function(){
return "My name is: " + this.uber('getName');
});
尝试过使用一个可靠的继承加强的JavaScript库所带来的可能性之后,我们再来关注其它的一些广通用的流行的方法。
Base库
JavaScript对象创建和继承领域近期的成果是Dean Edwards所开发的Base库。这一特别的库提供了一些不同的方式来扩展对象的功能。除此之外,它甚至提供了一种直觉式的对象继承方式。Dean最初开发这个库是为了用于他的其它的项目,包括IE7项目(作为对IE一整套的升级)。Dean的网站上列出的例子相当易于理解并确实很好的展示了这个库的能力: http://dean.edwards.name/weblog/2006/03/base 。除此而外,你可以在Base源代码目录里找到更多的例子: http://dean.edwards.name/base/。
Base库是相当冗长而复杂的,它值得用额外的注释来说明(包含于 http://www.apress.com的Source Code/Download所提供的代码中)。除了通读注释过的代码以外,强烈建议你去看Dean在他的网站上提供的例子,因为它们非常有助于澄清常见的疑惑。
但作为起点,我将带你一览Base库的几个可能对你的开发很有帮助的重要的方面。具体地,在程序3-4展示了类创建、单父继承和重写父类函数的例子。
程序3-4. 利用Dean Edwards的Base库进行简单的类创建和继承的例子
//创建一个新的Person类
var Person = Base.extend({
//Person类的构造函数
constructor: function( name ) {
this.name = name;
},
//Person类的简单方法
getName: function() {
return this.name;
}
});
//创建一个新的继承了Person类的User类
var User = Person.extend({
//创建User类的构造器,
constructor: function( name, password ) {
//该构造器顺次调用了父类的构造器方法
this.base( name );
this.password = password;
},
//为User类创建另一个简单的方法
getPassword: function() {
return this.password;
}
});
我们来看看在程序3-4中Base库是如何达到先前所归纳的三个目标从而创造出一种对象创建和继承的简单形式的。
Base.extend(...);:这一表达式用来创建一个新的基本的构造器对象。此函数授受一个参数,即一个简单的包含属性和值的对象,其中的属性都会作为原型方法被被增添到(所创建的构造器)对象中。
Person.extend(...);:这是Base.extend()语法的一个可替换版本。所有的创建的构造器都使用.extend()方法获取它们自己的.extend()方法,这意味着直接从它们继承是可能的。程序3-4中,正是通过直接从最初的Person构造器中直接继承的方式创建了User构造器。
this.base();:最后,this.base()方法用来调用父对象的被重写了的对象。你会发现这与Corockford's的类继承所使用的this.uber()函数截然是截然不同的,你无需提供父类的方法名(这一点有助于真正地清理并明晰化你的代码)。在所有的面向对象的JavaScript库中,Base库的重写父方法的功能是最好的。
个人而言,我觉得Dean的Base库能够出产最可读的、实用的和可理解的面向对象的JavaScript代码。当然,最终选择什么库要看开发者自己觉得什么最适合他。接下来你将看到面对对象的JavaScript代码如何在流行的Prototype库中实现。
Prototype库
Prototype是一个为了与流行的"Ruby on Rails"web框架协同工作而发的JavaScript库。不要把库的名字与构造器的prototype属性混淆——那是只一种令人遗憾的命名情况。
撇开命名不谈,Prototype库使得JavaScript外观和行为上者更接近于Ruby。为达到这一点,Prototype的开发者们利用了JavaScript的面向对象本质,并且附加了一些函数和属性给核心的JavaScript对象。不幸的是,该库根本不是由它的创造者们给出文档的;而幸运的是它写得非常清晰,而且它的一些用户介入编写了他们自己版本的文档。你们可以在Prototype的网站( http://prototype.conio.net/)上随意地浏览完整的代码,从文章 "Painless JavaScript Using Prototype"里得到Prototype的文档。
在这一节里,我们将仅着眼于Prototype用于创建其面象对象结构并提供基本继承的特定的函数和对象。程序3-5展示了Prototype使用的达到此目标的全部代码。
程序3-5. Prototype所使用的模拟面向对象JavaScript代码的两个函数
//创建一个名为"Class"的全局对象
var Class = {
//它拥有一个用来创建新的对象构造器的函数
create: function() {
//创建一个匿名的对象构造器
return function() {
//调用它本身的初始化方法
this.initialize.apply(this, arguments);
}
}
}
//为对象"Object"添加静态方法,用以从一个对象向另一个对象复制属性
Object.extend = function(destination, source) {
//遍历欲扩展的所有属性
for (property in source) {
//并将它添加到目标对象
destination[property] = source[property];
}
//返回修改过的对象
return destination;
}
Prototype确实只用了两个明显的函数来创建和维护其整个面向对象体系。你们可能已发现,仅通过看观察代码,也能断定它不如Base或者Crockford的类式方法那样强大。两个函数的前提很简单:
Class.create():这个函数简单地返回一个可用做构造器的匿名函数包装。这个简单的构造器做了一件事:调用和执行对象的initialze属性。这意味着,你的对象里至少有一个包含函数的initialize属性;否则,代码将会出错。
Object.extend():这个函数简单地从一个对象往另一个对象复制属性。当你使用构造器的prototype属性时你能设计出一种更简单的继承的形式(比JavaScript中可用的缺省的原型继承更简单)。
既然你已经了解了Prototype的底层代码是如何工作的,程序3-6展示了一些例子,说明它在Prototype库自身中是怎样用来通过添加功能层来扩展天然的JavaScript对象的。
程序3-6. Prototype怎样使用面对对象函数扩展JavaScript中字符串的缺省操作的例子。
//为String对象的原型添加额外的方法
Object.extend(String.prototype, {
//一个新的stripTags函数,删除字符串中的所有HTML标签
stripTags: function() {
return this.replace(//?[^>]+>/gi, '');
},
//将一个字符串转换成一个字符的数组
toArray: function() {
return this.split('');
},
//将文本"foo-bar"转换成'骆驼'文本"fooBar"(译注:fooBar中间的大写字符像是驼峰吧)
//Converts "foo-bar" text to "fooBar" 'camel' text
camelize: function() {
//以'-'拆分字符串
var oStringList = this.split('-');
//若字符串中没有'-'则提前返回
if (oStringList.length == 1)
return oStringList[0];
//随意地"骆驼化"字符串的开头
//Optionally camelize the start of the string
var camelizedString = this.indexOf('-') == 0
? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1)
/*
译注:this.indexOf('-')==0,那oStringList[0]显然就是空字符串了,
有必要toUpperCase加substring吗?
*/
: oStringList[0];
//将后继部分的首字母大写
for (var i = 1, len = oStringList.length; i < len; i++) {
var s = oStringList[i];
camelizedString += s.charAt(0).toUpperCase() + s.substring(1);
}
//返回修改的字符串
return camelizedString;
}
});
//stripTags()方法的一个例子
//可以看到它删除了字符串中的所有HTML
//只保留纯文本
"Hello, world!".stripTags() == "Hello, world!"
//toArray()方法的一个例子
//我们将得到字符串中的第四个字符
"abcdefg".toArray()[3] == "d"
//camelize()方法的例子
//它将原字符串转换成新的格式
"background-color".camelize() == "backgroundColor"
接下来,让我们再一次回到这章我所用到的那个有着User和Person对象且User对象从Person对象继承属性的例子。使用Prototype的面向对象风格的代码,见程序3-7。
程序3-7. Prototype的用于创建类和实现简单继承的辅助函数
//用名义上的构造器创建一个Person对象
var Person = Class.create();
//将下列的函数复制给Person的prototype
Object.extend( Person.prototype, {
//此函数立即被Person的构造器调用
initialize: function( name ) {
this.name = name;
},
//Person对象的简单函数
getName: function() {
return this.name;
}
});
//用名义上的构造器创建一个User对象
var User = Class.create();
//User对象从其父类继承所有属性
User.prototype = Object.extend( new Person(), {
//用新的初始化函数重写原来的
initialize: function( name, password ) {
this.name = name;
this.password = password;
},
//为对象添加一个新的函数
getPassword: function() {
return this.password;
}
});
尽管Prototype库所提出的面向对象技术不是革命性的,它们也强大到足以帮助开发者创建更简单、更易编写的代码了。然而,如果你正将编写数量巨大的面向对象代码,最终你可能更趋向于选择Base这样的库来辅助你的工作。
接下来我们将探讨怎样处理你的面向对象的代码,并使之准备好被其它的开发者或库所使用并与之相合。
[
本帖最后由 mozart0 于 2007-4-8 12:50 编辑 ]