ECMAScript是JavaScript的核心部分,就如同其他语言的核心一样,ECMAScript描述了JavaScript的语法、类型、变量,语句,函数,运算符等。ECMAScript 和Java,C等在大部分语法现象上都很相似。同时,它是一种脚本语言,也有着和VBSript,Perl等共同的特性。下面,将简单谈一下js的类型。如没有任何编程基础的读者,请自行参阅各类书籍。
ECMAScript的共有两种类型,分别是原始值和引用值。原始值是指把值直接存储在变量里。另外一种是引用值,即把引用存储在变量里,这个引用再指向真正的对象。这个和Java、C++类似。C没有引用,只有指针。
原始类型
与原始值相对应的,ECMAScript共有5种原始类型,当变量赋值为这五种类型中的一种时,该变量存放的就是原始值。这五种原始值分别是Undefined,Null,Boolean,Number, String。注意,ECMAScript并没有把数字类型再细分为整型,长整型,单精度型,双高精度型等。在这五种类型中,比较特殊的是Undefined和Null。当变量已经定义,但未初始化时,其将被赋予值undefined。而Null则空对象,即对象是已经存在的,只是它的值为空。强烈建议,任何变量都必须初始化,即使当一开始还未确定赋何值时,String类型就赋值为空字符串,Boolean类型就赋值为false,数字类型就赋值为0,如果连变量类型都无法在定义时确定,那么就赋值为null。
对于这五种原始类型,ECMAScript提供一个运算符来直接检查这五种类型,它是typeof运算符。
- var int=1;
- var str=‘1′;
- var bool=true;
- var undef;
- var anymous=null;
- alert(typeof int);
- alert(typeof str);
- alert(typeof true);
- alert(typeof undef);
- alert(typeof anymous);
在上面的运算中,要注意的是,typeof null返回的是object。这个是早期JavaScript中的一个Bug。最后还是被ECMAScript沿用了。但是,在ECMAScript 3rd的规范文档中,已经把null表述成了对象,所以typeof返回object是正确的。
类型转换
ECMAScript提供两种简单转换,分别是转换成字符串和转换成数字。实际上ECMAScript的所有类型(包括对象)都内建了一个toString()方法(原始类型其实是伪对象,下面会阐明),该方法就是把当前值按合适的方式转换成字符串。5个原始类型都将被转换成相同字面的字符串。而对于对象,又分为宿主对象和本地对象。本地对象本身已经实现了内在的toString()方法,而宿主对象可以由开发者自行创建toString方法。另外,在字符串连接操作中,所有对象或类型都会自动调用toString()对象。
- alert(‘this is a number: ’+1+‘\nthis is a boolean: ’+ true +‘\nthis is a null: ’+ null + ‘\nthis is a undefined: ’+undefined);
当要转换成数字时,可以使用parseInt和parseFloat。这两个全局函数,会把字符串中第一个无效字符前的字符串转换成数字类型。当无法完成这样的转换时,函数将返回一个特殊值“NaN”。例如:
- alert(parseInt(‘1234abcd’));
- alert(parseInt(‘22.5′));
- alert(parseInt(‘blue’));
另外,ECMAScript还提供三种强制类型转换。分别是Boolean(value),Number(value),String(value)。String(value)等同与对值使用toString()方法,而Number()类似与 parseInt和parseFloat,不同的时,Number()需检查整个字符串是否能有效的转换成数字类型(在ECMASript可以把ture看成1,把false看成0)。而Boolean()比较复杂。和toString()一样,任何类型和对象也都可以强制转换成Boolean型。规则如下(见示例):
- /*Boolean类型,转换后不变*/
- alert(‘true:’+Boolean(true));
- alert(‘false:’+Boolean(false));
- /*数字类型,0为false,非0为true*/
- alert(‘0:’+Boolean(0));
- alert(‘4:’+Boolean(4));
- /*字符串类型,空串为false,其他为true*/
- alert(‘"":’+Boolean(”));
- alert(‘"abcd":’+Boolean(‘abcd’));
- /*undefined和null,为false*/
- alert(‘undefined:’+Boolean(undefined));
- alert(‘null:’+Boolean(null));
- /*所有引用类型,都为true*/
- alert(‘Object:’+Boolean(new Object()));
ECMAScript除了原始类型,还有引用类型。引用类型也通常叫做类。但ECMAScript实际上是没有类这个概念的。它仅仅只能称作对象。类的实例化用new关键字来创建。例如var o = new Object();ECMAScript中的类分别有:
- Object类是其他所有类的基类,Object类中的所有属性和方法都会出现在其他类中。对于Object类,会在对象一章中重点介绍。
- Boolean类,Number类,String类,RegExp类,Date类,Array类,Function类
在使用这些类的时候必须特别小心,因为,用new关键字创建的,都属于对象类型。在“转换”一节提到过,任何引用类型,转换成Boolean型都将是true。所以,应该尽量避免使用Boolean,Number,String类,而直接使用他们的原始值。
- //转换引用类型的误区
- var oBoolean=new Boolean(false);
- var oNumber=new Number(0);
- alert(Boolean(oBoolean));
- alert(Boolean(oNumber));
在不推荐使用上述三个引用类型时,有人会怀疑,这样岂不是不能使用类提供的丰富的方法。但其实,除了null和undefined,其他三种原始类型其实都是伪对象。也就是说,一个存储原始值的变量,同样可以使用对应的类的方法,甚至,即使是字面量也可以直接使用这些方法,但它本身依旧是原始类型。
- //伪对象的方法
- var int=4;
- var str=‘abcd’;
- alert(int.toString(8)+‘ ’+(4).toString(16));
- alert(typeof int);
- alert(str.toUpperCase()+ ‘ ’+ ‘EFGH’.toLowerCase());
- alert(typeof str);
另外,对于Object类、RegExp类、Array类,Function类,也是可以通过字面量来直接定义的,而不需要使用new关键字来创建,但是,它们依旧是引用类型。为了提高效率,建议尽量用字面量来直接定义类。
- var oObject={};
- var oRegExp=/\d/ig;
- var oArray=[];
- var oFunction=function() {}
js的类型暂时先说到这里,其实,还有一个重要的类型没讲,就是函数。函数在js里是“一级”对象。关于的函数的知识将另起一篇讲述。
之前为了解决wordPress和google的SyntaxHighlighter的冲突,费了好大的力气,而且对原有的代码侵入太多,用的也不是很爽。而且,我一直“崇拜”FCKeditor,觉得tinyMCE完全没有它好用。于是,在baidu里随便搜索一下果然出来了用于wordPress的FCKeditor--chenPress。
同时又找到了用于FCKeditor的dpHighlighter的pluging,并经过一番周折整合了下,下面放出完整的chenPress。只需要用到FCKeditor的,也可以下载后在目录中单独拿出FCKeditor。
chenPress本地下载(版权归chenPress原作者所有)
js中的晚绑定和大多数语言(比如c++)中的晚绑定概念一样,简单的说,就是动态地引用不同的实例的同名方法(或属性),例如代码:
JavaScript代码
- window.a=function() {};
- a.prototype.method=function() {alert(‘a’);};
- window.b=function() {};
- b.prototype.method=function() {alert(‘b’);};
- c=new window[prompt(”)];
- alert(c.method());
当运行时,用户在输入框里输入a,那么最后提示的就是a,而输入b,提示的就是b,这就是晚绑定。
而js中,还有一种叫做“极晚绑定” ,很少语言有支持极晚绑定。而js得prototype链机制,导致了js支持极晚绑定。示例代码如下:
JavaScript代码
- var a=function() {};
- a.prototype.p1=1;
- var b=new a();
- alert(b.p1);
- alert(b.p2);
- a.prototype.p2=2;
- alert(b.p2);
b初始化a的一个实例,此时输出b.p1,根据prototype链接,结果是1。而第一次输出b.p2时,显示的undefined。然后,再设置a.prototype.p2=2,此时在输出b.p2,显示的是2。所有这些就是prototype链在作怪。因为,访问b.p1和b.p2都不是真正访问对象b中的p1属性和p2属性(因为没有语句采取b.p1=xx;b.p2=xx;来设置的对象b的属性),访问的都是b对象沿着prototype链上的p1和p2属性,即都是a.prototype.p1和a.prototype.p2,所以在b初始化后,再设置a.prototype.p2,就使得能够通过b.p2来访问p2属性。(如果读者不理解这里的prototype链机制,请参看另一篇文章《ECMAScript 3rd阅读笔记之一–js prototype链解惑》)。
众所周知,js不是class-based的,而是object-based。在ecma-262中,开篇对js的继承机制做了简单的描述(见 ecma-262 4.2.1)。其中提到,js中的对象搜索属性的一个链。首先,对象搜索自己定义的属性(即用obj.property的形式直接赋值的属性),如果不存在,则在对象隐含的prototype引用去找,而这个隐含的prototype引用即是其构造函数的prototype属性。这里要注意的是,构造函数的prototype属性仍旧是一个对象。所以,仍旧按照上面的描述规则来进行属性搜索,直到最终找到这个属性或者不存在隐含的prototype引用(即再也不能通过链属性了)为止。这就是ecma-262中的prototype chain。
为了形象点,我们可以把prototype chain用最形象的图来表示,它就是一条链子:
c(=new b())
|
|—-p=2
|
b.prototype
|
|——–p=1
|
…………
相应代码如下:
- var b=function() {
- }
- b.prototype.p=1;
- var c=new b;
- c.p=2;
b是一个构造器,c通过new初始化一个b的实例。然后,c在“修改”p的值为2。需要注意的是,我在修改这个词上加了引号,这是有原因的。
先来看看,如果运行alert(c.p),好无疑问,值是2。那么,假如去掉代码的第6行即c.p=2,此时,在c对象的属性中找不到p,就去找c隐含的prototype引用,即b.prototype。于是找到了b.prototype.p=1,所以输出就是1。又假如去掉代码的第3行即b.prototype.p=1,那么找不到p这个属性,又会去找b.prototype隐含的prototype引用(例如有b.prototype=new a(),而a.prototype.p=0),以此类推。最终等到找到p或者无法再沿隐含的prototype链走下去为止。
所以,事实,当c初始化为b的实例时,访问c.p,并不是真正访问c对象中的p属性,而是c在prototype链上的p属性。于是,假如在初始化后,用语句c.p=3来“修改”p的值,其实不是“修改”,而是覆盖。即用c对象自己的p属性覆盖了在prototype链上的p属性。而之后是仍旧可以找回prototype链上的p属性的。关于此方法请参见我的另一篇文章《找回被覆盖掉的原型属性》
诚然,prototype链确实很晦涩,对初学者也难以理解。对于final user,大可以按照习惯的“继承”思路来看待他们。而对于使用js来进行二次开发的developer,却必须搞清楚令人即爱又恨的prototype chain。
在此后的文章中,将进一步讨论prototype链的问题,同时将再引入另一个奇怪的属性__proto__。它是对象的隐含属性,它的存在,就更加让人匪夷所思了。
一直没接触过struts2,前些天听人介绍了下struts2的框架设计,其拦截器的设计,感觉很像操作系统的管道机制。即在传输流的过程中,要经过多个管道,每个管道对流进行一些处理,并传送给下一个管道。管道只有一个输入一个输出,多个管道可以随意拼接。同样拦截器的设计,也是对数据依次进行处理并最终传递到对应Action。这样使得Action和struts2框架之间的耦合度变得非常低。这也是借鉴了AOP的机制。当然AOP也有自己的坏处,比如要读懂代码流程其实并没那么容易,框架把“切点”封装的很好,以至于很难找到是哪两个切面相切。比如就像一个不透明的大圆里,有多个小圆相切连成一线,我们只知道和大圆内切的两个小圆(即一个入口一个出口),至于内部状况就不得而知了。虽然,有时候我们并不想去了解细节,但对于代码维护上来说,我个人感觉AOP完全没有优势。
第二,是代理接口的设计,这当然不是struts2独有的,大多数基于web的框架,都会考虑代理接口,这样就容易让客户端和服务器端维护一个共同的域模型(位于客户端的底层以及服务器端的顶层),而使各自的MVC不用去关注另一方的情况。客户端和服务器端互相是透明的,就像我在javaeye上看到fins的一则评论“世上没有B/S系统,只有B系统和S系统”。现在两个系统都日趋复杂,特别是一直被人忽视的B系统也在引入了RIA的概念后变得和S系统同等重要。一个程序员不再可能即精通B系统又精通S系统。为此让各自系统的开发人员尽量专注于份内的事是很重要的。代理分别为各自系统的域模型提供符合其风格的接口,使得各自系统的开发人员能够顺利开发,而域模型的维护就交给框架或者专门的开发人员来完成,我认为这是高效率且聪明的做法。
说了struts2以上两个特征,其实我最终的目的是想借由研究它的框架设计,来完成对ajaxgo库的开发。AOP、管道、代理接口,这些名词都已经在库开发起就在我脑子中形成了,苦于没有一个设计完善的例子作为借鉴。在写这篇文章前,又接触了下一个目的和我的库蛮像的库,jsvm。也是国人开发的,粗粗阅览了下它的设计代码,也看了许多人的评论,由于自己没有仔细研究,所以没有资格发布太多的言论。只是希望jsvm的作者能够把重点放在vm这个词汇上,即真正做到一个跨浏览器的runtime,而不要过多的专注于一些定义自己的“解释器”,且发现其中的类库部分,也采用了自己“解释器”的语法(import啊,package这些关键字),但又没有提供真正可以模仿OO的机制,有点得不偿失的感觉。而且也感觉类库零零散散,开发了两年的类库,却找不到几个真正实用的类,难免有些心寒。不知道,jsvm的作者看了我这段评论有什么感想,我只是个后辈,但我也不会忌讳说这些话。可能我的知识尚浅,未能抓住重点,希望jsvm的作者不要太仇视我
。
我们知道,在javascript中,可以在原型中定义类实例的属性,比如Class.prototype.member="1"。那么来考虑下面的代码:
- var Class1=function() {};
- Class1.prototype.member="1";
- var c=new Class1();
- c.member="2";
- alert(c.member); //覆盖了原型中的定义
- c.member=null; //重新设置成null
- alert(c.member); //但还是输出null,假如设置成undefined,返回的也仍旧是undefined
其中,在实例中定义的member属性覆盖了在原型中定义的。并且即使把属性再次设置成null,也无法找回原型中的属性。其原因其实是因为,在javascript中,null表示的是空值,而不是“没有”。所以,它并没有使实例中定义的member属性“消失”。当访问这个属性时,js解释器找到的仍旧是实例中定义的那个member,只不过它的值是null罢了。那么该如何才能找回被被覆盖掉的在原型中定义的属性呢?可以这么做:
- var Class1=function() {};
- Class1.prototype.member="1";
- var c=new Class1();
- c.member="2";
- alert(c.member);
- delete c.member; //删除了实例中定义的属性
- alert(c.member);
不过要注意的是,即使把实例中的属性设置成undefined,那么输出的还是undefined。
说起js的单元测试,那就不得不说jsUnit。但是jsUnit做的也不是很完善,比如没有批量自动化测试,当然也就没有了批量自动化测试的报告。所以,我对其进行了修改,加入了批量自动化测试并生成报告的功能。
使用方法是:
首先编辑一个配置文件,用于配置需要进行自动化测试的case,改配置文件使用xml标准格式。
其中jsUnit元素是对jsUnit的全局测试环境进行设置,同时,每个test_case元素,都可以包含一个option元素,来重写针对于该test_case的jsUnit测试环境参数。
每一个test_suite类似于一个命名空间,其path属性指名了路径,其子元素可以重定义path属性。
定义好xml配置文件后,运行auto_unit_test.html(请在iis环境下运行,apache没测试过,因为是纯js的,想必应该可以),就可以自动测试,并给出测试报告。同时auto_unit_test.html还可以接受两个参数,分别是auto_unit_test.html?show=true$configxml=testCongfig.xml。show表示是否显示jsUnit的测试运行界面,默认为false。configxml表示xml配置文件的路径,默认是同一目录下的testConfig.xml。
任何一个框架或者库,都不可能信手拈来的完成。所以,我就决定采用UML建模,并用“测试先行”的方式来进行开发。由于标准UML的实现并没有js,所以只能用java来代替。并且,经过搜寻,在网上发现了一个不错的国人做的UML开发工具,且是以eclipse插件的形式提供的,在这里推荐给大家:http://www.trufun.net/。
此外要说起js的单元测试,那就不得不说jsUnit。但是jsUnit做的也不是很完善,比如没有批量自动化测试,当然也就没有了批量自动化测试的报告。所以,我对其进行了修改,加入了批量自动化测试并生成报告的功能。详细情况,请参见本blog另外一篇关于《基于jsUnit的自动化测试平台》的文章!
由于js先天的缺陷,其并没有私有作用域。因此爱好者们就想出很多的方法来模仿或者制造出私有作用域。模仿的机制,就不多说了,这只是一种“说明它是私有的”,而不是真正的私有(外部要是想访问,仍旧能访问)。而如果要制造这种机制,那就需要利用闭包的机制。
以下几种方式大同小异,都是用到了闭包的特性,可按照个人喜好运用(此后会对这几种做个性能测试,但暂且的估计,这几种其实都是差不多的)。
最典型,也是最原始的就是这种模式(定义类):
- var class1=function() {
- var privateMember;
- this.getPrivateMember=function() {
- return privateMember;
- }
- }
然后,是目前比较流行的(定义对象,不适合定义类,因为私有变量会被所有类实例共享):
- var class2=(function() {
- var privateMember;
- return {
- getPrivateMember:function() {
- return privateMember;
- }
- }
- })();
最后是我目前在用的(其实和上一个没多大差别,同样也用于定义对象):
- var class3=new function() {
- var privateMember;
- this.getPrivateMember=function() {
- return privateMember;
- }
- };
假如上两种方法要来用于定义类,可以通过结合第一种方法,并且,定义的类,将能拥有“静态域”,示例如下:
- var class4=(function() {
- var staticPrivateMember; //静态私有属性
- var constructor=function() {
- var privateMember; //实例私有属性
- this.getMember=function() {} //实例公有方法
- }
- /*静态公有成员必须在构造器声明之后定义*/
- constructor.getMember=function() {} //静态公有方法
- return constructor;
- })();
在正式开始这次开发前,其实已经开发过了一次库,当初起名也叫做ajaxgo。大致上开发了,io(ajax),event,util等模块,算是个极小型,且主要对js底层进行扩展的库。在开发过程中,也渐渐出现许多新的理念和想法。之后慢慢的,觉得,这个库的架构已经无法再支撑其自己脑海中的一片ajax的广阔天空。于是决心,重新对这个库进行开发,或者也可以算是,一个全新库的开发。
这套库,主要着眼于,js底层性能的扩展,并且,将它大胆的定位成os-like。比如,将线程机制,管道机制等不少os核心部分的功能。在语言层,也将引入包机制,类oo机制等。同时,预计将开发一套类似于win32 api的,应用程序开发api(于ext的ui层api不同,应该说稍微还要低一点,类似于用.net开发win32程序那样),为使用js来开发RIA提供方便、快捷且接近于win32开发的途径。这套库的架构设想是等同于win32的架构。
之所以,选择这样的架构思路,因为觉得虽然目前很出色的库层出不穷,但是基本上大多数的重心都是基于ui层的,且开发者似乎也只对ui层感兴趣。也许我的做法,有点固执或者说吃力不讨好。但谁让我喜欢js呢。说我是自娱自乐也好,得不偿失也好,我的观念就是坚持到底,快乐就好!