欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  web前端

使用Modello编写JavaScript类_javascript技巧

程序员文章站 2022-04-03 17:18:12
...
From:http://www.ajaxwing.com/index.php?id=2


一,背景
回顾一下编程语言的发展,不难发现这是一个不断封装的过程:从最开始的汇编语言,到面向过程语言,然后到面向对象语言,再到具备面向对象特性的脚本语言,一层一层封装,一步一步减轻程序员的负担,逐渐提高编写程序的效率。这篇文章是关于 JavaScript 的,所以我们先来了解一下 JavaScript 是一种怎样的语言。到目前为止,JavaScript 是一种不完全支持面向对象特性的脚本语言。之所以这样说是因为 JavaScript 的确支持对象的概念,在程序中我们看到都是对象,可是 Javascipt 并不支持类的封装和继承。曾经有过 C++、Java或者 php、python 编程经验的读者都会知道,这些语言允许我们使用类来设计对象,并且这些类是可继承的。JavaScript 的确支持自定义对象和继承,不过使用的是另外一种方式:prototype(中文译作:原型)。用过 JavaScript 的或者读过《设计模式》的读者都会了解这种技术,描述如下:

每个对象都包含一个 prototype 对象,当向对象查询一个属性或者请求一个方法的时候,运行环境会先在当前对象中查找,如果查找失败则查找其 prototype 对象。注意 prototype 也是一个对象,于是这种查找过程同样适用在对象的 prototype 对象中,直到当前对象的 prototpye 为空。

在 JavaScript 中,对象的 prototype 在运行期是不可见的,只能在定义对象的构造函数时,创建对象之前设定。下面的用法都是错误的:

o2.prototype = o1;
/*
这时只定义了 o2 的一个名为“prototype”的属性,
并没有将 o1 设为 o2 的 prototype。
*/

// ---------------

f2 = function(){};
o2 = new f2;
f2.prototype = o1;
/*
这时 o1 并没有成为 o2 的 prototype,
因为 o2 在 f2 设定 prototype 之前已经被创建。
*/

// ---------------

f1 = function(){};
f2 = function(){};
o1 = new f1;
f2.prototype = o1;
o2 = new f2;
/*
同样,这时 o1 并不是 o2 的 prototype,
因为 JavaScript 不允许构造函数的 prototype 对象被其它变量直接引用。
*/

正确的用法应该是:

f1 = function(){};
f2 = function(){};
f2.prototype = new f1;
o2 = new f2;

从上面的例子可以看出:如果你想让构造函数 F2 继承另外一个构造函数 F1 所定义的属性和方法,那么你必须先创建一个 F1 的实例对象,并立刻将其设为 F2 的 prototype。于是你会发现使用 prototype 这种继承方法实际上是不鼓励使用继承:一方面是由于 JavaScript 被设计成一种嵌入式脚本语言,比方说嵌入到浏览器中,用它编写的应用一般不会很大很复杂,不需要用到继承;另一方面如果继承得比较深,prototype 链就会比较长,用在查找对象属性和方法的时间就会变长,降低程序的整体运行效率。

二,问题
现在 JavaScript 的使用场合越来越多,web2.0 有一个很重要的方面就是用户体验。好的用户体验不但要求美工做得好,并且讲求响应速度和动态效果。很多有名的 web2.0 应用都使用了大量的 JavaScript 代码,比方说 Flickr、Gmail 等等。甚至有些人用 Javasript 来编写基于浏览器的 GUI,比方说 Backbase、Qooxdoo 等等。于是 JavaScript 代码的开发和维护成了一个很重要的问题。很多人都不喜欢自己发明*,他们希望 JavaScript 可以像其它编程语言一样,有一套成熟稳定 Javasript 库来提高他们的开发速度和效率。更多人希望的是,自己所写的 JavaScript 代码能够像其它面向对象语言写的代码一样,具有很好的模块化特性和很好的重用性,这样维护起来会更方便。可是现在的 JavaScript 并没有很好的支持这些需求,大部分开发都要重头开始,并且维护起来很不方便。

三,已有解决方案
有需求自然就会有解决方案,比较成熟的有两种:

1,现在很多人在自己的项目中使用一套叫 prototype.js 的 JavaScript 库,那是由 MVC web 框架 Ruby on Rails 开发并使用 JavaScript 基础库。这套库设计精良并且具有很好的可重用性和跨浏览器特性,使用 prototype.js 可以大大简化客户端代码的开发工作。prototype.js 引入了类的概念,用其编写的类可以定义一个 initialize 的初始化函数,在创建类实例的时候会首先调用这个初始化函数。正如其名字,prototype.js 的核心还是 prototype,虽然提供了很多可复用的代码,但没有从根本上解决 JavaScript 的开发和维护问题。

