KeeperJS 一个改进Javascript面向对象的框架

感兴趣的读者可以去我的资源里下载,或者加我QQ158828014

目录

目录... 1

序言... 1

1预备知识... 2

1.1        Javascript中的this指针... 2

1.2        Javascript原型继承prototype. 3

1.3        名字空间... 6

2.第一个Hello World. 6

3.包... 7

4接口... 7

4.1        接口的定义... 7

4.2        $implemented的使用... 8

5... 9

6抽象类... 10

7.继承... 11

7.1域的安全问题... 11

7.2超类构造... 12

7.3作用域... 13

7.4继承时方法覆盖的优先权... 14

8.多态... 14

8.1        重载(Overload... 15

8.2        覆盖(Override... 15

8.3        Multiple polymorphism.. 17

9.反射... 19

9.1从类声明中得到类信息... 19

8.2从类实例中得到类信息... 19

8.3可调用的API参考... 20

10.实现KeeperJS继承的原理... 20

10.1.实例克隆... 21

11$import命令... 22

12.集合容器类... 23

13.一些实用的方法... 24

13.1          一些常用的全局命令... 24

13.2          Javascript内置对象的加强... 24

14Javascript中的面向对象思考... 25

15AOP编程... 28

 

序言

Javascript,在设计动态网页中必不可少的语言。尽管Javascript小巧,灵活,易用,容易上手,但随着Ajax的不断流行,Javascript的作用越加重要了。但面对构建复杂的应用程序,显然Javascript还是过于复杂了。这些复杂性主要源于以下几个原因:

1.  Javascript是基于对象的,但却不是面向对象的语言。

尽管Javascript中存在对象的概念,但却缺少大部分面向对象语言所具有的特性。如继承,接口,多态,重载。虽然Javascript2.0中已经存在了classextends这些关键字的使用,但各个浏览器厂商却迟迟推出不了一个支持此版本的浏览器。包括最新的IE7依然不支持,虽然火狐将会在08年底推出最新的Firefox4中支持Javascript2.0,但即便如此也不能完全使用Javascript2.0的新特性,因为要考虑兼容原有的浏览器版本,至少要等到大部分用户都更新到最新版本的浏览器后,我们才敢大量使用新特性。

由于缺乏这些已被熟知的面向对象特性的支持,Javascript在结构和封装上的优化是比较困难复杂的。

2.  Javascript兼容性问题

尽管存在ECMAScript这样的标准存在,但是各个浏览器厂商依然不能够完全实现标准,而且在某些地方各个浏览器还存在较大差异,这给使用者也带来了诸多不便。为了兼容各个浏览器的不同,使得编写Javascript程序也变的复杂了不少。

3.  缺乏丰富的类库支持

我们使用Java的时候知道Java中拥有丰富的类库封装,这些封装好的类,帮助我们简化了开发难度,加快了开发周期。例如对于基本数据结构的实现。虽然我们可以去网上下载许多它人的封装,但是这些封装的写法习惯,使用习惯上不够统一,这对使用者来说还需要花费时间去适应。

为了更好的结构化Javascript的程序,解决以上的问题,我设计了KeeperJS框架。尽管现在同时存在很多其它解决Javascript面向对象的框架,但KeeperJS主要的目的就是提供一个适合Java使用者习惯的改进方案。如果你看了后面的演示后,你会发现KeeperJS里的所有语法非常接近于Java语言,这对于Java使用者将会很容易使用学习。同时KeeperJS还封装了一些基本的类库以便于开发Javascript应用,所有的类库都是接近于Java语言的,最典型的特征就是java.util包的实现。另外如果你了解AOP编程的话,KeeperJS也加入了对AOP编程的支持

         为了容易学习KeeperJS,你应该了解Javascript中的原型继承,名字空间,以及面向对象的一些概念,同时最好你是一个Java程序员。

1预备知识

1.1     Javascript中的this指针

         this指针是面向对象程序设计中的一个重要概念,它表示当前运行的对象。和其它面向对象语言不同,Javascript中的this指针是一个动态的变量,一个方法内的this指针并不是始终指向定义该方法的对象。(例如可以使用apply或者call方法动态修改this指针。)看一个例子:

*代码片断*

<script>

<!--

var obj1={};

var obj2={};

 

obj1.name="mathkeeper";

obj2.name="keeperJS";

 

obj1.getName=function(){

         alert(this.name);//表面上看似this指向的是obj1

}

 

obj1.getName();      //alert "mathkeeper"

obj2.getName=obj1.getName;

obj2.getName();      //alert "keeperJS"

//-->

</script>

         从代码上看,分别弹出了对话框显示了不同的内容。getName函数仅仅定义了一次,在不同的场合运行,却显示了不同的结果,这是由this指针的变化所决定的。执行obj1getName方法时,this就指向obj1,而在执行obj2getName方法时,this就指向了obj2对象。

         由此可见,JavaScript中的this指针是一个动态变化的变量,它表明了当前运行该函数的对象。由this指针的性质,可以更好的理解Javascript中对象的性质:一个对象就是由一个或者多个属性(方法)组成的集合。每个集合的元素不是仅仅属于一个集合(就像上例中的getName方法),而是可以属于多个集合。这样,一个方法由谁调用,this指针就指向谁。

1.2     Javascript原型继承prototype

         Javascript中定义一个类时使用function进行定义。例如:

*代码片断*

<script>

<!--

var MyClass=function(){};

var myClass=new MyClass();

alert(myClass);

//-->

</script>

         在这里你就定义了一个简单(没有方法,也没有属性)的类MyClass,并且可以使用关键字new创建类实例对象。

         在每个Function对象中以及String,Date这些内部类中都拥有一个prototype属性,Javascript就是利用该属性实现原型继承的。

*代码片断*

<script>

<!--

var Child=function(){};

var Parent=function(){

         this.name="mathkeeper";

}

Child.prototype=new Parent();

var child1=new Child();

var child2=new Child();

alert(child1.name); //alert “mathkeeper”

alert(child2.name); //alert “mathkeeper”

//-->

</script>

这里就简单得利用原型继承,Child并没有声明属性name,但是它的原型指向了一个Parent实例,所以所有的Child实例也就继承了这个name属性。

这里我们不长篇大论关于原型继承,详细的请参考Javascript书籍。我们紧紧提到几点原型继承中的特点:

1.原型属性可以被动态的修改,修改后将影响继承该原型的所有实例。

*代码片断*

<script>

<!--

var Child=function(){};

var Parent=function(){

         this.name="mathkeeper";

}

var parent=new Parent();

Child.prototype=parent;

var child1=new Child();

var child2=new Child();

alert(child1.hello);//alert “undefined”

alert(child2.hello); //alert “undefined”

parent.hello="Hello World!";//为原型中添加一个额外的属性

alert(child1.hello); //alert “Hello World!”

alert(child2.hello); //alert “Hello World!”

//-->

</script>

在这里最初原型对象中是不存在属性hello的,所以第一次两个对象都alert “undefined”当我们动态的为原型添加一个属性时,将影响所有的已经创建的对象实例。

2.继承原型的类所创建的所有实例都将共同分享同一个原型对象,内存上也仅仅分配一个原型对象所占用的空间。所以原型继承的内存利用率上是很高效的。这里也意味着如果你修改原型将影响所有的实例,你不能指望原型对象保存有各个实例自身的状态。

3.原型继承在读属性时的搜索顺序,是从原型链的最底端开始,一直向上。Javascript内部维护了一个原型链,它首先会在原型链的最底端(也就是调用者自身)中查找所需要的属性,如果没有,Javascript就会顺着原型链一直查找直到找到所需要的属性,这也意味着原型继承虽然会节省内存,但是却增加了一个指针操作,降低了运行速度。

1:原型链演示

从图中我们看到了一个链条,每一个实例对象都拥有一个隐藏得属性proto,指向着它的原型实例。这个隐藏属性你是无法访问操作的,我们不用去关心它是如何实现的,我们仅仅需要知道存在着一个这样的原型链就可以了。

4.原型继承仅在读取属性时发生。意味着如果你希望在方法中使用this指针修改属性状态时,它所修改的不是原型对象中的属性,而是为调用者自身添加了一个新的属性。这一点是合理的,否则如果一个实例修改了自己状态到原型中,将影响其它的实例状态。

*代码示例*

function Parent(){

         this.name=”parent”;

}

function Child(){

         this.getName=function(){

                   return this.name;

         }

         this.setName=function(name){

                   this.name=name;

         }

}

Child.prototype=new Parent();

var child1=new Child();

var child2=new Child();

alert(chlid1.getName());//alert  “Parent”

alert(child2.getName());//alert  “Parent”

child1.setName(“Child”);//这里不是修改Parent实例属性name,而是为child1实例添加

alert(child1.getName());//alert  “Child”

alert(child2.getName());//alert  “Parent”

通过代码我们看到虽然在调用getName方法时,使用this.name可以访问到原型对象中的属性,但是却不能够通过给this.name赋值的方式修改原型中的属性。如果修改了的话,那么上例中的最后一行应该alert  “Child”,而不是”Parent”

1.3     名字空间

         我们经常会将一些抽象的公有的方法提取出来封装在一个模块中,以便将来复用。但如果不同的人或者公司进行封装时使用了相同的名称,这会引发名称上的冲突,我们无法同时使用这两者的模块在同一个项目中。这个时候名字空间就起到了重要的作用。名字空间为各自的模块创建了一个独立的标示,不同的模块在不同的空间中,从而避免了冲突。在Java中是通过包来实现名字空间的。Javascript本身并不支持名字空间,它没有包的概念。但是Javascript中有对象的概念,所有的属性(也包括方法)都可以放在一个对象中。所以我们可以通过创建不同的对象来实现名字空间。例如:

*代码片断*

<script>

<!--

var ferrari={};

var michelin={};

ferrari.Carpart=function(){

         /**your code**/

};

michelin.Carpart=function(){

         /**your code**/

}

//-->

</script>

         在这里法拉利和米其林公司都会制作汽车配件,但是法拉利做的是汽车引擎,而米其林做的是汽车轮胎,有天你要造一辆汽车,轮胎是米其林的,引擎是法拉利的。你向他们购买了配件,你看不到配件里面是什么样子的,但是你给他们贴了标签。所以你的机器人知道法拉利生产的是引擎,米其林生产的是轮胎,于是机器人顺利将他们装配在了一起。想想如果没有为他们贴标签,而你拿到的东西都叫做汽车配件,那你的机器人如何知道这些汽车配件该放在哪里呢?

2.第一个Hello World

做一切事情最好都是先从简单的做起,首先演示一个KeeperJSHello World。在这里我们定义一个类,然后调用它的方法向大家问好。

1.  建立一个工程目录如C:/MyItem,KeeperJS的类库文件夹放到该工程目录中。

2.  建立一个html文件。名字叫做Test.html

3.  htmlhead标签体中引入keeper/core/System.js文件。如下图所示

<html>

    <head>

       <script type="text/javascript" src="jslib/keeper/core/System.js"></script>

    </head>

</html>

4.  在这里假设所有的KeeperJS类库都放在一个叫做jslib的文件夹中,在该文件夹中创建一个文件夹test,在test的文件夹中建立一个MyClass.js文件,文件的内容如下:

$package("test");

 

$class({

    $classname:"test.MyClass",

    $methods:{

        greet:function(){

            alert("Welcome come in the KeeperJS World!");

        }

    }

})

5.  html文件中加入script节点,同时加入代码如下:

<script>

    $import("test.MyClass");

    var myClass=new MyClass();

    myClass.greet();

    </script>

         好了完事大吉,用浏览器打开Test.html文件,你会看到Welcome come in the KeeperJS World!弹出。

         在这里我们应该看到KeeperJS定义一个类和Java中相似,如果你定义一个类,那么类的包名就是你的文件路径,你的类名就是你的文件名字。

3.包

KeeperJS支持名称空间,这意味着,当你声明了两个同名的类,只要包不一样,他们将分别在各自的名字空间中,不会引发冲突。这对于引入别人开发的类库时是十分有用的。在KeeperJS中使用$package命令声明包,在声明类时,在定义的最上面使用$package声明所声明类的包名。这个命令非常简单,它的理解也跟Java中完全一样。例如我们定义一个类keeper.test.MyClass。在这里包名是:keeper.test。所以你应该这样声明包:

4接口

Javascript里并没有接口的概念,在KeeperJS中使用$interface来定义一个接口。在Java中的接口是完全抽象的,意味着你只能声明抽象的方法,不能提供默认的实现。在java中如果你继承了一个接口却没有全部实现一个接口中定义的方法,那么在编译期,就会提示你这个错误。但Javascript却不同,它是一个解释型的语言,所以即便是你声明继承一个接口,直到执行期才能发现你没有完全实现其方法而导致的错误。所以KeeperJS认为提供一个带有默认实现的接口具有更多的实际意义。当然你也可以使用$implemented命令检查你所定义的类是否完全实现了接口中的全部的抽象方法。

4.1     接口的定义

首先看如何定义接口的:

*代码片断*

$package("test");

 

$import("test.Animal");

 

$interface({

         $classname:"test.Animal",

         $extends:test.Mammal,

         $methods:{

                   run:function(){

                            alert("Mammal running");

                   },

                   eat:$abstract

         },

         $statics:{

                   NAME:"I am a Mammal"

         }

})

在这个例子中使用$interface定义声明一个接口,在这里需要注意$interface后面的用法必须用’({})’包括起来内部的定义,同时内部使用逗号分割。在以后的类,以及抽象类中都是这种用法,一个接口可以有如下的行为:

1.  使用关键字$classname定义接口的名称,这是一个必须声明的选项,同时你必须使用完整的类名称来声明,不能使用短类名代替。定义时使用字符串赋值给关键字。

2.  使用关键字$extends继承一个接口,像java中一样一个接口可以继承另一个接口。而且所继承的类必须是一个接口,不能是抽象类,也不可以是一个具体类。当继承一个接口时,超类接口中的方法将需要一并被实现者实现。定义时直接使用所继承类的构造器赋值。在上面中一个哺乳动物的接口就继承了一个动物接口。

3.  使用关键字$methods使用{}包括定义内容,定义实现者需要实现的方法。如上面的代码一个动物可以拥有两个方法,一个是eat,定义eat方法时使用$abstract关键字声明该方法为一个抽象方法,意味着凡是继承该接口的类,都必须实现此抽象方法,另一个是run,该方法在这个接口中提供了一个默认的实现,这意味着凡是继承该接口的类,都拥有这个默认的实现方法。

4.  使用关键字$statics使用{}包括定义内容,在$statics方法块中你只能定义一些静态的常量,不能定义方法。像Java中一样,接口中声明抽象的静态方法是没有意义的。

在上面的定义中我们注意到一个命令$import(),这个命令用于加载所使用的类,详细看关于10$import命令的介绍

4.2     $implemented的使用

         因为Javascript解释型的语言,所以即便你继承的抽象类或者接口,但没有实现类中的全部抽象方法,也无法知道自己这个无心的错误,所以KeeperJS加入了一个命令$implemented它的作用就是检测你所定义的类是否全部实现了抽象方法。该方法包括两个参数,第一个参数是继承抽象类的实现者,第二个参数是所要检测的抽象类。例如:

*代码片断*

//声明一个抽象接口

$package("test");

 

$interface({

         $classname:"test.Greeter",

         $methods:{

                   greet:$abstract

         }

})

//实现这个接口的类

$package("test");

 

$import("test.Greeter");

 

$class({

         $classname:"test.GreeterImpl",

         $implements:[test.Greeter]

})

//执行检测代码

$import("test.GreeterImpl");

$import("test.Greeter");

alert($implemented(GreeterImpl,Greeter));//alert  false

在上面接口中定义了一个抽象方法greet但是实现者GreeterImpl没有实现该方法,所以调用$implemented检测返回的是false,否则返回true

5

KeeperJS中使用$class关键字来定义一个具体类。首先看一个例子:

*代码片断*

关键字$class可以拥有以下行为:

1.  使用关键字$classname定义完整的类名,用法和接口中的定义一样。

2.  使用关键字$implements声明继承的接口,使用数组[]包括所有的继承接口来定义声明,注意可以添加多个接口来声明实现多个接口。

3.  使用关键字$extends声明继承的超类。所继承的超类可以是一个抽象类,或者是一个具体类,但不能是一个接口。

4.  使用$constructor关键字来定义此类的构造函数。

5.  使用$methods关键字定义对象的方法,在该定义块中你只能定义方法,不能定义域变量。

6.  使用$statics关键字定义类的静态方法,或者静态常量。

在这里我们看到$constructor用于声明一个构造函数,KeeperJS推荐将所有的域或者叫状态变量都声明在构造函数中,稍候在继承中会继续阐述这个习惯的好处。

6抽象类

         KeeperJs中使用$abstractclass来定义一个抽象类。代码示例:

*代码片断*

         应该说抽象类的定义与具体类的定义并没有太大的不同,唯一的不同仅仅就是在methods块中,抽象类可以声明抽象的方法,而具体类不行。其它的则完全一样,所以可以参考$class的行为。

7继承

7.1域的安全问题

         看了之前所有的基本定义之后开始步入第一个重点,继承。继承是面向对象的一个重要特性,之前看到的$class,以及$abstractclass定义中允许使用$extends来声明继承。这里值得一提的是关于域的问题。首先看个例子。

*代码片断*

//超类的代码

$package("test");

 

$class({

         $classname:"test.Parent",

         $constructor:function(){

                   this.ball="football";

         },

         $methods:{

                   kick:function(){

                            return this.ball;

                   }

         }

})

//子类的代码

$package("test");

 

$import("test.Parent");

 

$class({

         $classname:"test.Child",

         $extends:test.Parent,

         $constructor:function(){

                   this.ball="baseball";

         },

         $methods:{

                   bat:function(){

                            return this.ball;

                   }

         }

})

//执行代码

$import("test.Child");

var child=new Child();

alert(child.kick());//alert  “baseball”

alert(child.bat());//alert  “baseball”

执行一下上面的代码猜猜会是什么结果?结果是alert两次baseball。这隐藏着一个容易出错的问题。类似的代码在java中会打印出footballbaseball。原本设计者期望父亲会踢足球,儿子继承了父亲,于是儿子除了会踢足球以外还会打棒球,但是结果却是儿子只会打棒球了。这里的原因主要是this指针搞得鬼。在第一章this指针中,我们说过this指针是动态改变的,它默认指向执行方法的对象(这里不考虑使用apply,call强行修改 this指针)。所以当执行kick方法时,this指针实际上指向的是孩子对象而不是父亲。另一方面从原型继承中知道,Javascript首先会从原型链的最底端寻找属性,所以它首先查找孩子对象中是否拥有名字叫做ball的属性,没有则向上寻找,也就是到父亲中寻找。结果kick方法发现孩子中有这个属性于是就返回了。

这里面临的一个重要的问题是,当超类与子类定义的域发生重名现象时,会遇到这种问题。这个问题没有完美的解决方案,最佳的解决方式就是避免重复,所以推荐办法就是将所有的域声明在构造函数中,以便于继承者注意是否自己使用了不该使用的名字,同时推荐尽量将超类作为一个无状态的继承。所谓的无状态继承就是说仅仅关注的是方法的继承不涉及状态。另外如果可以的话最好声明的域名称尽可能具有自己的类个性,如加一个个性的前缀,这样就可以避免这种问题,同时可以大胆的使用状态。在这里值得一提的是最后一个建议说可以大胆使用状态并不代表你的状态全部保存在了父亲对象中。想想为什么?在第一章原型继承中说过原型继承的所有实例都将公用一个父亲实例,如果放在父亲中那末一个实例的状态改变将影响所有的其它实例。但是如果继承时没有域重名的问题,你将会感觉状态就是保存在父亲中。这点一定要切记。

7.2超类构造

在使用继承时另一个需要注意的就是超类构造,当类的构造需要参数时,你可以使用$super方法进行超类构造,例如下面的用法:

*代码片断*

这里需要注意的是,你只能调用一次超类构造,不能调用第二次,否则程序将报错。

7.3作用域

         Java中存在作用域的修饰符private publicprotected,来告诉别人对象的资源哪些不可以被人访问,哪些是可以被人访问的,哪些是可以给特殊人群访问的。在Javascript中不存在这样的作用域。Javascript是函数式语言,它只有函数内、函数外的区别。我们可以通过var修饰符告诉其它人这个变量不可以被函数之外的人看到。

*代码片断*

(function outer(){

         var str=”mathkeeper”;

         (function inner(){

                   alert(str);//alert  “mathkeeper”

         })()

         alert(str);//alert  “mathkeeper”

})()

alert(str);//报错,因为str 没有被定义

运行上面的代码我们看到前两个alert都正常输出了,而最后一个却抱错。充分说明var的作用,它告诉程序这个变量只有在函数(这里是函数outer)内可以被访问。第一个alert在一个内部嵌套的函数inner中,而inner在外部函数outer内,所以可以访问str变量;第二个alert它就在函数outer内部,所以可以访问str无可厚非;第三个alert,它在函数outer的外面所以程序抱错说str变量没有被定义。通过这个例子我们知道Javascript中是肯定实现不了protected作用域,但是想想我们有办法实现private吗?

答案是肯定的,例如我们这样定义一个类,我们就可以实现publicprivate

*代码片断*

function People(){

         this.name=”mathkeeper”;

         var sex=”man”;

         this.getName=function(){

                   return this.name//####

         }

         this.getSex=function(){

                   return sex;//####

         }

}

//执行代码

var p=new People();

alert(p.name);//alert  “mathkeeper”

alert(p.getName());//alert  “mathkeeper”

alert(p.getSex());//alert  “man”

alert(p.sex);//alert  “undefined”

通过这个代码我们看到了通过var声明的变量sex不能够直接被外界访问,但是却可以通过public的方法间接被外界访问。

然而我们看到了一个问题,就是用//####标注的两行,我们注意到访问不同作用域变量的方式存在区别,name变量需要通过this指针来访问,而sex不需要,这种不同带给了使用者不便,它要求使用者必须意识到自己的变量是何种作用域,以便用不同的方式编写代码。同时对于阅读者也是一种考验。正因为如此KeeperJS没有实现作用域修饰,因为KeeperJS希望自己的使用是简单的,而不是将复杂转交给使用者。最终KeeperJS选择放弃私有域的实现。但是我们依然应该了解Javascript中流行的对私有属性的一些约定,这有助于以后的代码规范。在书中约定凡是私有的属性都使用”_”下划线作为前缀来命名.所以如果你是个阅读者,看到”_”你应该告诉自己这个方法我一定不要去使用。例如:

_getWarning:function(){

return  “Do not touch me!”;

}

7.4继承时方法覆盖的优先权

         我们知道当发生继承时,并且超类与子类的方法同名时,会发生方法的覆盖。由于接口中可以提供默认实现,这使得覆盖变得复杂了点。比如超类中有一个方法A,接口中也有一个方法A,子类继承了超类同时实现了接口,那么调用子类的A时,是谁的方法A呢?这里我们总结一下规则:

1.       子类将会覆盖超类的方法,子接口会覆盖超类接口的方法。

2.       方法覆盖的优先权,子类>超类>接口。当同时存在重名方法时,优先权高的类方法将会覆盖优先权低的类方法。

3.       如果一个类继承的多个接口里存在同名方法时,将只使用第一个出现在接口中定义的非抽象方法。

8.多态

多态是面向对象的另一个重要特性,在KeeperJS中随着继承的引入而引入。虽然多态不像继承那么简单,从名字就可以理解出个大意,但是多态也是对现实世界的一种反映。我们的世界是缤纷复杂的,对象是对我们现实世界中真实个体的体现,而每个个体有着自己的个性,就像每个人,同样是跑步各有各的跑步方式,同样都叫做李四,但可能是完全不同的两个人,多态就是对这种现象的反映。这里不讨论多态的概念是什么,仅仅就我们经常使用的多态特性进行展现,看看在KeeperJS中又是如何实现的。对于多态的具体表现有很多种,经常见到的有三种,下面分别讲述。

8.1     重载(Overload

         重载可能是我们用到的最广的一种多态特性。简单的说重载的作用就是让程序识别方法时,对名字相同的方法,但参数的个数不同,或者参数的类型不同的方法,认为是两个不同的方法。例如Java中这三个方法被认为是不同的。

         public void add(Integer i);

         public void add(Short i);

         public void add(Integer i,Integer j);

不过令人失望的是KeeperJS中不支持重载,在Javascript中你可以为方法声明任意多个参数但是如果他们的方法名字只能有一个。同时调用方法时你也不一定要为每一个参数都输入值。例如:

         *代码片断*

         //我们定义一个方法

         greed:function(message,people,times){/*codes*/}

         在这个方法里我们声明了三个参数,问候的信息,问候的人,问候的次数。但是我们调用方法时,不一定三个参数全部传递,只要你的参数所在的位置相同就可以。同时我们也看到Javascript中也没有类型的概念,所以也不存在参数类型不同,名称相同的方法。例如你可以如下调用:

***.greed(message);

***.greed(message,people);

***.greed(message,null,times);

这也许看着很像是方法的重载,但其实完全不是,因为你仅仅只有一个方法的定义。

8.2     覆盖(Override

         覆盖也是我们最经常使用的特性之一,先看看什么是覆盖。

         首先看Java中如何体现覆盖的:

*代码片断*

//声明一个超类的

public class Parent{

         public void kick(){

                   System.out.println(“kick football beeline.”);

         }

}

//声明一个子类

public class Child extends Parent{

         public void kick(){

                   System.out.println(“kick football arc.”);

         }

}

//执行代码

public class test{

         public static void mian(String[] args){

                   Child cld=new Child();

                   cld.kick();//print kick football arc.

         }

}

         可以看到儿子继承了父亲的方法kick,但是儿子与父亲踢球方式不同,儿子只会踢弧线球,儿子复写父亲的方法,对同名的方法,却拥有了不同的内部实现。这个行为就是覆盖。在KeeperJS中你依然可以这样做。代码例如:

*代码片断*

//超类的代码

$package("test");

 

$class({

         $classname:"test.Parent",

         $methods:{

                   kick:function(){

                            alert("kick football beeline.");

                   }

         }

})

//子类的代码

$package("test");

 

$import("test.Parent");

 

$class({

         $classname:"test.Child",

         $extends:test.Parent,

         $methods:{

                   kick:function(){

                            alert("kick football arc.");

                   }

         }

})

//执行代码

$import("test.Child");

var child=new Child();

child.kick();//alert    “kick football arc.”

有时候也许我们并不是想完全重写超类的代码,而仅仅是对超类方法的补充,这个时候在Java中存在一个关键字super,使用它可以调用超类的方法。在KeeperJS中你依然可以这样做,继续关于父亲儿子的事情,代码如下:

*代码片断*

//超类的代码

$package("test");

 

$class({

         $classname:"test.Parent",

         $methods:{

                   kick:function(){

                            alert("kick football beeline.");

                   }

         }

})

//子类代码

$package("test");

 

$import("test.Parent");

 

$class({

         $classname:"test.Child",

         $extends:test.Parent,

         $methods:{

                   kick:function(){

                            this.$super.kick();

                            alert("kick football arc.");

                   }

         }

})

//执行代码

$import("test.Child");

var child=new Child();

child.kick();//alert    “kick football beeline.” “kcik football arc.”

这一次父亲会踢足球,但他只能踢出直线球,儿子继承了父亲他比父亲更胜一筹,不仅能踢出直线球,还能提出弧线球。看,只要使用this.$super就会直接指向超类的实例,从而调用超类中的方法。

另外值得一提的是当要在构造函数中使用$super调用超类的方法时,必须首先进行超类构造才可以使用$super调用超类方法,无论是否超类的构造需要参数。也就是这样用:

8.3     Multiple polymorphism

Java编程思想中的第七章,列举了一个关于乐器的多态示例,在那里声明了一个乐器的接口,然后定义一个方法接受参数是一个抽象接口,从而只要传入的是一个实现该接口的实例就可以执行不同实例中的接口方法,虽然他们的行为各不相同。这种形式的多态是伴随着抽象类的存在而产生的。在KeeperJS中由于有了接口以及抽象类,你依然可以实现它。例如:

*代码片断*

//声明一个接口乐器

$package("test");

 

$interface({

         $classname:"test.Instrument",                 

         $methods:{

                   play:$abstract

         }

})

//创建两种乐器

$package("test");

 

$class({

         $classname:"test.Violin",

         $methods:{

                   play:function(){

                            alert("play violin.");

                   }

         }

})

$package("test");

 

$class({

         $classname:"test.Piano",

         $methods:{

                   play:function(){

                            alert("play piano.");

                   }

         }

})

//演奏人

$package("test");

 

$class({

         $classname:"test.Player",

         $methods:{

                   play:function(instrument){

                            instrument.play();

                   }

         }

})

//让演奏人演奏

$import("test.Piano");

$import("test.Violin");

$import("test.Player");

var player=new Player();

var piano=new Piano();

var violin=new Violin();

player.play(piano);//alert         “play piano.”

player.play(violin);//alert          “play violin.”

看虽然演奏人并不知道给他的是什么乐器(至少Player类中的代码上看是这样的),但他依然可以把各个乐器演奏的非常好。

9.反射

Java中有一个称为反射的功能,通过反射你可以得到类信息,同时可以动态的调用类实例的方法,KeeperJS也引入了类似Java中反射的功能,当然其功能还是很有限的,但是当你需要了解你所得到的类的信息时它将非常有用。在使用反射时有两种情况,一种是从类实例中得到类信息,另外一种是从类声明中得到类信息。

9.1从类声明中得到类信息

类似java中有一个Class类一样,虽然KeeperJS中没有这个类,但是你却可以得到一个实例对象,它拥有类似Class类的功能。(在这里我们依然叫它Class实例吧,但要注意根本没有Class类的声明定义)当你要从一个类声明中得到这个对象你可以这样使用:

当然如果是java中,唯一的区别就是那个$。因为class是一个关键字不能使用,所以就用$class代替了。如果你得到了一个class实例,你就可以通过它得到类的信息。

例如如果你想知道这个类是否是一个接口,你可以这样调用。

如果这个Cat类是一个接口那你就可以得到true

再比如如果你想得到该类的超类的class实例对象。你可以这样用:

如果该类存在超类的话,将返回超类的class实例对象,如果没有超类,默认是继承Javascript中的Object,这时将返回null,这个有些特殊值得注意。

具体的class实例API可以参考8.3

8.2从类实例中得到类信息

         当你得到了一个类实例cat想从中知道这个类的相关信息,你可以这样得到之前提到的class类实例。

        

         所有使用KeeperJS声明的类所创建的实例都将拥有getClass方法,就像javaObject对象拥有该方法一样。当你得到class实例后,你就可以 像之前那样得到类信息了。

8.3可调用的API参考

1String  getCanonicalName():得到类的标准类名称。

2String  getName():得到类的名字。

3String  getSimpleName():得到类的简单短类名。

4String  getPackage():得到类的包名。

5String  getClassType():得到一个字符串指示该类得类型信息。

6Boolean  isInterface():指示该类是否为一个接口。

7Boolean  isAbstractClass():指示该类是否为一个抽象类。

8Boolean  isConcreteClass():指示该类是否为一个具体类。

9Boolean  isInstance(Object obj):指示输入类实例obj是否为该类得实例。

10Object  getConstructor():返回该类得构造函数引用。

11Class  getSuperClass():返回该类超类的class实例对象。

12Object[]  getInterfaces():返回该类实现的所有接口的引用数组。

13Function  getMethod(String methodName):根据指定的方法名称,返回类实例方   法。

14Function  getStaticMethod(String methodName):根据指定的方法名称,返回类的静态方法。

10.实现KeeperJS继承的原理

         Javascript本身是存在继承的,它使用的是原型继承,所有继承原型的类都指向同一个实例对象,这对内存利用率是很高的。为了继续利用这个特性,KeeperJS将所有的方法声明都放在了原型中,将状态域都放在了类实例自身中。同时为了实现多态的覆盖特性,这意味着你必须能区分超类和子类的方法。KeeperJS在原型之上加入了一个原型,也就是原型的原型,将所有超类的构造都放在了该原型中,这意味当你声明一个类后,它的状况如下:

        

10.1.实例克隆

如果你知道了接口,并且使用过java中的java.lang.Cloneable接口的话那你一定很关心如何克隆的。

通过上面的分析我们知道克隆一个实例仅仅需要copy类自身中的状态域就可以了,下面看看如何clone

KeeperJS的类库中有一个接口keeper.lang.Cloneable,凡是实现了该接口的类就拥有了clone的能力,它将拥有一个方法就叫做clone()。当实例调用该方法时就可以返回一个自己的克隆体,有兴趣的人可以自己去试试看下面的代码。

*代码片断*

//首先建立个类

$package("test");

 

$import("keeper.lang.Cloneable");

 

$class({

         $classname:"test.TestClone",

         $implements:[keeper.lang.Cloneable],

         $constructor:function(){

                   this.name;

         },

         $methods:{

                   getName:function(){

                            return this.name;

                   },

                   setName:function(name){

                            this.name=name;

                   }

         }

})

//执行代码

$import("test.TestClone");

var tc=new TestClone();

tc.setName("MathKeeper");

alert(tc.getName());//alert     “MathKeeper”

var tc2=tc.clone();

tc.setName("KeeperJS");

alert(tc.getName());//alert     “KeeperJS”

alert(tc2.getName());//alert   “MathKeeper”

11$import命令

KeeperJS中有个重要的命令$import,像java中一样,当你要使用一个类时,必须要首先import它,同时$import命令对于KeeperJS中命名空间的实现也是至关重要的。

当你使用$import命令时,必须使用完整的类名称,KeeperJS会首先寻找是否已经加载过该类,如果未加载,则根据该类名,计算出类路径,并向服务器寻找类文件,在计算类路径时KeeperJS是根据你所声明引入的keeper/core/System.js文件路径为基计算的,如果寻找到该类文件,那么KeeperJS会加载执行该文件,同时为所引入的类的短类名声明引用,所以当你使用$import命令引入keeper.test.MyClass类时,你既可以使用完整类名,同时也可以使用短类名MyClass,也就是说下面的两种用法都可以:

所以在下一次$import同类名不同名字空间的类之前,你使用短类名都是安全的,然而有一种情况这将导致一个问题,就是侦听器的情况,当注册完侦听器时,侦听器方法中使用了一个类例如叫keeper.test1.MyClass,但并不立即执行,接着在后续的代码中又import另一个类keeper.test2.MyClass。当侦听器得到通知进入所注册的方法并使用短类名MyClass时,这个时候MyClass实际引用的是keeper.test2.MyClass,而不是应该的keeper.test1.MyClass。解决该问题的办法有两种:

第一种方法:完全都使用完整类名,不要使用短类名。

第二种方法:在方法中使用$import加载类。

也就是类似如下使用:

第二种方法也许是个不错的选择,因为它仅仅会在真的需要使用keeper.test1.MyClass时才会去加载该类文件,同时拥有名字空间的特性还可以安全的使用短类名,但是它却缺少了几分优雅,多了几分复杂,所以可以根据自己的喜好来使用。

为了更好的引入其它框架写的类库文件,KeeperJS加入了另外一个命令$import2$import2$import几乎没有什么区别,但是$import2不会为类声明名字空间,也不会为短类名声明引用,这意味着你无法直接使用类名称来进行构造,它仅仅负责加载你所指定的文件,使用类名称来命名的路径。例如你要加载一个文件keeper/test/Script.js,并且这个路径与Keeper/core/System.js在一个平级的路径上时(因为KeeperJS计算路径是根据所引入的System文件为基的),你就可以使用$import2(“keeper.test.Script”)来加载该文件。

12.集合容器类

         如果你是一个Java程序员你一定会用到java.util包下的集合类,这个包实现了一些基本的数据结构,方便构建复杂的程序。例如MapListQueue等。

         KeeperJS也模仿Java的使用方法构建了一个keeper.util包,实现了类似的功能。Java中为了遍历集合类,存在一个迭代子接口IteratorKeeperJS仍然加入了该接口并实现了它。如果你熟悉Java用法的话,你一定会感觉KeeperJS中的用法非常简单相似,基本上毫无二异。所以这里仅仅举一个遍历Map的例子,并简单的展示下所实现了类库目录,其它的使用你可以查看API文档。

        

执行上面的程序将会输出

name1      A

name2      B

name3      C

类库目录:

13.一些实用的方法

         为了方便使用KeeperJS提供了一些方便的函数,或者加强了一些Javascript的内部类。

13.1   一些常用的全局命令

         (1)    $isIE(),$isFF()

         如果你希望设计的Javascript程序是兼容主流浏览器的话,那你一定会思考如何知道现在的程序是执行在什么浏览器中的,KeeperJS提供了两个全局函数用于反映当前的浏览器情况,如果是IE浏览器那么$isIE函数将返回true,如果是firefox浏览器那么$isFF函数将返回true

         (2)    $instanceof()

         Javascript中也有一个关键字instanceof,它用于测试所定义的类是否继承自另一个类,但这里是基于原型继承,也就是说它测试所定义的类的原型是否指向了另一个类的实例。通过原理分析我们知道,所继承的超类实例确实是被原型属性引用的,但是接口就不同了,所有的接口的方法都拷贝到了一个对象中。所以你无法用内置的instanceof去测试接口,但依然可以测试继承的类。为此KeeperJS定义了一个$instanceof()命令来拟补这个不足。使用法法类似instanceof,第一个参数是你要测试的实例对象,第二个参数是你要测试的类定义。

13.2   Javascript内置对象的加强

         (1)    字符串对象String的加强。

         任何程序我想对字符串的处理都是必不可少的,Java中有String 类型,Javascript中一样存在String类,我们在Java中可以直接调用trim方法清理两侧的空字符。而Javascript中没有这个方法,所以KeeperJS引入了这个功能,如果你引入了KeeperJS框架,那么所有的字符串都将拥有三个补充的方法分别是:trim,leftTrim,rightTrim

1.  trim:用于清理两侧的空字符。

2.  leftTrim:用于清理左侧的空字符。

3.  rightTrim:用于清理右侧的空字符。

用法很简单,例如:

*代码片断*

var str=”    mathkeeper    “;

alert(str.trim());//alert   “mathkeeper”;

alert(str.leftTrim());//alert   “mathkeeper    “;

alert(str.rightTrim());//alert      mathkeeper”;

14Javascript中的面向对象思考

         为了更好的说明面向对象编程的好处,我们举一个小例子来说明。这个例子不期望给他家介绍一个很实用的工具类,仅仅希望对大家能够是一个思想的启发。

         在通常的网站开发中,用到Javascript最多的要数客户端的表单验证了,所以这里就以表单验证为例来说明问题。看下面的代码:

*代码片断*

         <html>

         <head>

                   <script type="text/javascript" src="jslib/keeper/core/System.js"></script>

         </head>

         <body>

                   <form>

                            <input type="text" οnblur="vldA(this);" id="id1"/><br>

                            <input type="text" οnblur="vldB(this);" id="id2"/><br>

                            <input type="submit" value="submit" οnclick="return isValid();"/>

                   </form>

         </body>

         <script>

                   $import("keeper.html.validate.prompt.PromptIcon");

                   var result1;//保存id1的验证信息

                   var result2;//保存id2的验证信息

                   function vldA(obj){

                            var value=obj.value;

                            if(value=="id1"){

                                     result1=null;

                                     PromptIcon.release(obj);

                            }else{

                                     result1="id1 wrong";

                                     PromptIcon.show(obj);

                            }

                   }

                   function vldB(obj){

                            var value=obj.value;

                            if(value=="id2"){

                                     result2=null;

                                     PromptIcon.release(obj);

                            }else{

                                     result2="id2 wrong";

                                     PromptIcon.show(obj);

                            }

                   }

                   function isValid(){

                            if(result1!=null){

                                     return false;

                            }else if(result2!=null){

                                     return false;

                            }else{

                                     return true;//如果都验证通过则认为合法。

                            }

                   }

         </script>

</html>

         这段小代码很简单,但是我们看到,对于验证信息的处理上是很麻烦的事情。因为仅仅只有两个表单元素,所以我们可以用两个全局变量来保存验证结果,但是想想如果将来一个表单里存在着十几甚至二十个表单元素时,我们该如何是好?如果继续利用全局变量保存验证信息,程序代码将是十分繁杂的。况且,很可能到时候你会在这些变量中打转转。而且这种逻辑并不复杂的代码让人觉得看着就很头痛,无趣。

         那么如何以面向对象的方式来改进这段代码呢?我们知道面向对象的关键就是能够抽象出对象实体。如果我们在这里的角色是验证者(这里的验证者就是上面的两个函数vld1,vld2),那么我们一定会抱怨,我们仅仅负责验证,为什么还要我们去负责维护验证的信息该放在何处?最后当别人问传令兵(这里是isValid函数)验证都合法吗?传令兵也开始抱怨,我只是负责传达验证的逻辑结果,为什么还需要我去处理这些繁杂的逻辑判断?信息也不是我放的 ,为什么还要我去知道信息放在哪里了?换个角度想想,为什么我们不把这些责任分工下,有人负责验证,有人专门负责验证信息的维护,另外有人专门负责传达验证的结果?所以我们抽象出了一个对象ValidHolder,验证持有者。它专门负责验证信息的保存,获得,删除,以及分析表单验证的逻辑结果。

*代码片断*

$package("test");

 

$import("keeper.util.HashMap");

 

$class({

         $classname:"test.ValidHolder",

         $statics:{

                   putMessage:function(key,message){//保存验证信息

                            if(!this.map){

                                     this.map=new HashMap();

                            }

                            this.map.put(key,message);

                   },

                   getMessage:function(key){//获得验证信息

                            if(!this.map){

                                     return null;

                            }

                            return this.map.get(key);

                   },

                   removeMessage:function(key){//删除验证信息

                            if(!this.map){

                                     return;

                            }

                            this.map.remove(key);

                   },

                   isValid:function(){//是否表单验证合法

                            if(!this.map){

                                     return true;

                            }

                            if(this.map.isEmpty()){

                                     return true;

                            }

                            return false;

                   }

         }

})

         下面看看如何让验证持有者履行自己的责任的。

*代码片断*

<html>

         <head>

                   <script type="text/javascript" src="jslib/keeper/core/System.js"></script>

         </head>

         <body>

                   <form>

                            <input type="text" οnblur="vldA(this);" id="id1"/><br>

                            <input type="text" οnblur="vldB(this);" id="id2"/><br>

                            <input type="submit" value="submit" οnclick="return isValid();"/>

                   </form>

         </body>

         <script>

                   $import("keeper.html.validate.prompt.PromptIcon");

                   $import("test.ValidHolder");

                   function vldA(obj){

                            var value=obj.value;

                            if(value=="id1"){

                                     ValidHolder.removeMessage(obj.id);

                                     PromptIcon.release(obj);

                            }else{

                                     ValidHolder.putMessage(obj.id,"id1 wrong");

                                     PromptIcon.show(obj);

                            }

                   }

                   function vldB(obj){

                            var value=obj.value;

                            if(value=="id2"){

                                     ValidHolder.removeMessage(obj.id);

                                     PromptIcon.release(obj);

                            }else{

                                     ValidHolder.putMessage(obj.id,"id2 wrong");

                                     PromptIcon.show(obj);

                            }

                   }

                   function isValid(){

                            return ValidHolder.isValid();

                   }

         </script>

</html>

看看这次,一切的分外事情都交给验证持有者吧。是不是简单许多了?

15AOP编程

         如果你知道AOP编程,那你一定会被这个新的编程思想所吸引。KeeperJS也加入该功能,尽管还不够强大。KeeperJS力求语法简明,易于理解,所以所有的AOP语法都是基于有名的AspectJ的。下面举一个简单的例子。

         1.首先你定义一个类MyClass

        

2.然后在html文件中引入keeper/core/AOPSystem.js文件。这个文件负责提供了AOP框架的功能。

         3.然后使用$aspect定义一个方面:

        

         4.然后,使用编织者编织方面:

        

         5.最后,调用MyClassgreet方法看看是什么效果:

        

         没错我的类她会说Hello! Mr Keeper!,很神奇是不是?

         如果为您完全阐述AOP的编程的思想,手法那可能太多了,考虑到KeeperJSAOP功能还是比较有限的,所以这里仅仅讲下KeeperJS如何编写一个简单的AOP实例。敬请期待以后的版本中继续扩展AOP功能。

         在这里首先你需要使用$aspect来定义方面,$aspect存在以下行为:

1.  使用$aspectname来定义方面的名称,这是一个必须选项,同时必须使用完整类名。

2.  使用$pointcut来声明切入点,一个切入点负责将你的关注点和通知连接在一起。目前KeeperJS仅仅支持对类方法关注点的拦截。在声明调用关注点时,你需要使用$call关键字来定义该类型的关注点。$call很容易使用,它存在三个行为,

1$class关键字,指示你所要关注的类。

2$method关键字,指示你所要关注的类实例方法。

3$static关键字,指示你所要关注的类静态方法。

这里需要注意的就是$method$static不能够同时使用,因为你不能在一个切入点中声明两个关注点。当然如果你需要关注两个关注点的话,那你应该另外声明一个切入点。

3.  $before通知,声明了一个切入点后,结下来就是当关注点到达时如何通知你来执行你的代码。这在里我们看到很简单,对于切入点”greet”你添加自己的执行代码即可。该关键字中可以对一个或者多个切入点声明通知。

4.  $around通知,类似于$before通知,所不同的是当round通知来时,会为你所声明的通知函数中的第一个参数传递一个method引用(该参数引用着真实的关注函数),如果在实际调用关注方法时还存在其它参数,会在第二,第三个参数位上依次传递,你必须手动的使用apply(this,method)来调用它,

5.  $after通知,同于$before

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值