1. 主页
  2. 文档
  3. 学习君土脚本
  4. 君土脚本对象入门
  5. 适合初学者的君土脚本面向对象编程

适合初学者的君土脚本面向对象编程

学完基础后, 现在我们集中于面向对象的君土脚本 —— 本文首先提出了面向对象编程理论的基本观点, 然后介绍如何创建类, 以及如何创建对象.

从零开始面向对象的程序设计

首先,我们从高维度且简化的角度看看面向对象的程序是什么。我们将简单描述面向对象的程序,因为面向对象的程序概念已变得很复杂,如果完整地描述面向对象的程序将使读者难以理解。面向对象的程序的基本思想是:在程序里,我们通过使用对象去构建现实世界的模型,把原本很难(或不可)能被使用的功能简单化并提供出来,以供访问。

对象可以包含相关的数据和代码,这些数据和代码用于表示你所建造的模型是什么样子,以及拥有什么样的行为或功能。对象包(或者叫命名空间)存储(官方用语:封装)着对象的数据(常常还包括函数),使数据的组织和访问变得更容易了;对象也常用作数据存储体,用于在网络上传输数据,十分便捷。

定义一个对象模板

让我们来考虑一个简单的程序,它可以显示一个学校的学生和老师的信息.在这里我们不讨论任何程序语言,我们只讨论面向对象的程序思想.

首先,我们可以回到第一节拿到定义好属性和方法的人物对象。对于一个来说,我们能在他们身上获取到很多信息(他们的住址,身高,鞋码,基因图谱,护照信息,显著的性格特征等等),然而,我们仅仅需要他们的名字,年龄,性别,兴趣这些信息. 然后,我们会基于他们的这些信息, 写一个简短的关于他们自己的介绍. 在最后我们还需要教会他们打招呼。以上的方式被称为抽象-为了我们编程的目标而利用事物的一些重要特性去把复杂的事物简单化.

在君土脚本中,我们用类/*class*/的概念去描述一个对象——类并不完全是一个对象,它更像是一个定义对象特质的模板。

创造一个真正的对象

从上面我们创建的中, 我们能够基于它创建出一些对象 —— 一些拥有中属性及方法的对象。基于我们的这个类,我们可以创建出许许多多的真实的人:

当一个对象需要从类中创建出来时,我们可以实例化类得到对象, 类实例化时会调用类的构造函数. 这种创建对象的过程我们称之为实例化——实例对象被类实例化。

具体的对象

在这个例子里,我们不想要泛指的人,我们想要像老师和学生这样类型更为具体的人。在面向对象的程序里,我们可以创建基于其它类的新类,这些新的子类可以继承它们父类的数据和功能。比起复制来说这样能够使用父对象共有的功能。如果类之间的功能不同,你可以根据需要定义专用的特征。

这是非常有用的,老师和学生具有一些相同的特征比如姓名、性别、年龄,因此只需要定义这些特征一次就可以了。您可以在不同的类里分开定义这些相同的特征,这样该特征会有一个不同的命名空间。比如,一个学生的 问候 可以是 “嘿,我是[姓][名]” (例子 嘿,我是张明),老师的可能会正式一些,比如”你好, 我是[姓]老师” (例子 你好,我是李老师)。

注:多态——这个高大上的词正是用来描述多个对象拥有实现共同方法的能力。

现在可以根据子类创建对象。如:

下面我们来看看面向对象的程序理论如何应用到君土脚本实践中去的。

类和对象

让我们来看看君土脚本如何通过类声明来创建类。

一个简单的例子

让我们看看如何定义”人“这个类。在您的文件中添加以下代码:

类 人 {
  姓名: 文;
  构(姓名: 文) {
    此.姓名 = 姓名;
  }
  问候() {
    回 "你好, " + 此.姓名;
  }
}
定 人1 = 启 人("张三");
定 人2 = 启 人("李四");

我们声明一个这个类。这个类有3个成员:一个叫做姓名的属性,一个构造函数和一个问候方法。
你会注意到,我们在引用任何一个类成员的时候都用了。 它表示我们访问的是类的成员。
最后一行,我们使用启/*new*/构造了这个类的两个实例。 它会创建一个这个类的新对象,并执行构造函数初始化它。

让我们添加以下代码来使用创建的两个实例:

控制台.日志(人1.姓名);
人1.问候();
控制台.日志(人2.姓名);
人2.问候();

酷!您现在看到我们有两个对象,每一个保存在不同的命名空间里,当您访问它们的属性和方法时,您需要使用人1或者人2来调用它们。尽管它们有着相同的姓名属性和 问候()方法, 它们是各自独立的,所以相互的功能不会冲突。注意它们使用的是自己的 姓名 值,这也是使用 关键字的原因,它们使用的是从实参传入形参的自己的值,而不是其它的什么值。

