1.类的由来
1.1 C 语言中的结构体
这部分属于历史问题,与技术无关,了解历史可以让我们更好地把握现在和将来。C 语言中的结构体 struct 可以说是类的最原始的雏形。只有 int, float, char 这些基本数据类型是不够的,有时需要用不同的数据类型组合成一个有机的整体来使用。例如一个学生有学号和姓名就可以定义一个 Student 的结构体:
struct Student {
int id;
char[20] name;
} student;
main() {
// 可以使用“对象名.属性” 的方式来操作数据
student.id = 5;
student.name = “ZhangSan”;
}
// 可以使用“对象名.属性” 的方式来操作数据
student.id = 5;
student.name = “ZhangSan”;
}
1.2 从结构体到类的演化(C —— C++)
C 中的结构体 C++ 中的结构体
struct 结构名 {
数据成员
}; struct 结构名 {
数据成员
成员函数
}
struct 结构名 {
数据成员
}; struct 结构名 {
数据成员
成员函数
}
C++ 首次允许在结构体中定义成员函数!那么再将 struct 关键字换成 class 不就是我们现在所看到的类的形态了吗?
class Student {
private:
int id;
char[20] name;
public:
void gotoSchool() {}
}
C++ 最初的名字叫做“C with class”(带类的C),经过长时间的发展,最终敲定将其命名为 C++,“++”表示加一操作,代表它比 C 语言更进步,更高级。
面向过程的编程就是在处理一个个函数,而现在的面向对象编程处理是函数加数据,形式上就这么点儿差别。也许刚刚接触时它时会感到有些困难,这很正常。一旦你真正了解它,那你一定会爱上它。所以,请大家不要畏惧,技术永远向着更方便,更简单,更高效的方向发展,而不会向越来越难,越来越复杂发展。对于面向对象程序设计(OOP)而言,代表着越来越接近人类的自然语言,越来越接近人类的思维,因此一切都会变得越来越简单。
从结构体到类的演变过程中我们看到,类中是可以定义函数的。因此,引出了面向对象三大特性之一,封装。
2.封装(Encapsulation)
2.1 封装的概念
封装的定义:把过程和数据包围起来,对数据的访问只能通过已定义的界面。在程序设计中,封装是指将数据及对于这些数据有关的操作放在一起。
知道这些定义,并不能代表技术水平有多高。但是如果去参加面试也许会用得着。简单解释一下,它的意思是指把成员变量和成员函数放在一个类里面,外面要想访问该类的成员变量只能通过对外公开的成员函数来访问。用户不需要知道对象行为的实现细节,只需根据对象提供的外部接口访问对象即可。
这里有一个原则叫做“信息隐藏”—— 通常应禁止直接访问成员变量,而应该通过对外公开的接口来访问。下面,看一个小例子:
class Father {
private var money:int = 10000000;
public takeMoney():int {
money -= 100;
return 100;
}
}
定义名为 Father 的类(一个有钱的父亲),类中有一个成员变量 money 它的访问权限为 private 意思是说 money 是 Father 私有的,private 是指只有在这个类里面才能访问,出了这个类就无法访问该成员了,稍后会有关于访问权限更多的解释。
类中还定义了 takeMoney() 这个方法,它是 public 的,可以说是对外公开的一个接口。
从这个例子中可以看出,任何人要想从 Father 类取一些 money 的话,都只能通过 takeMoney() 这个方法去拿,而不能直接访问 money 这个属性,因为它是私有的。只有通过 takeMoney() 这个公开的方法从能修改 Father 类的成员变量让 money -= 100 —— 每次只能给你 100 元。对外只能看到 takeMoney() 这个方法,该方法如何实现的别人不知道,反正你每次只能得到 100 块。
2.2 封装的好处
封装的好处:保证了模块具有较高的独立性,使得程序的维护和修改更加容易。对应程序的修改仅限于类的内部,将程序修改带来的影响减少到最低。
2.3 封装的目的
(1)隐藏类的实现细节;
(2)迫使用户通过接口去访问数据;
(3)增强代码的可维护性。
(1)隐藏类的实现细节;
(2)迫使用户通过接口去访问数据;
(3)增强代码的可维护性。
2.4 封装的技巧
按照纯面向对象编程的思想,类中所有的成员变量都应该是 private 的,要操作这些私有的成员变量只能通过对外公开的函数来完成。实际工作中,有时也常把变量的访问权限设置为 public 目的就是为了调用起来方便。在 AS 3 中提供了 get/set 关键字,它能让我们以函数调用的方式处理属性。按照封装的原则,再结合 get/set 如何去操作一个成员变量呢?看下面这个例子:
class Person {
private var _age:int;
public function get age():int {
return _age;
}
public function set age(a:int):void {
if (a < 0) {
trace("age 不能小于 0!");
return;
}
_age = a;
}
}
首先,实例化出该类的对象 var person:Peron = new person()。如果要设置成员变量 _age 就要通过调用 set age() 来实现:person.age = 5。实际上,我们是在调用 set age(5) 这个方法,但是由于有了 set 修饰符,操作的方法就是给对象的属性赋值,但实际上是在调用函数。但是如果这么写 person.age = -20,这样合理吗?一个人的年龄等于 -20 岁,显然不对。因此在调用 set age() 方法中,可以进行一下判断,如果给的参数不对就发出提示,这项工作只能由函数来完成,这是属性没办法到的。
回想一下封装的定义,它的用意就在于数据(成员变量)是核心,不可以随随便便去改,要想改只能去调用公开的函数,而在函数可以进行各种判断,保证对数据的修改是经过思考的,合理的。
说到 get/set 现在去看看 ActionScript 3 是怎么做的。我们知道 TextField 类有一个 text 属性,查看帮助文档,会看到:
text 属性
实现
public function get text():String
public function set text(value:String):void
实现
public function get text():String
public function set text(value:String):void
可见这个属性也是通过 get/set 方法实现的,虽然我们是用“对象名.text”方式去操作 text 属性,但实际上是在调用 get/set text(),那么这两个函数具体怎么实现的呢?Who knows!这就叫隐藏实现。AS 3 里面所有类的属性都是这样实现的,只有 get 没有 set 的是只读属性。
封装时还要考虑,这个方法应该归属哪个类。首先,逻辑上要说得过去,例如制作一个贪吃蛇的游戏,有一个方法叫 eat(),“吃”这个方法应该给谁?给文档类?给食物?当然应该放到蛇这个类里面,蛇去吃食物这才合理。然后,还要注意该方法要用到哪些属性。例如,一个人拿粉笔在黑板上画圆,那么画圆这个方法应该归属哪个类。人?黑板?粉笔?画圆的时候需要知道圆的半径和圆心,而这些属性都在圆这个类里面,所以画圆这个方法应该放在圆这个类里面。
下面,学习访问控制修饰符。
封装时还要考虑,这个方法应该归属哪个类。首先,逻辑上要说得过去,例如制作一个贪吃蛇的游戏,有一个方法叫 eat(),“吃”这个方法应该给谁?给文档类?给食物?当然应该放到蛇这个类里面,蛇去吃食物这才合理。然后,还要注意该方法要用到哪些属性。例如,一个人拿粉笔在黑板上画圆,那么画圆这个方法应该归属哪个类。人?黑板?粉笔?画圆的时候需要知道圆的半径和圆心,而这些属性都在圆这个类里面,所以画圆这个方法应该放在圆这个类里面。
下面,学习访问控制修饰符。
3. 访问控制(Access Control)
修饰符 类内可见 包内可见 子类可见 任何地方
private Yes
protected
Yes
Yes
internal(默认) Yes Yes
Yes
public Yes Yes Yes Yes
private Yes
protected
Yes
Yes
internal(默认) Yes Yes
Yes
public Yes Yes Yes Yes
(1)private:仅当前类可访问,称为私有成员;
(2)internal(默认):包内可见,在同一文件夹下可访问(如果不写权限修饰符,默认权限也是它);
(3)protected:子类可访问,与包无关。如果不是该类的子类,那么 protected 和 private 是一样的;
(4)public:完全公开。任何地方、任何对象都可以访问这个类的成员。
4. 继承(Inheritance)
4.1 继承的概念
下面进入面向对象的第二大特性 —— 继承。
继承的定义:继承是一个类可以获得另一个类的特性的机制,继承支持层次概念。
继承是一种代码重用的形式,允许程序员基于现有类开发新类。现有类通常称为"基类"或"超类",新类通常称为"子类"或"派生类"。通过继承还可以在代码中利用多态。
继承的好处:继承最大的好处是代码的重用,与它同样重要的是它带来了多态。关于多态后面会给大家非常详细的讲解,我们现在只讨论继承。
AS 3 和 Java 一样都是单根继承的。在 AS 3 中不论哪个类都有一个相同的唯一的老祖宗 —— Object。拿我们最熟悉的 Sprite 类来说,查看一下帮助文档:
包 flash.display
类 public class Sprite
继承 Sprite→DisplayObjectContainer→InteractiveObject→DisplayObject→EventDispatcher→Object
子类 FLVPlayback, FLVPlaybackCaptioning, MovieClip, UIComponent
类 public class Sprite
继承 Sprite→DisplayObjectContainer→InteractiveObject→DisplayObject→EventDispatcher→Object
子类 FLVPlayback, FLVPlaybackCaptioning, MovieClip, UIComponent
从这里可以清楚地看到,Sprite 的父类是 DisplayObjectContainer,而 DisplayObjectContainer 的父类是 InteractiveObject。一直往下找,最终都能够找到 Object类,不仅是 Sprite,所有的类皆如此,这就叫单根继承。AS 单根继承的思想应该说是从 Java 借鉴来的,实践证明单根继承取得了很大的成功。Java 发明的单根继承摒弃了 C++ 的多继承所带来的很多问题。
虽然在 AS 3 中不能直接利用多继承,但是可以通过实现多个接口(interface)达到相同的目的,关于接口后面会单独讲解。单继承和多继承相比,无非多敲些代码而已,但是它带来的巨大好处是潜移默化的。具体有哪些好处这里就不详细讨论了,总之是非常非常多,大家如果有兴趣可以到网上搜索一下相关内容。
我们知道 Sprite 类有 x 和 y 属性,现在请大家在帮助文档中查找出 Sprite 类的这两个属性。你一定是先去打开 Sprite 这个类,但只看到五个属性,里面没有我没要找到 x, y 属性!怎么回事?这时候应该首先想到的是继承,一定是它的父类里面有,这两个属性是从父类中继承过来的!OK,没错,就是这样,那么就去 DisplayObjectContainer 找找,可是还没找到,再找父类的父类 InteractiveObject 还没有,再找父类的父类的父类 DisplayObject 找到了吧。学会查帮助文档非常重要,知道某个类有某个属性或方法,如果在这个类中找不到就要去它的父类中去找,再找不到就去父类的父类中找,最远到 Object 不信你找不到。
下面看一些例子。
4.2 TestPerson.as —— 属性与方法的继承
package {
public class TestStudent {
public function TestPerson() {
var student:Student = new Student();
student.name = "ZhangSan";
student.age = 18;
student.school = "114";
trace(student.name, student.age, student.school);
}
}
}
package {
public class TestStudent {
public function TestPerson() {
var student:Student = new Student();
student.name = "ZhangSan";
student.age = 18;
student.school = "114";
trace(student.name, student.age, student.school);
}
}
}
class Person {
private var _name:String;
private var _age:int;
public function get name():String{
return _name;
}
public function set name(n:String):void {
if (n == "") {
trace("name 不能为空!");
return;
}
_name = n;
}
public function get age():int {
return _age;
}
public function set age(a:int):void {
if (a < 0) {
trace("age 不能小于 0!");
return;
}
_age = a;
}
}
private var _name:String;
private var _age:int;
public function get name():String{
return _name;
}
public function set name(n:String):void {
if (n == "") {
trace("name 不能为空!");
return;
}
_name = n;
}
public function get age():int {
return _age;
}
public function set age(a:int):void {
if (a < 0) {
trace("age 不能小于 0!");
return;
}
_age = a;
}
}
class Student extends Person {
private var _school:String;
public function get school():String {
return _school;
}
public function set school(s:String):void {
_school = s;
}
}
注意,package 中包的是用来测试的类,为了演示方便Person 和 Student 跟这个测试类放在了一个 as 文件中。
首先定义一个 Person 类,人都有名字(name)和年龄(age),下面让 Student 类继承 Person 类,也就是说学生继承了人的所有特性,或者说“学生是一个人”,这话没错吧! “Student is a Person”,满足 is-a,那么就可以使用继承。Student 在 Person 的基础上还多出了 school 属性,记录着他所在学校的名称。
在测试类中创建了一个 Student 对象,使用 student.name, student.age, student.school,设置学生的姓名,年龄和学校。虽然 Student 类中没有定义 name, age 属性,但这两个属性是它从父类 Person 继承而来的,因此实现了代码的复用,因此 Student 也就拥有了父类属性和方法。
private var _school:String;
public function get school():String {
return _school;
}
public function set school(s:String):void {
_school = s;
}
}
注意,package 中包的是用来测试的类,为了演示方便Person 和 Student 跟这个测试类放在了一个 as 文件中。
首先定义一个 Person 类,人都有名字(name)和年龄(age),下面让 Student 类继承 Person 类,也就是说学生继承了人的所有特性,或者说“学生是一个人”,这话没错吧! “Student is a Person”,满足 is-a,那么就可以使用继承。Student 在 Person 的基础上还多出了 school 属性,记录着他所在学校的名称。
在测试类中创建了一个 Student 对象,使用 student.name, student.age, student.school,设置学生的姓名,年龄和学校。虽然 Student 类中没有定义 name, age 属性,但这两个属性是它从父类 Person 继承而来的,因此实现了代码的复用,因此 Student 也就拥有了父类属性和方法。
4.3 TestExtends.as —— 继承的限制
package {
public class TestExtends {
public function TestExtends() {
new Son();
}
}
}
class Father {
private var money:int = 1000000;
public var car:String = "BMW";
private function work():void {
trace("writing");
}
}
private var money:int = 1000000;
public var car:String = "BMW";
private function work():void {
trace("writing");
}
}
class Son extends Father {
public var bike:String = "YongJiu";
// 没有继承因此谈不上重写
private function work():void {
trace("studying");
};
function Son (){
trace(bike);
trace(car);
//trace(money); // private 的属性不能被继承
work();
}
}
这个例子要演示哪些属性或方法不能被继承。本例中定义了一个 Father 类,其中包括 private 的money 属性,public 的 car 属性和 private 的 work() 方法。
然后定义一个 Son 类继承自 Father 类,在 Son 类中新增加一个 bike 属性,同样也有一个 work() 方法。在 Son 的构造函数中打印出 bike, car属性或调用 work() 方法都没问题。但是不能打印出 money 属性,因为它是 Father 类私有的,回想一下第三节所讲的“访问控制”查一下那个表,可以看到 private 的成员只能在类内访问,不能被继承。那么父类中还有一个 work() 方法也是 private 的,因此也不会被继承,所以在子类中再定义一个 work() 方法也不会有冲突,也就谈不上重写。关于重写,后面还有相关的例子。
学习 OOP 编程时,如果可以理解程序执行时内存的结构,那将对我们学习 OOP 编程有莫大好处。下面就来了解一下内存的结构以及各部分的作用。
public var bike:String = "YongJiu";
// 没有继承因此谈不上重写
private function work():void {
trace("studying");
};
function Son (){
trace(bike);
trace(car);
//trace(money); // private 的属性不能被继承
work();
}
}
这个例子要演示哪些属性或方法不能被继承。本例中定义了一个 Father 类,其中包括 private 的money 属性,public 的 car 属性和 private 的 work() 方法。
然后定义一个 Son 类继承自 Father 类,在 Son 类中新增加一个 bike 属性,同样也有一个 work() 方法。在 Son 的构造函数中打印出 bike, car属性或调用 work() 方法都没问题。但是不能打印出 money 属性,因为它是 Father 类私有的,回想一下第三节所讲的“访问控制”查一下那个表,可以看到 private 的成员只能在类内访问,不能被继承。那么父类中还有一个 work() 方法也是 private 的,因此也不会被继承,所以在子类中再定义一个 work() 方法也不会有冲突,也就谈不上重写。关于重写,后面还有相关的例子。
学习 OOP 编程时,如果可以理解程序执行时内存的结构,那将对我们学习 OOP 编程有莫大好处。下面就来了解一下内存的结构以及各部分的作用。
4.4 内存分析
根据不同的操作系统内存的结构可能有所差异,但是通常在程序执行中会把内存分为四部分:
(1)heap 堆:存放 new 出来的对象;
(2)stack 栈:局部变量;
(3)data segment 数据段:静态变量和字符串常量;
(4)code segment 代码段:存放代码。
后面会在一些例子中利用内存图来帮助理解。先看下面一个例子 this 和 super。
(2)stack 栈:局部变量;
(3)data segment 数据段:静态变量和字符串常量;
(4)code segment 代码段:存放代码。
后面会在一些例子中利用内存图来帮助理解。先看下面一个例子 this 和 super。
4.5 this 和 super 关键字
4.5.1 TestSuper.as —— this 和 super 以及 override
package {
public class TestSuper {
public function TestSuper() {
new Extender();
}
}
}
class Base {
public var name:String;
public var age:uint;
//function Base(){
// 即使不写构造函数,系统也会自动添加的无参构造函数
//}
public function work():void {
trace("Writing!");
}
}
public var name:String;
public var age:uint;
//function Base(){
// 即使不写构造函数,系统也会自动添加的无参构造函数
//}
public function work():void {
trace("Writing!");
}
}
class Extender extends Base {
//public var name:String; // name 属性已从父类继承,不能重复定义
function Extender() {
// super(); // 即使不写 super(),系统也会添加的无参的 super();
super.work();
this.work();
}
override public function work():void {
trace("Programming!");
}
}
这个例子演示了 this 与 super 关键字的使用,以及重写(override)的概念。
首先,定义 Base 类代表基类,它被 Extender 类继承。Base 类中定义两个 public 的属性 age 和 name,以及一个公有的方法 work()。Extender 类继承了 Base 类的这些属性和方法,但是如果要想在 Extender 类中再重复定义 name 或 age 属性就不行了,因为它已经继承了这两个属性,不能再重复定义。但是方法可以被重新定义,前提是要在方法前面加上 override 关键字,显示地说明要重写(覆盖)基类的 work() 方法。
这一样来就有两个 work() 方法了如何区分它们呢?我们可以在方法名前面显示地加上 super 或 this 关键字,见 Extender 的构造函数。super.work() 表示调用父类的 work() 方法,this.work() 表示调用该类中的 work() 方法。
override 关键字表示对父类方法的重写,override 是利用继承实现多态的必要手段。
何时使用重写呢?当我们对父类的方法不满意、或者同样的一个方法对于两个类来说实现的功能不相同时,就要考虑重写,把父类的方法冲掉,让它改头换面。
下面我们看一下内存图中,this 和 super 各代表着什么。
//public var name:String; // name 属性已从父类继承,不能重复定义
function Extender() {
// super(); // 即使不写 super(),系统也会添加的无参的 super();
super.work();
this.work();
}
override public function work():void {
trace("Programming!");
}
}
这个例子演示了 this 与 super 关键字的使用,以及重写(override)的概念。
首先,定义 Base 类代表基类,它被 Extender 类继承。Base 类中定义两个 public 的属性 age 和 name,以及一个公有的方法 work()。Extender 类继承了 Base 类的这些属性和方法,但是如果要想在 Extender 类中再重复定义 name 或 age 属性就不行了,因为它已经继承了这两个属性,不能再重复定义。但是方法可以被重新定义,前提是要在方法前面加上 override 关键字,显示地说明要重写(覆盖)基类的 work() 方法。
这一样来就有两个 work() 方法了如何区分它们呢?我们可以在方法名前面显示地加上 super 或 this 关键字,见 Extender 的构造函数。super.work() 表示调用父类的 work() 方法,this.work() 表示调用该类中的 work() 方法。
override 关键字表示对父类方法的重写,override 是利用继承实现多态的必要手段。
何时使用重写呢?当我们对父类的方法不满意、或者同样的一个方法对于两个类来说实现的功能不相同时,就要考虑重写,把父类的方法冲掉,让它改头换面。
下面我们看一下内存图中,this 和 super 各代表着什么。
4.5.2 this 和 super 的内存分析
测试类中有一条语句 new Extender()。new 出来的对象放在堆内存(Heap Segment)中。在对象内部隐藏着两个对象:this 和 super。
this 持有当前对象的引用;super 持有父类的引用。
上例中我们有两种调用的方法:super.work() 和 this.work()。如果不加修饰,只写 work() ,那么系统默认调用的是 this.work()。
既然系统默认调用的就是 this.work(),那还要 this 有什么用?比如,当局部变量与成员变量同名时,如果要在属于这个局部变量的上下文中引用成员变量,那么就要显示地调用“this.成员变量”名来指定引用的是成员变量而非局部变量。请看下面一个例子。
4.5.3 TestThis.as —— 用 this 区分局部变量与成员变量
package {
public class TestThis {
public function TestThis() {
var p:Person = new Person();
p.setInfo("ZhangSan", 20);
}
}
}
class Person {
private var name:String;
private var age:int;
public function setInfo(name:String, age:int) {
this.name = name;
this.age = age;
}
}
本例中就是用 this.name 指定是成员变量 name,而非传进来的局部变量的 name。age 也是如此。下面请看内存分析。
private var name:String;
private var age:int;
public function setInfo(name:String, age:int) {
this.name = name;
this.age = age;
}
}
本例中就是用 this.name 指定是成员变量 name,而非传进来的局部变量的 name。age 也是如此。下面请看内存分析。
传进去的两个变量 name 和 age 属于临时变量存放在栈内存(Stack Segment),setInfo() 方法中的 this.name 和 this.age 则指的是该对象的成员变量 name 和 age。由于 name 和 age 都是基元数据类型,因此直接传值,把 “zhang” 和 20 赋给了 Person 的成员变量。最后在 setInfo() 执行完成后,为该方法分配的临时变量全部释放,赋值工作完成。
4.6 初始化顺序
下面 Think in Java 中的一段演示代码,见 TestSandwich.as:
package {
public class TestSandwich {
public function TestSandwich() {
new Sandwich();
}
}
}
class Water {
//static var w = trace("static water");
function Water() {
trace("Water");
}
}
//static var w = trace("static water");
function Water() {
trace("Water");
}
}
class Meal {
//static var w = trace("static meal");
function Meal() {
trace("Meal");
}
}
//static var w = trace("static meal");
function Meal() {
trace("Meal");
}
}
class Bread {
function Bread() {
trace("Bread");
}
}
function Bread() {
trace("Bread");
}
}
class Cheese {
function Cheese() {
trace("Cheese");
}
}
function Cheese() {
trace("Cheese");
}
}
class Lettuce{
function Lettuce() {
trace("Lettuce");
}
}
function Lettuce() {
trace("Lettuce");
}
}
class Lunch extends Meal {
function Lunch() {
trace("Lunch");
}
}
function Lunch() {
trace("Lunch");
}
}
class PortableLunch extends Lunch {
//static var w = trace("static lunck");
function PortableLunch() {
trace("PortableLunch");
}
}
//static var w = trace("static lunck");
function PortableLunch() {
trace("PortableLunch");
}
}
class Sandwich extends PortableLunch {
var bread:Bread = new Bread();
var cheese:Cheese = new Cheese();
var lettuce:Lettuce = new Lettuce();
//static var good = trace("static sandwich");
function Sandwich() {
trace("Sandwich");
}
}
测试类很简单只有一句:new Sandwich()。构造出 Sandwich 类一个实例。
Sandwich 类继承了 PortableLunch 这个类。现在有一个问题,是先有子类还是先有父类?是先有父亲后有儿子,还是先有儿子后有父亲?肯定是先有父亲。那么怎么有的父亲?需要先构造出来。怎么构造?调用构造函数!
因此,我们说在构造子类之前,要先将它的父类构造出来,如果父类还有父类,就要先把父类的父类构造出来。在这段程序中每个类在构造出来后都会打印出该类的类名。下面请看执行结果:
Bread
Cheese
Lettuce
Meal
Lunch
PortableLunch
Sandwich
我们看到,最先打印出来的是 Bread, Cheese, Lettuce。这是 Sandwich 类的三个成员变量,可见在调用构造函数之前,要新将该类的成员变量构造出来,然后再去构造这个类本身。
前面提到,要构造这个类就先要构造它的父类,Sandwich 的父类是 PortableLunch,而 PortableLunch 还有父类叫 Lunch,而 Lunch 还有父类叫 Meal,到了 Meal 就终止了。这时将执行 Meal 的构造函数,有了 Meal 之后就可以构造Lunch 了,有了 Lunch,PortableLunch 就可以构造,有了 PortableLunch 我们的 Sandwich 才被构造出来。
现在我们得到的结论就是:类的成员变量先被初始化,然后才是构造函数。
下面,请大家把代码中注释掉的部分全部打开。现在,又新加入了一些 static 的成员变量,我们来实验一下静态的成员变量是何时被调用的,执行结果如下:
static water
static meal
static lunck
static sandwich
Bread
Cheese
Lettuce
Meal
Lunch
PortableLunch
Sandwich
我们看到,所有静态的成员都先于非静态成员变量被构造出来!最上面有一个 Water 类,虽然没有地方会用到它,但是它也被打印出来了。结论是:当类被加载时静态的属性和方法就会被初始化。注意,什么叫类被加载时?这是指类被加载到内存里面。可见,我们整个这个 as 文件中的所有类都被加载到了内存中了,只要这个类被读入到内存中,那么它的所有静态成员就会被初始化。
最终的结论 —— 初始化顺序:
(1)当类被加载时该类的静态的属性和方法就会被初始化
(2)然后初始化成员变量
(3)最后构造出这个类本身
OK,既然这里提到了静态成员,下面我们就来了解一下它。
var bread:Bread = new Bread();
var cheese:Cheese = new Cheese();
var lettuce:Lettuce = new Lettuce();
//static var good = trace("static sandwich");
function Sandwich() {
trace("Sandwich");
}
}
测试类很简单只有一句:new Sandwich()。构造出 Sandwich 类一个实例。
Sandwich 类继承了 PortableLunch 这个类。现在有一个问题,是先有子类还是先有父类?是先有父亲后有儿子,还是先有儿子后有父亲?肯定是先有父亲。那么怎么有的父亲?需要先构造出来。怎么构造?调用构造函数!
因此,我们说在构造子类之前,要先将它的父类构造出来,如果父类还有父类,就要先把父类的父类构造出来。在这段程序中每个类在构造出来后都会打印出该类的类名。下面请看执行结果:
Bread
Cheese
Lettuce
Meal
Lunch
PortableLunch
Sandwich
我们看到,最先打印出来的是 Bread, Cheese, Lettuce。这是 Sandwich 类的三个成员变量,可见在调用构造函数之前,要新将该类的成员变量构造出来,然后再去构造这个类本身。
前面提到,要构造这个类就先要构造它的父类,Sandwich 的父类是 PortableLunch,而 PortableLunch 还有父类叫 Lunch,而 Lunch 还有父类叫 Meal,到了 Meal 就终止了。这时将执行 Meal 的构造函数,有了 Meal 之后就可以构造Lunch 了,有了 Lunch,PortableLunch 就可以构造,有了 PortableLunch 我们的 Sandwich 才被构造出来。
现在我们得到的结论就是:类的成员变量先被初始化,然后才是构造函数。
下面,请大家把代码中注释掉的部分全部打开。现在,又新加入了一些 static 的成员变量,我们来实验一下静态的成员变量是何时被调用的,执行结果如下:
static water
static meal
static lunck
static sandwich
Bread
Cheese
Lettuce
Meal
Lunch
PortableLunch
Sandwich
我们看到,所有静态的成员都先于非静态成员变量被构造出来!最上面有一个 Water 类,虽然没有地方会用到它,但是它也被打印出来了。结论是:当类被加载时静态的属性和方法就会被初始化。注意,什么叫类被加载时?这是指类被加载到内存里面。可见,我们整个这个 as 文件中的所有类都被加载到了内存中了,只要这个类被读入到内存中,那么它的所有静态成员就会被初始化。
最终的结论 —— 初始化顺序:
(1)当类被加载时该类的静态的属性和方法就会被初始化
(2)然后初始化成员变量
(3)最后构造出这个类本身
OK,既然这里提到了静态成员,下面我们就来了解一下它。
4.7 静态属性与方法
4.7.1 static的概念
静态属性与方法属于这个类,而不属于该类的实例。
静态属性只生成一份。同类对象的静态属性值都相同。改变一个类的静态属性值会影响该类所有的对象。静态属性可以节省内存,方便调用。一个方法即使不声明为静态的实际上也只生成一份,因为除了方法所处理的数据不同外,方法本身都相同的。
静态属性和静态方法中不能存在非静态的属性和方法,或者说 static 的属性和方法中不能出现 this。
4.7.2 TestStatic.as —— static 属于这个类,不属于该类实例
package {
public class TestStatic {
public function TestStatic() {
var b:Ball = new Ball();
// 静态属性或方法属于这个类,而不属于该类对象,只能用类名引用
// b.sMethod();
// trace(b.color);
Ball.sMethod();
trace(Ball.color);
}
}
}
class Ball {
public var xpos:Number = 300;
public var xpos:Number = 300;
public static var color:uint = 0xff0000;
public function changeColor():void {
color = 0;
}
public static function sMethod():void {
trace("I'm a static Method");
//trace(xpos); // 静态方法中不能调用非静态成员变量
//changeColor(); // 静态方法中不能调用非静态成员函数
}
}
在 Ball 这个类中有一个静态属性 color,一个静态方法 sMethod()。注意这条原则:静态属性与方法属于这个类,而不属于该类的实例。在 sMethod 中不能出现非静态的成员变量或方法。反之可以,在非静态的成员变量或方法中可以调用静态的成员变量或方法。
注意,在测试类 TestStatic 中,要访问静态的成员变量或方法只能通过“类名.方法(或属性)”的形式去调用,而不能通过“实例名.方法(或属性)”的形式调用。
由于静态属性只生成一份,所有该类对象都共享这一个,因此可以节省一部分内存,并且可以一改全改。既然大家都用一份属性,那就不能存在差别化了,这也是声明静态属性的一个原则,当所有对象都共用一个相同的属性时,可考虑将其声明为静态属性。而静态方法带来的好处就是方便引用。
下面,我们看看在设计模式中是如何使用 static 的,有请单例模式。
color = 0;
}
public static function sMethod():void {
trace("I'm a static Method");
//trace(xpos); // 静态方法中不能调用非静态成员变量
//changeColor(); // 静态方法中不能调用非静态成员函数
}
}
在 Ball 这个类中有一个静态属性 color,一个静态方法 sMethod()。注意这条原则:静态属性与方法属于这个类,而不属于该类的实例。在 sMethod 中不能出现非静态的成员变量或方法。反之可以,在非静态的成员变量或方法中可以调用静态的成员变量或方法。
注意,在测试类 TestStatic 中,要访问静态的成员变量或方法只能通过“类名.方法(或属性)”的形式去调用,而不能通过“实例名.方法(或属性)”的形式调用。
由于静态属性只生成一份,所有该类对象都共享这一个,因此可以节省一部分内存,并且可以一改全改。既然大家都用一份属性,那就不能存在差别化了,这也是声明静态属性的一个原则,当所有对象都共用一个相同的属性时,可考虑将其声明为静态属性。而静态方法带来的好处就是方便引用。
下面,我们看看在设计模式中是如何使用 static 的,有请单例模式。
4.7.4 单例模式(Singleton Pattern)
单例模式是指“只能有一个该类的对象存在”。
单例模式有什么用处?例如系统的缓存、注册表、日志、显卡驱动、回收站等等都是独一无二的,都只有一个。如果造出了多个实例,就会导致系统出问题。这时就需要用到单例模式,原则就是该类的对象只能有一个。
首先,如果一个类的构造函数是公开的,那么就有可能被其它地方 new 出来,首先将构造函数变为私有的。遗憾的是 ActionScript 3 中,构造函数只能是 public 的,不能为 private。以下是 AS 3 版本的单例模式:
package {
public class Singleton {
static private var instance:Singleton;
public function Singleton(singletonEnforcer:SingletonEnforcer) {}
public static function getInstance():Singleton {
if (instance == null) {
instance = new Singleton(new SingletonEnforcer());
}
return instance;
}
}
}
class SingletonEnforcer { }
单例模式有什么用处?例如系统的缓存、注册表、日志、显卡驱动、回收站等等都是独一无二的,都只有一个。如果造出了多个实例,就会导致系统出问题。这时就需要用到单例模式,原则就是该类的对象只能有一个。
首先,如果一个类的构造函数是公开的,那么就有可能被其它地方 new 出来,首先将构造函数变为私有的。遗憾的是 ActionScript 3 中,构造函数只能是 public 的,不能为 private。以下是 AS 3 版本的单例模式:
package {
public class Singleton {
static private var instance:Singleton;
public function Singleton(singletonEnforcer:SingletonEnforcer) {}
public static function getInstance():Singleton {
if (instance == null) {
instance = new Singleton(new SingletonEnforcer());
}
return instance;
}
}
}
class SingletonEnforcer { }
这里用 SingletonEnforcer 类作为 Singleton 类构造函数的参数,由于这两个类放在一个 as 文件中,instance这个类只能被 Singleton 访问到,保证其它类得不到 SingletonEnforcer 的实例,用这种方法达到私有构造函数的作用。
这就是单例模式,一个最简单的设计模式!单例模式还可分为饿汉式/懒汉式。上面演示的是懒汉式单例模式,它在第一次调用 getInstance() 方法时才 new 出这个对象来。没人调用它的话,它永远不会主动去做,是不是很懒?!另一种是饿汉式的,instance 一上来就 new 出来该类的实例。
这就是单例模式,一个最简单的设计模式!单例模式还可分为饿汉式/懒汉式。上面演示的是懒汉式单例模式,它在第一次调用 getInstance() 方法时才 new 出这个对象来。没人调用它的话,它永远不会主动去做,是不是很懒?!另一种是饿汉式的,instance 一上来就 new 出来该类的实例。