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
|