再看看这个构造对象的语法:

定 人1 = 启 人("张三");
定 人2 = 启 人("李四");

上述代码中,关键字  跟着一个类名,用于告知系统我们想要创建一个对象,非常类似函数调用,并把结果保存到变量中。

创建我们最终的类

上面的例子仅仅是简单地介绍如何开始。让我们现在开始创建这个类。

  1. 移除掉您之前写的所有代码, 用如下类定义替代 —— 实现原理上,这与我们之前的例子并无二致, 只是变得稍稍复杂了些:
 类 人 {
    姓名: { 姓: 文, 名: 文 };
    年龄: 数;
    性别: 文;
    兴趣: 文[];
    构(姓: 文, 名: 文, 年龄: 数, 性别: 文, 兴趣: 文[]) {
      此.姓名 = { 姓, 名 };
      此.年龄 = 年龄;
      此.性别 = 性别;
      此.兴趣 = 兴趣;
    }
    描述() {
      控制台.日志(此.姓名.姓 + 此.姓名.名 + '有' + 此.年龄 + '岁。他喜欢' + 此.兴趣[0] + ' 和 ' + 此.兴趣[1] + '。');
    }
    问候() {
      回 "你好, 我是" + 此.姓名.姓 + 此.姓名.名;
    }
  }

接下来加上这样一行代码, 用来创建它的一个对象:

  定 人1 = 启 人("张", "三", 32, '男', ['编程', '乒乓球']);

这样,您就可以像我们定义第一个对象一样访问它的属性和方法了:

  控制台.日志(人1['年龄']);
  控制台.日志(人1.兴趣[1]);
  人1.描述();
  // 等等

进一步的练习

首先, 尝试着写几行代码创建您自己的对象, 接着,尝试读取设置对象中的成员。

此外, 我们的描述()方法里仍有一些问题 —— 纵然您有更多的兴趣列举在兴趣数组中, 描述只会展示您的两个兴趣。 您能想出如何在类定义中解决这个问题吗? 您可以按照您喜欢的方式编写类(您可能需要一些条件判断和循环)。 考虑一下描述语句如何根据性别、兴趣列表中兴趣数目的不同, 显示不同的内容。

继承

在君土脚本里,我们可以使用常用的面向对象模式。 基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类。

看下面的例子:

  类 动物 {
    移动(米制距离: 数 = 0) {
      控制台.日志(`动物移动了${米制距离}米。`);
    }
  }

  类 狗 承 动物 {
    叫() {
      控制台.日志('汪,汪');
    }
  }

  常 狗0 = 启 狗();
  狗0.叫();
  狗0.移动(10);
  狗0.叫();

这个例子展示了最基本的继承:类从基类中继承了属性和方法。 这里,是一个派生类,通过承/*extends*/关键字,它派生自动物这个基类。 派生类通常被称作子类,基类通常被称作超类。

因为继承了动物的功能,因此我们可以创建一个的实例,它能够叫()移动()

下面我们来看个更加复杂的例子。

  类 动物 {
    名: 文;
    构(名0: 文) { 此.名 = 名0; }
    移动(米制距离: 数 = 0) {
      控制台.日志(`${此.名}移动了${米制距离}米。`);
    }
  }

  类 蛇 承 动物 {
    构(名: 文) { 先(名); }
    移动(米制距离 = 5) {
      控制台.日志("爬行...");
      先.移动(米制距离);
    }
  }

  类 马 承 动物 {
    构(名: 文) { 先(名); }
    移动(米制距离 = 45) {
      控制台.日志("奔跑...");
      先.移动(米制距离);
    }
  }

  定 螣 = 启 蛇("螣蛇");
  定 赤兔: 动物 = 启 马("赤兔");

  螣.移动();
  赤兔.移动(34);

这个例子展示了一些上面没有提到的特性。 这一次,我们使用关键字创建了动物的两个子类:

与前一个例子的不同点是,派生类包含了一个构造函数,它必须调用先(),它会执行基类的构造函数。 而且,在构造函数里访问的属性之前,我们一定要调用先()。 这个是君土脚本强制执行的一条重要规则。

这个例子演示了如何在子类里可以重写父类的方法。 类和类都创建了移动方法,它们重写了从动物继承来的移动方法,使得移动方法根据不同的类而具有不同的功能。 注意,即使赤兔被声明为动物类型,但因为它的值是,调用赤兔.移动(34)时,它会调用里重写的方法:

爬行...
螣蛇移动了5米。
奔跑...
赤兔移动了34米。

公共, 私有与受保护的修饰符

默认为显

