使用Modello编写JavaScript类

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

(0)

相关推荐

  • 使用Modello编写JavaScript类

    From:http://www.ajaxwing.com/index.php?id=2 一,背景 回顾一下编程语言的发展,不难发现这是一个不断封装的过程:从最开始的汇编语言,到面向过程语言,然后到面向对象语言,再到具备面向对象特性的脚本语言,一层一层封装,一步一步减轻程序员的负担,逐渐提高编写程序的效率.这篇文章是关于 JavaScript 的,所以我们先来了解一下 JavaScript 是一种怎样的语言.到目前为止,JavaScript 是一种不完全支持面向对象特性的脚本语言.之所以这样说是因

  • 极简主义法编写JavaScript类

    这个所谓的"极简主义法"是荷兰程序员Gabor de Mooij提出来的,这种方法不使用this和prototype,代码部署起来非常简单,这大概也是它被叫做"极简主义法"的原因.下面就介绍如何使用极简主义法完成JavaScript的封装和继承 1. 封装 首先,它也是用一个对象模拟"类".在这个类里面,定义一个构造函数createNew(),用来生成实例. var Cat = { createNew: function(){ // some c

  • 结合 ES6 类编写JavaScript 创建型模式

    目录 前言 什么是设计模式? 创建型设计模式 1. 工厂模式 实例 2. 抽象工厂 实例 3. 构建器模式 实例 4. 原型模式 实例 5. 单例模式 实例 前言 什么是设计模式? 设计模式是软件设计中常见问题的解决方案,这些模式很容易重复使用并且富有表现力. 在软件工程中,设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案.它并不直接用来完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案.面向对象设计模式通常以类别或对象来描述

  • 使用CoffeeScrip优美方式编写javascript代码

    JavaScript无疑是在web最伟大的发明之一,几乎一切网页动态效果都是基于它丰富的计算能力.而且它的能力在各种新的JavaScript的Engine下也越来越强了,比如Google Chrome用的V8 Engine. 但是由于诞生的太早,有很多语法定义在今天看来有些效率低下了,一些更加先进的语法形式,由于历史原因,没办法加入到现在的JavaScript语言中,可以说一种遗憾. 世界上的很多天才都在为构建更好的JavaScript而努力.已经有了很多尝试,其中最有前途的,无非就是Coffe

  • 结合ES6 编写 JavaScript 设计模式中的结构型模式

    目录 前言 什么是设计模式? 结构型设计模式 适配器模式 实例 桥接模式 实例 组合模式 实例 装饰者模式 实例 门面模式 实例 享元模式 实例 代理模式 实例 前言 本文将对 20 多种 JavaScript 设计模式进行简单概述,然后结合 ES6 类的方式来编写实例代码展示其使用方式. JavaScript 在现代前端中扮演重要的角色,相比过去能够做的事情已经不在一个级别上了.JavaScript 最大的特征是其灵活性,一般只要敢想敢写,可以把程序写得很简单,有可以写得很复杂.其灵活性导致编

  • 一个简单的javascript类定义例子

    复制代码 代码如下: <script> //定义一个javascript类 function JsClass(privateParam/* */,publicParam){//构造函数 var priMember = privateParam; //私有变量 this.pubMember = publicParam; //公共变量 //定义私有方法 function priMethod(){ return "priMethod()"; } //定义特权方法 //特权方法可以

  • javascript 类和命名空间的模拟代码

    先上一段最简单的: 复制代码 代码如下: // 以下几行代码展示了命名空间.类.以及函数的模拟定义和使用: NameSpace = {}; NameSpace.Class = function(){ this.Method = function(info){alert(info);} }; new NameSpace.Class().Method("Hello world"); 再来一些可见到的,各种情况的代码 1.类的模拟 复制代码 代码如下: // 类定义 function Cla

  • 非常不错的一个javascript 类

    非常不错的一个javascript 类 复制代码 代码如下: /*    *  Author:aoao  *    Homepage:http://www.loaoao.com  *  Email:loaoao@gmail.com / QQ:2222342  *  Copyright (c) 2006 aoao  *  Licensed under a Creative Commons Attribution 2.5 License (http://creativecommons.org/lic

  • 【经验总结】编写JavaScript代码时应遵循的14条规律

    本文讲述了编写JavaScript代码时应遵循的14条规律.分享给大家供大家参考,具体如下: 1. 总是使用 'var' 在javascript中,变量不是全局范围的就是函数范围的,使用"var"关键词将是保持变量简洁明了的关键.当声明一个或者是全局或者是函数级(function-level)的变量,需总是前置"var"关键词,下面的例子将强调不这样做潜在的问题. 不使用 Var 造成的问题 var i=0; // This is good - creates a

  • 在Javascript类中使用setTimeout第1/2页

    最近遇到了一道 Javascript 考题,内容如下: 尝试实现注释部分的 Javascript 代码,可在其他任何地方添加更多 代码(如不能实现,说明一下不能实现的原因): var Obj = function(msg){    this.msg = msg;    this.shout = function(){       alert(this.msg);    } this.waitAndShout = function(){       // 隔五秒钟后执行上面的 shout 方法  

随机推荐