2,使用 asp.net 的人一般都会听过或者用到一个叫 Atlas 的框架,那是微软的 AJAX 利器。Atlas 允许客户端代码用类的方法来编写,并且比 prototype.js 具备更好的面向对象特性,比方说定义类的私有属性和私有方法、支持继承、像java那样编写接口等等。Atlas 是一个从客户端到服务端的解决方案,但只能在 asp.net 中使用、版权等问题限制了其使用范围。

从根本上解决问题只有一个,就是等待 JavaScript2.0(或者说ECMAScript4.0)标准的出台。在下一版本的 JavaScript 中已经从语言上具备面向对象的特性。另外,微软的 JScript.NET 已经可以使用这些特性。当然,等待不是一个明智的方法。

四,Modello 框架
如果上面的表述让你觉得有点头晕,最好不要急于了解 Modello 框架,先保证这几个概念你已经能够准确理解:

JavaScript 构造函数:在 JavaScript 中,自定义对象通过构造函数来设计。运算符 new 加上构造函数就会创建一个实例对象
JavaScript 中的 prototype:如果将一个对象 P 设定为一个构造函数 F 的 prototype,那么使用 F 创建的实例对象就会继承 P 的属性和方法
类:面向对象语言使用类来封装和设计对象。按类型分,类的成员分为属性和方法。按访问权限分,类的成员分为静态成员,私有成员,保护成员,公有成员
类的继承:面向对象语言允许一个类继承另外一个类的属性和方法,继承的类叫做子类,被继承的类叫做父类。某些语言允许一个子类只能继承一个父类(单继承),某些语言则允许继承多个(多继承)
JavaScript 中的 closure 特性:函数的作用域就是一个 closure。JavaScript 允许在函数 O 中定义内部函数 I ,内部函数 I 总是可以访问其外部函数 O 中定义的变量。即使在外部函数 O 返回之后,你再调用内部函数 I ,同样可以访问外部函数 O 中定义的变量。也就是说,如果你在构造函数 C 中用 var 定义了一个变量V,用 this 定义了一个函数F,由 C 创建的实例对象 O 调用 O.F 时,F 总是可以访问到 V,但是用 O.V 这样来访问却不行,因为 V 不是用 this 来定义的。换言之,V 成了 O 的私有成员。这个特性非常重要,如果你还没有彻底搞懂,请参考这篇文章《Private Members in JavaScript》
搞懂上面的概念,理解下面的内容对你来说已经没有难度,开始吧!

如题,Modello 是一个允许并且鼓励你用 JavaScript 来编写类的框架。传统的 JavaScript 使用构造函数来自定义对象,用 prototype 来实现继承。在 Modello 中,你可以忘掉晦涩的 prototype,因为 Modello 使用类来设计对象,用类来实现继承,就像其它面向对象语言一样,并且使用起来更加简单。不信吗?请继续往下看。

使用 Modello 编写的类所具备如下特性:

私有成员、公共成员和静态成员
类的继承,多继承
命名空间
类型鉴别
Modello 还具有以下特性:

更少的概念,更方便的使用方法
小巧,只有两百行左右的代码
设计期和运行期彻底分离,使用继承的时候不需要使用 prototype,也不需要先创建父类的实例
兼容 prototype.js 的类,兼容 JavaScript 构造函数
跨浏览器,跨浏览器版本
开放源代码,BSD licenced,允许免费使用在个人项目或者商业项目中
下面介绍 Modello 的使用方法:

1,定义一个类

Point = Class.create();
/*
创建一个类。用过 prototype.js 的人觉得很熟悉吧;)
*/

2,注册一个类

Point.register("Modello.Point");
/*
这里"Modello"是命名空间,"Point"是类名,之间用"."分隔
如果注册成功,
Point.namespace 等于 "Modello",Point.classname 等于 "Point"。
如果失败 Modello 会抛出一个异常,说明失败原因。
*/

Point.register("Point"); // 这里使用默认的命名空间 "std"

Class.register(Point, "Point"); // 使用 Class 的 register 方法

3,获取已注册的类

P = Class.get("Modello.Point");
P = Class.get("Point"); // 这里使用默认的命名空间 "std"

4,使用继承

ZPoint = Class.create(Point); // ZPoint 继承 Point

ZPoint = Class.create("Modello.Point"); // 继承已注册的类

ZPoint = Class.create(Point1, Point2[, ...]);
/*
多继承。参数中的类也可以用已注册的类名来代替
*/