在上面的例子里,我们可以自由的访问程序里定义的成员。 如果你对其它语言中的类比较了解,就会注意到我们在之前的代码里并没有使用来做修饰;例如,爪哇(Java)要求必须明确地使用指定成员是可见的。 在君土脚本里,成员都默认为, 可见的。

你也可以明确的将一个成员标记成。 我们可以用下面的方式来重写上面的动物类:

  类 动物 {
    显 名: 文;
    显 构(名0: 文) { 此.名 = 名0; }
    显 移动(米制距离: 数 = 0) {
      控制台.日志(`${此.名}移动了${米制距离}米。`);
    }
  }

理解隐

当成员被标记成时,它就不能在声明它的类的外部访问。比如:

  类 动物 {
    隐 名: 文;
    显 构(名0: 文) { 此.名 = 名0; }
  }
  启 动物("猫").名; // 错误: '启' 是私有的.

君土脚本使用的是结构性类型系统。 当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。

然而,当我们比较带有成员的类型的时候,情况就不同了。 如果其中一个类型里包含一个成员,那么只有当另外一个类型中也存在这样一个成员, 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于成员也使用这个规则。

下面来看一个例子,更好地说明了这一点:

 类 动物 {
    隐 名: 文;
    构(名: 文) { 此.名 = 名; }
  }

  类 犀牛 承 动物 {
    构() { 先("犀牛"); }
  }

  类 植物 {
    隐 名: 文;
    构(名: 文) { 此.名 = 名; }
  }

  定 动物0 = 启 动物("山羊");
  定 犀牛0 = 启 犀牛();
  定 植物0 = 启 植物("梅花");

  动物0 = 犀牛0;
  动物0 = 植物0; // 错误: 动物 与 植物 不兼容.

这个例子中有动物犀牛两个类,犀牛动物类的子类。 还有一个植物类,其类型看上去与动物是相同的。 我们创建了几个这些类的实例,并相互赋值来看看会发生什么。 因为动物犀牛共享了来自动物里的私有成员定义隐 名: 文,因此它们是兼容的。 然而植物却不是这样。当把植物赋值给动物的时候,得到一个错误,说它们的类型不兼容。 尽管植物里也有一个私有成员,但它明显不是动物里面定义的那个。

理解护

修饰符与修饰符的行为很相似,但有一点不同,成员在派生类中仍然可以访问。例如:

  类 人 {
    护 名: 文;
    构(名: 文) { 此.名 = 名; }
  }

  类 员工 承 人 {
    隐 部门: 文;

    构(名: 文, 部门: 文) {
      先(名)
      此.部门 = 部门;
    }

    显 得电梯游说() {
      回 `你好, 我叫${此.名},我在${此.部门}部工作。`;
    }
  }

  定 张三 = 启 员工("张三", "技术开发");
  控制台.日志(张三.得电梯游说());
  控制台.日志(张三.名); // 错误

注意,我们不能在类外使用,但是我们仍然可以通过员工类的实例方法访问,因为员工是由派生而来的。

构造函数也可以被标记成。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。比如,

  类 人 {
    护 名: 文;
    护 构(名: 文) { 此.名 = 名; }
  }

  类 员工 承 人 {
    隐 部门: 文;

    构(名: 文, 部门: 文) {
      先(名)
      此.部门 = 部门;
    }

    显 得电梯游说() {
      回 `你好, 我叫${此.名},我在${此.部门}部工作。`;
    }
  }

  定 张三 = 启 员工("张三", "技术开发");
  定 李四 = 启 人("李四"); // 错误: '人' 的构造函数是被保护的.

固修饰符

你可以使用关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。

  类 章鱼 {
    固 名: 文;
    固 脚数: 数 = 8;
    构(名: 文) {
      此.名 = 名;
    }
  }
  定 章鱼0 = 启 章鱼("八条腿");
  章鱼0.名 = "喷墨"; // 错误! 名 是只读的.

参数属性

在上面的例子中,我们不得不定义一个受保护的成员和一个构造函数参数名0类里,并且立刻给名0赋值。 这种情况经常会遇到。参数属性可以方便地让我们在一个地方定义并初始化一个成员。 下面的例子是对之前动物类的修改版,使用了参数属性:

  类 动物 {
    构(隐 名: 文) { }
    移动(米制距离: 数) {
      控制台.日志(`${此.名} 移动了 ${米制距离}米。`);
    }
  }

注意看我们是如何舍弃了名0,仅在构造函数里使用隐 名: 文参数来创建和初始化成员。 我们把声明和赋值合并至一处。

参数属性通过给构造函数参数添加一个访问限定符来声明。 使用限定一个参数属性会声明并初始化一个私有成员;对于来说也是一样。

存取器

君土脚本支持通过 取器/置器 来截取对对象成员的访问。 它能帮助你有效的控制对于对象成员的访问。

