一,背景
回顾一下编程语言的发展,不难发现这是一个不断封装的过程:从最开始的汇编语言,到面向过程语言,然后到面向对象语言,再到具备面向对象特性的脚本语言,一层一层封装,一步一步减轻程序员的负担,逐渐提高编写程序的效率。这篇文章是关于 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
// 上面的类均可替换成已注册的类名