/*
继承关系:
Point.subclasses 内容为 [ ZPoint ]
ZPoint.superclasses 内容为 [ Point ]
*/

5,定义类的静态成员

Point.count = 0;
Point.add = function(x, y) {
return x + y;
}

6,定义类的构造函数

Point.construct = function($self, $class) {

// 用 "var" 来定义私有成员
var _name = "";
var _getName = function () {
return _name;
}

// 用 "this" 来定义公有成员
this.x = 0;
this.y = 0;
this.initialize = function (x, y) { // 初始化函数
this.x = x;
this.y = y;
$class.count += 1; // 访问静态成员

// 公有方法访问私有私有属性
this.setName = function (name) {
_name = name;
}

this.getName = function () {
return _getName();
}

this.toString = function () {
return "Point(" + this.x + ", " + this.y + ")";
}
// 注意:initialize 和 toString 方法只有定义成公有成员才生效

this.add = function() {
// 调用静态方法,使用构造函数传入的 $class
return $class.add(this.x, this.y);
}

}

ZPoint.construct = function($self, $class) {

this.z = 0; // this.x, this.y 继承自 Point

// 重载 Point 的初始化函数
this.initialize = function (x, y, z) {
this.z = z;
// 调用第一个父类的初始化函数,
// 第二个父类是 $self.super1,如此类推。
// 注意:这里使用的是构造函数传入的 $self 变量
$self.super0.initialize.call(this, x, y);
// 调用父类的任何方法都可以使用这种方式,但只限于父类的公有方法
}

// 重载 Point 的 toString 方法
this.toString = function () {
return "Point(" + this.x + ", " + this.y +
", " + this.z + ")";
}

}

// 连写技巧
Class.create().register("Modello.Point").construct = function($self, $class) {
// ...
}

7,创建类的实例

// 两种方法:new 和 create
point = new Point(1, 2);
point = Point.create(1, 2);
point = Class.get("Modello.Point").create(1, 2);
zpoint = new ZPoint(1, 2, 3);

8,类型鉴别

ZPoint.subclassOf(Point); // 返回 true
point.instanceOf(Point); // 返回 true
point.isA(Point); // 返回 true
zpoint.isA(Point); // 返回 true
zpoint.instanceOf(Point); // 返回 false
// 上面的类均可替换成已注册的类名

以上就是 Modello 提供的全部功能。下面说说使用 Modello 的注意事项和建议:

在使用继承时,传入的父类可以是使用 prototype.js 方式定义的类或者 JavaScript 方式定义的构造函数
类实际上也是一个函数,普通的 prototype 的继承方式同样适用在用 Modello 定义的类中
类可以不注册,这种类叫做匿名类,不能通过 Class.get 方法获取
如果定义类构造函数时,像上面例子那样提供了 $self, $class 两个参数,Modello 会在创建实例时将实例本身传给 $self,将类本身传给 $class。$self 一般在访问父类成员时才使用,$class 一般在访问静态成员时才使用。虽然 $self和$class 功能很强大,但不建议你在其它场合使用,除非你已经读懂 Modello 的源代码,并且的确有特殊需求。更加不要尝试使用 $self 代替 this,这样可能会给你带来麻烦
子类无法访问父类的私有成员,静态方法中无法访问私有成员
Modello 中私有成员的名称没有特别限制,不过用"_"开始是一个好习惯
Modello 不支持保护(protected)成员,如果你想父类成员可以被子类访问,则必须将父类成员定义为公有。你也可以参考 "this._property" 这样的命名方式来表示保护成员:)
尽量将一些辅助性的计算复杂度大的方法定义成静态成员,这样可以提高运行效率
使用 Modello 的继承和类型鉴别可以实现基本的接口(interface)功能,你已经发现这一点了吧;)
使用多继承的时候,左边的父类优先级高于右边的父类。也就是说假如多个父类定义了同一个方法,最左边的父类定义的方法最终被继承
使用 Modello 编写的类功能可以媲美使用 Atlas 编写的类,并且使用起来更简洁。如果你想用 Modello 框架代替 prototype.js 中的简单类框架,只需要先包含 modello.js,然后去掉 prototype.js 中定义 Class 的几行代码即可,一切将正常运行。


如果你发现 Modello 的 bug,非常欢迎你通过 email 联系我。如果你觉得 Modello 应该具备更多功能,你可以尝试阅读一下源代码,你会发现 Modello 可以轻松扩展出你所需要的功能。

Modello 的原意为“大型艺术作品的模型”,希望 Modello 能够帮助你编写高质量的 JavaScript 代码。

5,下载
Modello 的完整参考说明和下载地址:http://modello.sourceforge.net