下面来看如何把一个简单的类改写成使用取/*get*/置/*set*/。 首先,我们从一个没有使用存取器的例子开始。

  类 员工 {
    全名: 文 = '';
  }

  定 员工0 = 启 员工();
  员工0.全名 = "张三";
  若 (员工0.全名) {
    控制台.日志(员工0.全名);
  }

我们可以随意的设置全名,这是非常方便的,但是这也可能会带来麻烦。

下面这个版本里,我们先检查用户密码是否正确,然后再允许其修改员工信息。 我们把对全名的直接访问改成了可以检查密码的方法。 我们也加了一个方法,让上面的例子仍然可以工作。

  定 密码 = "保密密码";

  类 员工 {
    隐 _全名: 文 = '';

    取 全名(): 文 {
      回 此._全名;
    }

    置 全名(新名: 文) {
      若 (密码 && 密码 == "保密密码") {
        此._全名 = 新名;
      } 别 {
        控制台.日志("错误:没有验证,不能更新名字。");
      }
    }
  }

  定 员工0 = 启 员工();
  员工0.全名 = "张三";
  若 (员工0.全名) {
    控制台.日志(员工0.全名);
  }

我们可以修改一下密码,来验证一下存取器是否是工作的。当密码不对时,会提示我们没有权限去修改员工。

对于存取器有下面几点需要注意的:

首先,存取器要求你将编译器设置为输出欧算脚本5(ECMAScript 5)或更高。 不支持降级到欧算脚本3(ECMAScript 3)。 其次,只带有不带有的存取器自动被推断为。 这在从代码生成.d.ts文件时是有帮助的,因为利用这个属性的用户会看到不允许够改变它的值。

静态属性

到目前为止,我们只讨论了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。 我们也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。 在这个例子里,我们使用定义原点,因为它是所有网格都会用到的属性。 每个实例想要访问这个属性的时候,都要在原点前面加上类名。 如同在实例属性上使用此.前缀来访问属性一样,这里我们使用网格.来访问静态属性。

  类 网格 {
    静 原点 = { 横: 0, 竖: 0 };
    计算与原点距离(点: { 横: 数; 竖: 数; }) {
      定 横距 = (点.横 - 网格.原点.横);
      定 竖距 = (点.竖 - 网格.原点.竖);
      回 算.方根(横距 * 横距 + 竖距 * 竖距) / 此.比例;
    }
    构(显 比例: 数) { }
  }

  定 网格1 = 启 网格(1.0);  // 1倍 比例
  定 网格2 = 启 网格(5.0);  // 5倍 比例

  控制台.日志(网格1.计算与原点距离({ 横: 10, 竖: 10 }));
  控制台.日志(网格2.计算与原点距离({ 横: 10, 竖: 10 }));

抽象类

抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。 虚/*abstract*/关键字是用于定义抽象类和在抽象类内部定义抽象方法。

  虚 类 动物 {
    虚 发声(): 无;
    移动(): 无 {
      控制台.日志('漫游的我们...');
    }
  }

抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。 抽象方法的语法与接口方法相似。 两者都是定义方法签名但不包含方法体。 然而,抽象方法必须包含关键字并且可以包含访问修饰符。

  虚 类 部门 {

    构(显 名: 文) {
    }

    打印名称(): 无 {
      控制台.日志('部门名称:' + 此.名);
    }

    虚 打印会议(): 无; // 必须在派生类中实现
  }

  类 会计部门 承 部门 {

    构() {
      先('会计与审计'); // 在派生类的构造函数中必须调用 先()
    }

    打印会议(): 无 {
      控制台.日志('会计部门每周一上午10点开会。');
    }

    生产报告(): 无 {
      控制台.日志('正在生成会计报告...');
    }
  }

  定 部门0: 部门; // 允许创建一个对抽象类型的引用
  部门0 = 启 部门(); // 错误: 不能创建一个抽象类的实例
  部门0 = 启 会计部门(); // 允许对一个抽象子类进行实例化和赋值
  部门0.打印名称();
  部门0.打印会议();
  部门0.生产报告(); // 错误: 方法在声明的抽象类中不存在

把类当做接口使用

如上一节里所讲的,类定义会创建两个东西:类的实例类型和一个构造函数。 因为类可以创建出类型,所以你能够在允许使用接口的地方使用类。

  类 点 {
    横: 数 = 0;
    竖: 数 = 0;
  }

  型 三维点 承 点 {
    深: 数;
  }

  定 三维点0: 三维点 = { 横: 1, 竖: 2, 深: 3 };

总结

这篇文章简单地介绍了一些面向对象原理 —— 这些描述还不够完整, 但它让您知道我们在这里处理什么。此外, 我们介绍如何使用类声明实现 君土脚本中的类, 以及生成对象的方法。