今天说一下ActionScript3.0的复杂数据类型,其实也就是传说中的引用数据类型,分为类、接口和函数。
其实严格说起来,String既然可以等于null,便已经说明它是引用类型了,由于它的其他特征很像个简单类型,姑且划到简单类型吧。
类和接口都不奇怪,几乎所有面向对象的语言都有的,函数就不一定了,既然ActionScript3.0把函数当作对象,那它应该是支持闭包的,相当于函数指针之类的,这些我就不太熟了,支持闭包的Javascript学得不够好的缘故。
ActionScript3.0用来管理引用数据类型的依然是包,和Java一样都是基于目录结构的,但存在一个顶级包的概念,放置能被所有地方使用的类或函数。看来我之前的理解还是有些偏差,认为函数的存在打破了ActionScript3.0的完全面向对象,其实不然,和Java里面不一样,它充当的并不仅仅只是类的方法的角色,它自己本身也是能被实例化为对象的,类、属性、方法都可以是对象,这回真是完全面向对象了。
至于事件,采用的依然是经典的观察者模式,只不过监听器不是实现接口的对象了,而是一个回调函数,参数列表符合规范就行了,一般根据事件的不同为flash.events.Event的一个子类,而事件处理代码则写在回调函数里面就行了,其他的和传统事件监听模型无不同。注册方法为:对象.addEventListener(事件类型,处理函数)。
下面是一个注册事件的例子:
var mc:MovieClip = new MovieClip(); mc.addEventListener(MouseEvent.CLICK, onClick); function onClick(event:MouseEvent):void { trace("MovieClip被点击了!"); } addChild(mc);
ActionScript3.0里面专门有一种类叫做显示类,是可以显示在舞台上的,通过addChild方法添加到舞台上就可以了,然后可以在上面进行绘制操作,常见的包括MovieClip、Sprite、Bitmap、Shape等等。
ActionScript3.0支持动态类型,即可以在运行时向里面动态地添加属性和方法,和Javascript里面很像,然后我们可以通过delete关键字删除掉动态对象里面我们不想再要的属性和方法,格式是:delete 对象.属性。
例如:
// 声明一个动态对象 var obj:Object = { name:"xxx", age:18 }; // 输出xxx trace(obj.name); // 删除属性成功,返回true trace(delete obj.name); // 输出undefined trace(obj.name);
删除成功会返回true,然后我们再访问该属性或方法,就会变成undefined。这和直接将该属性设置为undefined有何区别?这个我们容后解答。
我们也可以用delete删除掉数组里面对应索引的元素。
例如:
var arr:Array = [3, 2, 1, 0]; // 输出1 trace(arr[2]); // 输出4 trace(arr.length); // 输出true,删除成功 trace(delete arr[2]); // 输出undefined,确实删除成功了 trace(arr[2]); // 输出4,数组长度并未发生变化 trace(arr.length); // 再次给对应索引元素赋值 arr[2] = 5; // 输出5,该数组元素依然可用 trace(arr[2]);
从上面的例子可以看出,只是对应索引的值变为了undefined,并未真正删除,数组的长度没有变化,和直接将该索引的值赋为undefined效果并没有区别,真是这样吗?我们后面还有讨论。还有,对于非动态添加的属性和方法使用delete会报Error。
同时,可以通过中括号运算符[]来访问对象里面的属性和方法。
例如:
// 声明一个动态对象 var obj:Object = { name:"xxx", age:18 }; // 输出xxx trace(obj.name); // 输出xxx,和obj.name效果一样 trace(obj["name"]); // 创建一个包含非动态属性的MovieClip var mc:MovieClip = new MovieClip(); // 非动态属性也可以通过[]访问 trace(mc["x"]); // 方法同样可以通过[]访问 trace(mc["toString"]());
用它的好处是我们可以通过字符串的方式随意指定要访问的属性和方法,而不用硬编码。大家明显可以看得出来,所谓动态类型,其实就是关联数组,通过键值对的形式访问值,所以ActionScript3.0里面的动态类型,和关联数组其实没什么差别。所以,既然通过方法名来访问方法,不关参数列表的事,自然也就没有什么方法重载了,要通过其他的方式来实现。
至于ActionScript3.0里面的数组,是我见过的最强大的数组,之后会专门讨论。
下面看一下类型检测,ActionScript3.0里面有两种方式。一种是typeof()函数,格式是:typeof(对象)。
例如:
// 返回number trace(typeof(3)); // 返回boolean trace(typeof(true)); // 返回string trace(typeof("xxx")); // 返回object trace(typeof(new Object()));
但是个人感觉没太大的用处,它返回的是表示类型的一个字符串,而且也太笼统,对于int、uint、Number和NaN返回number,对于Boolean返回boolean,对于String返回string,对于XML和XMLList返回xml,对于Function返回function,对于undefined返回undefined,其它的几乎都返回object,除了有限的几个以外,大部分的类型不能做精确判断。
另一种是is运算符,格式是:对象 is 类名。
例如:
var mc:MovieClip = new MovieClip(); // 返回true trace(mc is MovieClip); // 返回true trace(mc is Object); // 返回false trace(mc is Shape); // 返回true,看来对于没有浮点部分的数,还是当作int处理 trace(3.0 is int); // 返回true,看来NaN也是Number trace(NaN is Number); // 返回false,看来null不属于任何类型 trace(null is Object); // 返回false,看来undefined也不属于任何类型 trace(undefined is Object);
和C#里面很像(据说是废弃掉了ActionScript2.0里面的instanceof运算符,我怎么感觉它像是从Java阵营跑到.Net阵营了呢?),判断属于某种类型或它的子类,或者实现了某个接口,返回的是Boolean,这个就相当精确了。
还有一种特殊的检测,就是“包含”,关键字是in,对于索引数组,它检测对应的索引在数组中是否存在,格式是:索引 in 数组。
例如:
var arr:Array = [3, 2, 1, 0]; // 输出1 trace(arr[2]); // 输出true trace(2 in arr); // 输出false,看来in操作符的确是检查索引的,索引为4显然超出范围 trace(4 in arr); // 将索引2的元素置为undefined arr[2] = undefined; // 输出true,索引2位置的元素依然存在 trace(2 in arr); // 输出true,删除成功 trace(delete arr[2]); // 输出false,索引2位置的元素已经不在了 trace(2 in arr);
对于关联数组和其他的普通对象,它检测对应的“键”(属性和方法可以通过“[]”访问,它们也是“键”)是否存在,格式是:键名 in 对象。
例如:
var mc:MovieClip = new MovieClip(); // 给MovieClip添加一个动态属性prop mc.prop = "prop"; // 将属性置为undefined mc.prop = undefined; // 输出undefined trace(mc["prop"]); // 输出true,看来将属性赋为undefined并未删除属性prop trace("prop" in mc); // 输出true,看来已经删除了mc的属性prop trace(delete mc.prop); // 输出false,看来这回真的删除了属性prop trace("prop" in mc); // 输出true,看来非动态的属性和方法一样有用 trace("x" in mc); trace("toString" in mc);
结合in的使用,我们可以看出,delete和直接赋值为undefined还是有区别的,delete是删除了对应索引或键的引用,而直接赋值是将索引或键的引用重新指向undefined,该引用依然存在。
ActionScript3.0里面对于复杂类型的强制转换也有两种方式。一种和简单类型一样,格式为:类名(对象)。
例如:
var obj:Object = new MovieClip(); // 未报任何Error,转换正常 var mc:MovieClip = MovieClip(obj); // 报TypeError,转换错误 var s:Shape = Shape(obj); // 输出NaN trace(Number(obj)); // 输出0 trace(int(obj)); // 输出true trace(Boolean(obj)); // 输出[object MovieClip],相当于调用toString()方法 trace(String(obj));
但是如果转换失败会报TypeError,通过验证发现,复杂类型转换成简单类型,是不会报TypeError的,Number的转换结果是NaN,int和uint的转换结果是0,Boolean的转换结果是true,String的转换结果是相当于调用对象的toString()方法,但是反过来即简单类型转换成引用类型是一定会报TypeError的,因为向简单类型的强制转换实际上是调用的顶级包的函数,里面有良好的逻辑判断,而向复杂类型的转换却无法做到,因为类、接口等的类型是可以自定义的,数量是不确定的。
另一种是as运算符,格式是:对象 as 类名。
例如:
var obj:Object = new MovieClip(); var mc:MovieClip = obj as MovieClip; // 输出[object MovieClip],转换正常 trace(mc); var s:Shape = obj as Shape; // 输出null,转换失败 trace(s);
又是C#特色,转换不成功时会返回null,可以由此来判断是否转型成功,它将处理Error的问题转换成为了判断一个引用是否为null问题,个人认为成本要低一些,看到Error心里会有些怕怕的。当初还和同事就C#里面的as操作符争议过这个问题(当然,当时我们都是做Java的,我偶尔提及C#里面这个as很好用),他认为异常处理就应该是异常处理,就应该纯粹,而不应该转成空引用的问题,这是见仁见智了。
最后说一下ActionScript3.0的垃圾回收,像Java的JVM一样,ActionScript3.0也有一个虚拟机叫AVM2(ActionScript Virtual Machine 2),它负责回收没有任何引用的对象。常用的算法有两种,一种是引用计数算法,即每增加一个对象的引用,对象的引用计数就加1,每减少一个对象的引用,对象的引用计数就减1,若该对象的引用计数为0,就该被回收了。这种算法效率不错,但在循环引用等场景的时候就会出现问题。
例如:
// a对象的引用计数为1 var a:Object = {}; // b对象的引用计数为1 var b:Object = {}; // b对象的引用计数为2 a.b = b; // a对象的引用计数为2 b.a = a; // a和b对象的引用计数都被置为1,但由于a.b和b.a永远无法再访问到,a和b的引用计数永远都不可能被置为0 a = null; b = null;
由于应用程序里面的对象实在太多,出现循环引用的情况只会更复杂,所以这种算法几乎是毫无意义。
另一种算法是标记清除算法,就是在某个特定的应用场景下(比如一个舞台中),AVM2会自顶部的根对象(比如stage)逐一扫描其中的所有引用,标记发现的对象,然后再逐一扫描这些对象中的所有引用,递归执行到遍历了整个对象树,那些未被标记的对象就是应该被回收的。
这种方式虽然非常准确,但是,由于要遍历整个对象树,是很耗费性能的,所以一般情况下不会被调用,AVM2一般只会在某些特定的情况下才会执行,比如垃圾回收之前,而且AVM2的垃圾回收时间也是不确定的,频率并不高,所以其实一般来说对性能没太大的影响,也算是对时间和空间的一个平衡。