转自(http://vinge.bokee.com/6239364.html)
关键词: 面向对象 JavaScript C#
类
尽管JavaScript将一个数据类型称为“对象”,但JavaScript并没有正式的“类”的概念。传统观念认为一门“面向对象的程序设计语言”应该是强类型的,并且提供基于类的继承。从这个标准出发,JavaScript绝不是一门真正的面向对象的语言。但另一方面,JavaScript大大的使用了对象,并且实现了自己的基于原型的继承。JavaScript是一门真正的面向对象的语言。
JavaScript中的对象可以有各种各样的属性,并且属性可以动态增加。这与强类型语言如c#是不同的,在强类型语言中对象只能有一套预定义的属性集,并且每个属性的类型也是一定的。
如果想用JavaScript来模拟面向对象的程序设计,通常是预先定义好对象的属性集以及每个属性的数据类型。
C#用类来定义对象的结构,类描述了一个对象拥有哪些属性以及每个属性是什么类型,也描述对象有哪些方法。JavaScript可以用构造函数和原型来近似的实现类。
下面模拟类成员的四种基本类型:实例属性(这里所说的更像是c#类中的公开字段,至于c#的属性只能用访问器函数模拟),实例方法(c#常常称之为方法),类属性(更像是c#类中的静态字段),类方法(c#常常称之为静态方法)。
实例属性:
JavaScript中的任何属性都默认是实例属性,为了模拟的真实性,我们说实例属性是那些在构造函数中创建和初始化的属性。(在构造函数中创建的属性将在每个实例中保持一份副本,跟c#同。)
调用方法跟c#一样,都是o.p;
实例方法:
实例方法跟实例属性很相似,只不过它是方法而不是数值(JavaScript中方法跟属性都是数据,所以区别不大。)
在方法体内用this来引用调用它的对象。跟c#一样,方法也不像字段那样在每个对象中都有一份副本,方法只存储一次并由所有实例共享。JavaScript定义实例方法的方法是给构造函数的prototype属性对象的某个属性赋予一个函数值,例如:
Rectangle.prototype.area = function( ) {
return this.width * this.height;
}
}
调用方法跟c#一样:o.m();
类属性:
通过给构造函数定义属性可以模拟类属性。例如,我们已经有了Circle()这个构造函数了,那么我们可以通过Circle.PI=3.14;来模拟一个类属性。(JavaScript中函数也是对象,所以允许我们给函数创建属性。)
类方法:
JavaScript通过将一个函数作为构造函数的某个属性即可定义一个类方法。
例子:
// 构造函数
function Circle(radius) { .
this.r = radius; // r是一个实例属性,在构造函数中创建和初始化的。
}
Circle.PI = 3.14159; // Circle.PI是一个类属性,构造函数的属性。
Circle.prototype.area = function( ) { return Circle.PI * this.r * this.r; } //area()是一个实例方法。
Circle.max = function(a,b) { if (a.r > b.r) return a; else return b;} // max()是一个类方法
// 调用
var c = new Circle(1.0); // 创建 Circle 类的一个对象c。
c.r = 2.2; // 设置实例属性
var a = c.area( ); // 调用实例方法
var x = Math.exp(Circle.PI); // 使用类属性
var d = new Circle(1.2); // 建立另一个对象d
var bigger = Circle.max(c,d); //调用类方法max().
上面讨论的这些似乎并没有体现“数据封装”的原则,为了能跟熟悉的c#中的一些概念联系起来,讨论如下:
私有成员:
c#中常常将字段声明为私有,有必要的话就将它们公开为属性。声明私有成员符合数据封装的原则。JavaScript中似乎不能很好的模拟C#中的“属性”,但可以模拟java的方式,即特定的访问器函数(包括写访问器,读访问器),这需要用“闭包”来模拟。但为了实现它们,必须使每个对象都存储一份访问器函数的副本,它们不能从原型继承。eg:
function ImmutableRectangle(w, h) {
// 构造函数不再让每个对象都存储一份width、height的副本。作为代替,它为每个对象定义了访问器函数。这些函数是闭包的并且width、height被限制在他们自己的作用域链内。
this.getWidth = function( ) { return w; }
this.getHeight = function( ) { return h; }
}
// 类可以在原型对象上定义其他常规的函数
ImmutableRectangle.prototype.area = function( ) {
return this.getWidth( ) * this.getHeight( ); //在“本类”的方法中也必须通过访问器函数读写w/h,回头看闭包。
};
补充:
构造函数的参数、构造函数内的var变量是私有的。eg:
function Container(param) { //param是私有的
this.member = param; //member是公开的,符合“数据封装”的公开方式是像上面一样构建getMember()/setMember()访问器
var secret = 3; //secret是私有的
}
构造函数内嵌的函数私有的。eg:
function Container(param) {
function secretMethod() {
if (secret > 0) {
return true;
} else {
return false;
}
}
this.member = param;
var secret = 3;
}
超类子类
JavaScript支持以原型为基础的继承,而不是以类为基础的继承。Object类是最普通的类,所有JavaScript内建的类都是Object类的子类,它们都从Object类继承了基本的方法。
我们知道“原型”对象本身就是一个对象,它是由Object()构造函数创建的。这就意味着“原型”对象本身就从Object.prototype继承属性。以“原型”为基础的继承不仅仅局限于继承一个“原型”对象,而是继承原型对象的那一串“链”。因此,complex对象从Complex.prototype继承属性,也从Object.prototype继承属性(因为Complex.prototype对象从Object.prototype中继承属性)。
当访问一个complex对象的属性时,首先查询这个对象本身,如果该属性未找到,就查询Complex.prototype对象,如果还没找到,就查询Object.prototype对象。
注意,由于Complex.prototype对象是在Object.prototype对象之前被查询的,所以Complex.prototype的属性就隐藏了Object.prototype的同名属性。
要创建一个子类,只要确保子类的“原型”对象是父类的一个实例即可,这样它就会继承父类的“原型”的所有属性。eg:
// 一个简单的Rectangle类,有width,height属性和area()方法
function Rectangle(w, h) {
this.width = w;
this.height = h;
}
Rectangle.prototype.area = function( ) { return this.width * this.height; }
// 子类化Rectangle类(扩充x,y坐标)
function PositionedRectangle(x, y, w, h) {
// 首先用call()方法调用父类的构造函数。
// call()会将Rectangle()构造函数作为this对象(我们要初始化的目标)的一个方法来调用。回头理解call()函数。
Rectangle.call(this, w, h);
this.x = x;
this.y = y;
}
//如果我们使用定义PositionedRectangle( )构造函数时默认生成的prototype对象,我们只会得到一个Object类的子类。所以为了子类化Rectangle类,我们必须显式的创建对象。
PositionedRectangle.prototype = new Rectangle( ); //这个对象有width,height属性,在子类的原型中需要有width,height属
//性吗?下一步还要删除。
// 我们在上面创建的Rectangle对象只是出于继承的目的,我们并不想继承这个Rectangle对象的width,height属性,所以我们还要从原型中删除它们。
delete PositionedRectangle.prototype.width;
delete PositionedRectangle.prototype.height;
// 因为prototype对象是用Rectangle( )构造函数创建的,所以它的constructor属性指向的还是Rectangle( )构造函数。但是我们想让PositionedRectangle对象有自己的constructor属性,所以我们必须重新设置constructor属性。
PositionedRectangle.prototype.constructor = PositionedRectangle;
// 到此为止,我们已经成功配置了子类的“原型”对象,我们可以增加实例方法了。
PositionedRectangle.prototype.contains = function(x,y) {
return (x > this.x && x < this.x + this.width &&
y > this.y && y < this.y + this.height);
}
//使用
var r = new PositionedRectangle(2,2,2,2);
print(r.contains(3,3)); // 调用实例方法
print(r.area( )); // 调用继承的实例方法
// 使用实例属性
print(r.x + ", " + r.y + ", " + r.width + ", " + r.height);
// r对象符合三个类型。
print(r instanceof PositionedRectangle &&
r instanceof Rectangle &&
r instanceof Object);
用上面的方式好像麻烦异常,David Flanagan也讲述了不用继承扩展类功能的方法,因为实际工作中一般碰不到这么复杂的情况(包括上面那些),暂且略过。
补充:
命名空间
c#中,命名空间有助于将所创建的类型与可能在.net框架中其他地方存在的类型隔绝开来。同样JavaScript中使用命名空间可以防止两个模块中的同名变量相互冲突。eg:
// 建立一个空的对象作为我们的命名空间,这个全局唯一的符号将维护我们所有的代码
var Class = {};
// 在这个命名空间中定义函数
Class.define = function(data) { /* 实现代码 */ }
Class.provides = function(o, c) { /* 实现代码 */ }
注意,在这里并不是创建某个类的实例方法(甚至静态方法),而是创建了普通的函数并把他们的引用存储在一个特地创建的对象上而不是JavaScript默认的全局对象。(当然,这个特地创建的对象是作为全局对象的一个属性了。)
(其实就是用一个全局对象来充当了命名空间的角色。)
记住书写JavaScript模块的第一条规则:模块绝不应该向全局命名空间中加入超出一个的符号。对此有两条建议:
1、 如果一个模块要向全局命名空间加入一个符号,它的文档应该明确声明这个符号是什么。
2、 如果一个模块要向全局命名空间加入一个符号,这个符号的名称应该与它所在的文件的名称有明确的关系。
例如上面那段代码应该保存在名称为Class.js的文件里,并且在该文件的开头应该有如下的注释:
/**
* Class.js: A module of utility functions for working with classes.
*
* This module defines a single global symbol named "Class".
* Class refers to a namespace object, and all utility functions are stored as properties of this namespace.
**/
跟C#一样,JavaScript的命名空间也能反映出一种层次关系,例如,另一个称为Class的命名空间:
/**
* flanagan/Class.js: A module of utility functions for working with classes.
*
* This module creates a single global symbol named "flanagan" if it does not already exist.
* It then creates a namespace object and stores it in the Class property of the flanagan object.
* All utility functions are placed in the flanagan.Class namespace.
**/
var flanagan; // 声明一个唯一的全局符号 "flanagan"
if (!flanagan) flanagan = {}; // 如果尚未定义,创建它
else if (typeof flanagan!= "object") throw new Error("flanagan already exists and is not an object");
//如果flanagan已经存在,但不是对象,抛出异常。
// 注意:对全局符号flanagan,我们在判断它是否存在之前先用var声明了一下。这是因为如果试图读一个未声明的全局变量将触发异常,但如果读一个声明了但未定义的符号将获得undefined值。这是全局对象独有的特征,如果你读的是一个命名空间对象的不存在的属性,将获得undefined值而不会触发异常。
flanagan.Class = {} // 现在创建 flanagan.Class 命名空间
// 在命名空间里定义我们的函数
flanagan.Class.define = function(data) { /* 实现代码 */ };
flanagan.Class.provides = function(o, c) { /* 实现代码 */ };
使用一个模块之前,最好先测试一个这个模块是否可用,eg:
var flanagan; // 在测试一个全局符号之前应该先声明一下
if (!flanagan || ! flanagan.Class)
throw new Error("Flanagan/Class.js has not been loaded");
如果模块的作者遵循了惯例,比如定义了命名空间的VERSION属性,那么不但可以判断模块是否可用还可以判断模块版本。