作者: zyl910
[TOC]
由于在 ES6 之前,JavaScript 中没有定义类(class)语法。导致大家用各种五花八门的办法来定义类,代码风格不统一。而且对于模拟面向对象的三大支柱 "封装"、"继承"、"多态",更是有许多专门的深度研究,实现办法更加复杂,不利于 JavaScript 新手使用。
于是我将这些优秀方法提炼总结,化繁为简。目标是——就算是 JavaScript 新手,只要有一点的面向对象编程经验(即有 Java、C# 等面向对象编程经验),也能按照本文的办法,轻松的在 JavaScript 中定义类,完整的使用封装、继承、多态等特性来组织代码。
其次,该方案还有这些优点——
(在 ES6 以前)JavaScript 推荐使用构造函数法来定义类。具体来说,就是写一个构造函数(constructor),然后用 new 关键字对该类来构造对象实例。
例如现在需要定义一个 PersonInfo(个人信息)类,它有 name(姓名)、gender(性别)字段。便这样定义类——
- function PersonInfo() {
- // 自身的实例字段.
- this.name = ""; // 姓名.
- this.gender = 0; // 性别. 0未知, 1男, 2女.
- }
随后便可用 new 关键字来创建对象,可以使用 "对象. 属性" 的语法来访问那些在构造函数中定义的实例字段了。例如——
- var p = new PersonInfo();
- p.name = "Zhang San"; // 张三.
- alert(p.name);
定义类之后,便可为它编写方法了。具体办法是在构造函数的原型中增加函数。例如我们给 PersonInfo 类增加一个 getHello 方法 取得欢迎文本。
- function PersonInfo() {
- this.name = ""; // 姓名.
- this.gender = 0; // 性别. 0未知, 1男, 2女.
- }
- PersonInfo.prototype.getHello = function() {
- var rt = "Hello " + this.name;
- return rt;
- };
随后便可以使用 "对象. 方法 (参数...)" 的语法来调用方法了。
- var p = new PersonInfo();
- p.name = "Zhang San"; // 张三.
- alert(p.getHello());
目前是用 alert 弹出对话框来显示处理结果的。该办法对于以后测试不利,会导致需要连续点多次确定等麻烦。
故还是写一个 addlog(追加日志)的函数比较好,在 textarea 中显示测试结果。
网页中增加 textarea 控件——
- 输出:<br/>
- <textarea id="txtresult" rows="12" style="width:95%" readonly ></textarea>
然后我们的测试函数可改成这样——
- function doTest() {
- var txtresult = document.getElementById("txtresult");
- txtresult.value = "";
- // do
- var p = new PersonInfo();
- p.name = "Zhang San"; // 张三.
- addlog(p.getHello());
- }
以上便是普通 JavaScript 教程中所讲的定义类的办法。该办法能组织实例字段,能编写方法,能都满足很多简单的需求了。
但该办法的缺点也很明显——
下列章节将解决这些问题。
为了避免全局名称污染,可使用命名空间(namespace)机制。
虽然 JavaScript 没有命名空间的语法,但可以通过一些办法模拟。详见 JavaScript 实现命名空间(namespace)的最佳方案 [] 。
其机制很简单,就是定义一个 Object 变量作为命名空间。然后便可将 类的构造函数 绑定到该命名空间中,随后便可按原来的办法给类再绑定方法。
例如将 PersonInfo 类放到 jsnamespace 命名空间中,可这样做——
- var jsnamespace = window.jsnamespace || {};
- jsnamespace.PersonInfo = function() {
- this.name = ""; // 姓名.
- this.gender = 0; // 性别. 0未知, 1男, 2女.
- }
- jsnamespace.PersonInfo.prototype.getHello = function() {
- var rt = "Hello " + this.name;
- return rt;
- };
随后便可使用该类了,注意需要写全命名空间。
- var p = new jsnamespace.PersonInfo();
- p.name = "Zhang San"; // 张三.
- addlog(p.getHello());
一般来说,可以通过构造函数参数的办法,来简化对象的创建、赋值。
- jsnamespace.PersonInfo = function(name, gender) {
- this.name = name; // 姓名.
- this.gender = gender; // 性别. 0未知, 1男, 2女.
- }
可这样使用——
- var p = new jsnamespace.PersonInfo("Zhang San", 1); // 张三, 男.
- addlog(p.getHello());
该做法有 2 点不足——
为了解决上述的 2 点不足,且为了方便对象复制。故推荐使用 "拷贝构造函数" 这种构造函数写法。
具体做法是,构造函数只用一个 Object 型的参数。
- jsnamespace.PersonInfo = function(cfg) {
- cfg = cfg || {}; // 当没传cfg参数时,将它当作空对象。
- this.name = cfg["name"] || ""; // 姓名.
- this.gender = cfg["gender"] || 0; // 性别. 0未知, 1男, 2女.
- }
可这样使用——
- var p = new jsnamespace.PersonInfo({"name": "Zhang San"}); // 张三, 男.
- addlog(p.getHello());
注意上述例子中没传 gender 参数。因构造函数中的
语句,故 gender 属性会赋值为默认值 0。
- this.gender = cfg["gender"] || 0
另外,拷贝构造函数更适合于在继承的场合下使用,详见后面的章节。
对于大型代码来说,即使写了注释,阅读代码也是非常费神的。
这时可编写文档注释,然后用工具将其生成为参考文档。有组织的文档,比代码更易读。且有了文档注释后,代码也更易读懂了。
且文档注释的一些标记能进一步加强代码的可读性。例如(ES6 之前的)JavaScript 没有 class 关键字,用构造函数法定义类与普通函数差异不大,分辨、搜索起来有一些麻烦。而文档注释一般提供了 @class 关键字来表示类。
对于 JavaScript 来说,个人觉得最好用的文档注释工具是 JSDuck。
将上面的代码加上 JSDuck 风格的文档注释,则变成在这样——
- /** @class
- * JavaScript的命名空间.
- * @abstract
- */
- var jsnamespace = window.jsnamespace || {};
- /** @class
- * 个人信息. 构造函数法的类.
- */
- jsnamespace.PersonInfo = function(cfg) {
- cfg = cfg || {};
- /** @property {String} 姓名. */
- this.name = cfg["name"] || "";
- /** @property {Number} 性别. 0未知, 1男, 2女. */
- this.gender = cfg["gender"] || 0;
- };
- /**
- * 取得欢迎字符串.
- *
- * @return {String} 返回欢迎字符串.
- */
- jsnamespace.PersonInfo.prototype.getHello = function() {
- var rt = "Hello " + this.name;
- return rt;
- };
JSDuck 文档注释标记说明——
若想知道 JSDuck 的文档注释的写法的更多说明,可参考其官网 wiki ( https://github.com/senchalabs/jsduck/wiki ),或是查看网上教程 (详见 "参考文献")。
对于其生成的文档,详见 "8.2 用 JSDuck 生成文档"。
之前对于性别,是直接用数值代码来表示。数值代码的可读性差,且不易维护,很多编程语言有 "定义枚举" 语法来解决该问题。
虽然 JavaScript 没有 "定义枚举" 语法,但是可以通过一些办法来模拟。例如可以定义一个 Object 变量,其中的字段就是各种枚举值。因(ES6 之前的)JavaScript 没有常量关键字(const),为了区分只读的枚举值与普通字段,建议使用大写字母来命名枚举值。
并且 JSDuck 有定义枚举的标注—— @enum。
现在便可在 jsnamespace 命名空间中 定义一个名为 GenderCode 的枚举了——
- /** @enum
- * 性别代码. 枚举类.
- */
- jsnamespace.GenderCode = {
- /** 未知 */
- "UNKNOWN": 0,
- /** 男 */
- "MALE": 1,
- /** 女 */
- "FEMALE": 2
- };
随后我们可以改进构造函数,使用枚举值。
- jsnamespace.PersonInfo = function(cfg) {
- cfg = cfg || {};
- /** @property {String} 姓名. */
- this.name = cfg["name"] || "";
- /** @property {jsnamespace.GenderCode} 性别. */
- this.gender = cfg["gender"] || jsnamespace.GenderCode.UNKNOWN;
- };
使用了枚举值之后,代码可读性、可维护性增加了很多。且 JSDuck 文档能将 gender 的类型作为链接,方便查看。
随后在使用时,也应该坚持用枚举值——
- var p = new jsnamespace.PersonInfo({"name": "Zhang San", "gender": jsnamespace.GenderCode.MALE}); // 张三, 男.
- addlog(p.getHello());
有了性别代码枚举后,便可考虑将称谓文本加到欢迎字符串中,使欢迎文本更有意义。
具体办法是可以写一个 getAppellation 方法计算称谓,然后在 getHello 中调用该方法拼接欢迎文本。
- /**
- * 取得称谓.
- *
- * @return {String} 返回称谓字符串.
- */
- jsnamespace.PersonInfo.prototype.getAppellation = function() {
- var rt = "";
- if (jsnamespace.GenderCode.MALE == this.gender) {
- rt = "Mr.";
- } else if (jsnamespace.GenderCode.FEMALE == this.gender) {
- rt = "Ms.";
- }
- return rt;
- };
- /**
- * 取得欢迎字符串.
- *
- * @return {String} 返回欢迎字符串.
- */
- jsnamespace.PersonInfo.prototype.getHello = function() {
- var rt = "Hello " + this.getAppellation() + " " + this.name;
- return rt;
- };
随后改进一下测试代码——
- var p1 = new jsnamespace.PersonInfo();
- p1.name = "Zhang San"; // 张三.
- p1.gender = jsnamespace.GenderCode.MALE;
- var p2 = new jsnamespace.PersonInfo({"name": "Li Si", "gender": jsnamespace.GenderCode.FEMALE}); // 李四.
- addlog(p1.getHello());
- addlog(p2.getHello());
便可看到——
- Hello Mr. Zhang San
- Hello Ms. Li Si
封装(encapsulation):将程序按照一定的逻辑分成多个互相协作的部分,并对外界提供稳定的部分(暴露稳定部分),而将改变部分隐藏起来,外界只能通过暴露的部分向这个对象发送操作请求从而享受对象提供的服务,而不必管对象内部是如何运行的。
封装性体现在 2 个方面——
在 JavaScript 中,可以使用立即执行函数(Immediately-Invoked Function Expression, IIFE)来隐藏私有变量。该办法也很适合用在对象的封装性上。
例如若想将之前 getHello 中的 "Hello" 放到一个内部的私有变量中(欢迎单词 m_WordHello),可以这样写——
- /** @class
- * JavaScript的命名空间.
- * @abstract
- */
- var jsnamespace = window.jsnamespace || {};
- /** @class
- * 个人信息. 构造函数法的类.
- */
- jsnamespace.PersonInfo = function(cfg) {
- cfg = cfg || {};
- /** @property {String} 姓名. */
- this.name = cfg["name"] || "";
- /** @property {Number} 性别. 0未知, 1男, 2女. */
- this.gender = cfg["gender"] || 0;
- }; (function() {
- /**
- * 欢迎单词.
- * @static @private
- */
- var m_WordHello = "Hello";
- /**
- * 取得称谓.
- *
- * @return {String} 返回称谓字符串.
- */
- jsnamespace.PersonInfo.prototype.getAppellation = function() {
- var rt = "";
- if (jsnamespace.GenderCode.MALE == this.gender) {
- rt = "Mr.";
- } else if (jsnamespace.GenderCode.FEMALE == this.gender) {
- rt = "Ms.";
- }
- return rt;
- };
- /**
- * 取得欢迎字符串.
- *
- * @return {String} 返回欢迎字符串.
- */
- jsnamespace.PersonInfo.prototype.getHello = function() {
- var rt = m_WordHello + " " + this.getAppellation() + " " + this.name;
- return rt;
- };
- })();
即将私有变量与 prototype 方法绑定代码都放到立即执行函数中了。该写法的优点有——
按照面向对象编程的定义,m_WordHello 实际上是一个静态私有变量。故在它的文档注释中加上 "@static @private" 标记。
对于私有成员命名,建议使用 "m_" 前缀。这样能与公开成员区分开,提高代码的可读性。
JSDuck 文档注释标记说明——
对于 JSDuck 生成的文档,注意它默认是不显示私有级别的。可点击 "Show",在下拉菜单中勾选 "Private",便可显示私有成员。
有些时候我们重构代码时,会将一些责任移到私有静态函数,使主要逻辑更短更易读。另外还可将各方法之间的重复代码移到私有静态函数中,避免重复。
例如可重构 getAppellation ,将计算称谓文本的责任,移到一个 m_getAppellationText 函数中。
- (function() {
- /**
- * 取得称谓文本.
- *
- * @param {jsnamespace.GenderCode} gender 性别.
- * @return {String} 返回称谓字符串.
- * @static @private
- */
- var m_getAppellationText = function(gender) {
- var rt = "";
- if (jsnamespace.GenderCode.MALE == gender) {
- rt = "Mr.";
- } else if (jsnamespace.GenderCode.FEMALE == gender) {
- rt = "Ms.";
- }
- return rt;
- };
- /**
- * 取得称谓.
- *
- * @return {String} 返回称谓字符串.
- */
- jsnamespace.PersonInfo.prototype.getAppellation = function() {
- var rt = m_getAppellationText(this.gender);
- return rt;
- };
- })();
JSDuck 文档注释标记说明——
将代码改成这样后,原先的测试代码依然能正常工作。
注意 m_getAppellationText 是将一个函数表达式赋值给它,而没有使用函数声明。这样做有 3 个好处——
静态成员是属于整个类的而不是某个对象实例的。故有些时候,是需要将静态成员公开给外部使用的。
对于大多数的面向对象编程语言,可使用 "类. 成员" 的语法,来使用静态成员。故我们也应该兼容该语法。
对于 JavaScript 来说,类的构造函数也是一个 Function,Function 也是一种 Object,并且 Object 可随时在它上面增加字段或函数。即,在构造函数上增加字段或函数,就是给类绑定公开的静态属性、静态方法。
例如对于上面的 m_WordHello,可提供一套 get/set 方法(getWordHello、setWordHello),使外部能够读写该值。
- (function() {
- /**
- * 欢迎单词.
- * @static @private
- */
- var m_WordHello = "Hello";
- // -- static method --
- /** 取得欢迎单词.
- *
- * @return {String} 返回欢迎单词.
- * @static
- */
- jsnamespace.PersonInfo.getWordHello = function() {
- return m_WordHello;
- };
- /** 设置欢迎单词.
- *
- * @param {String} v 欢迎单词.
- * @static
- */
- jsnamespace.PersonInfo.setWordHello = function(v) {
- m_WordHello = v;
- };
- })();
随后改进一下测试代码,将欢迎单词换为 Welcome——
- var p1 = new jsnamespace.PersonInfo();
- p1.name = "Zhang San"; // 张三.
- p1.gender = jsnamespace.GenderCode.MALE;
- var p2 = new jsnamespace.PersonInfo({"name": "Li Si", "gender": jsnamespace.GenderCode.FEMALE}); // 李四.
- addlog(p1.getHello());
- addlog(p2.getHello());
- jsnamespace.PersonInfo.setWordHello("Welcome");
- addlog(p1.getHello());
- addlog(p2.getHello());
便可看到——
- Hello Mr. Zhang San
- Hello Ms. Li Si
- Welcome Mr. Zhang San
- Welcome Ms. Li Si
虽然可通过 "给构造函数这个对象增加字段" 的办法来模拟静态成员属性,但是在一般情况并不推荐这样做。因为 JavaScript 中没有对属性进行读写控制的语法,故一般情况下建议参考上一节的办法,做一对 get/set 方法。
除非是无需读写控制的字段,才可考虑 "直接给构造函数增加字段" 的办法。
JavaScript 中无法实现实例字段、对象方法(绑定到 prototype 的函数)的 private 封装。
有一种变通策略,就是给这些私有实例字段、对象方法加上 "m_" 前缀,提醒它们是私有的,外部不要访问。
由于这些实例字段、对象方法在业务上不应访问,但语法上能够访问(且很多时候,子类需要访问它们,后面的章节会详述)。故我建议给它们的 JSDuck 文档注释中加上 @protected 标记。这样还有助于在 JSDuck 生成的文档中用 "Show" 筛选可见性。
继承(inherit)也称为派生(extend),在 UML 里称为泛化(generalization)。继承关系中,被继承的称为父类(或基类),从父类继承而得的被称为子类(或派生类)。继承是保持对象差异性的同时共享对象相似性的复用。能够被继承的类总是含有并只含有它所抽象的那一类事务的共同特点。继承提供了实现复用,只要从一个类继承,我们就拥有了这个类的所有行为。语义上的 "继承" 表示 "是一种(is-a)" 的关系。
在 JavaScript 中,可以使用 call 或 apply 方法实现 "用指定对象来调用某个方法" 的办法。call、apply 对构造函数也是有效的,故可以用他们来实现构造函数转发功能,即在子类的构造函数中去调父类的构造函数,使其构造好父类的实例字段。
例如需要新建一个 Employee(雇员信息)类,它继承自 PersonInfo(个人信息)类,它多了个 email 参数。便可这样定义该类(的构造函数)——
- jsnamespace.Employee = function(cfg) {
- cfg = cfg || {};
- jsnamespace.PersonInfo.call(this, (PersonInfo));
- // 自身的实例字段.
- /** @property {String} 电子邮箱. */
- this.email = cfg["email"] || "";
- };
对上面代码的解释——
这里便可看出 "拷贝构造函数" 写法的优点——
测试代码——
- var p1 = new jsnamespace.PersonInfo();
- p1.name = "Zhang San"; // 张三.
- p1.gender = jsnamespace.GenderCode.MALE;
- var p2 = new jsnamespace.Employee({"name": "Li Si", "gender": jsnamespace.GenderCode.FEMALE, "email": "lisi@mail.com"}); // 李四.
表明现在已成功的继承了实例字段。
刚才仅是继承了实例字段,还缺方法的继承。这时得使用 JavaScript 的原型链机制。
由于 JavaScript 原型链机制不太容易理解,这里直接给出了封装好的函数,重点讲解怎么使用。若对原理感兴趣,可看 "参考文献" 中的文章。
- /** 继承. 即设置好 Child 的原型为 Parent的原型实例,并设置 uber 属性.
- *
- * @param {Function} Child 子类(构造函数).
- * @param {Function} Parent 父类(构造函数).
- * @static
- */
- jsnamespace.extend = function(Child, Parent) {
- var F = function() {};
- F.prototype = Parent.prototype;
- Child.prototype = new F();
- Child.prototype.constructor = Child;
- Child.uber = Parent.prototype;
- };
因为我们已经使用了命名空间机制,故可将该函数放到 jsnamespace 命名空间中。
有了 extend 函数后,便可以用它来给子类继承方法了。
例如让 Employee 继承父类 PersonInfo 的方法,便只写这一行语句就行了——
- jsnamespace
- .
- extend
- (
- jsnamespace
- .
- Employee
- ,
- jsnamespace
- .
- PersonInfo
- )
- ;
测试代码——
- var p1 = new jsnamespace.PersonInfo();
- p1.name = "Zhang San"; // 张三.
- p1.gender = jsnamespace.GenderCode.MALE;
- var p2 = new jsnamespace.Employee({"name": "Li Si", "gender": jsnamespace.GenderCode.FEMALE, "email": "lisi@mail.com"}); // 李四.
- addlog(p1.getHello());
- addlog(p2.getHello());
便可看到——
- Hello Mr. Zhang San
- Hello Ms. Li Si
做好刚才的 2 步后(构造函数转发、使用 extend),虽然 JavaScript 中已经能完整的使用继承功能了。但对于 JSDuck 文档注释来说, 还需要手工加上 @extends 标记 ,使 JSDuck 了解它们的继承关系。
语法很简单,"@extends 父类的类名(构造函数名)",放在类(@class)的文档注释就行。
代码如下——
- /** @class
- * 雇员信息. 构造函数法的类.
- *
- * @extends jsnamespace.PersonInfo
- */
- jsnamespace.Employee = function(cfg) {
- cfg = cfg || {};
- jsnamespace.PersonInfo.call(this, cfg);
- // 自身的实例字段.
- /** @property {String} 电子邮箱. */
- this.email = cfg["email"] || "";
- };
- jsnamespace.extend(jsnamespace.Employee, jsnamespace.PersonInfo);
现在来做一个综合练习吧,测试一下多层继承。具体来说,即新增一个 Staff(职员信息)类,让它继承自 Employee(雇员信息)类,形成 "Staff->Employee->PersonInfo" 的继承关系。
Staff(职员信息)类还多了一个 duty(职务称号)属性。
代码如下——
- /** @class
- * 职员信息. 构造函数法的类.
- *
- * @extends jsnamespace.Employee
- */
- jsnamespace.Staff = function(cfg) {
- cfg = cfg || {};
- jsnamespace.Employee.call(this, cfg);
- // 自身的实例字段.
- /** @property {String} 职务称号. */
- this.duty = cfg["duty"] || "";
- };
- jsnamespace.extend(jsnamespace.Staff, jsnamespace.Employee);
测试代码——
- var p1 = new jsnamespace.PersonInfo();
- p1.name = "Zhang San"; // 张三.
- p1.gender = jsnamespace.GenderCode.MALE;
- var p2 = new jsnamespace.Employee({"name": "Li Si", "gender": jsnamespace.GenderCode.FEMALE, "email": "lisi@mail.com"}); // 李四.
- var p3 = new jsnamespace.Staff({"name": "Wang Wu", "gender": jsnamespace.GenderCode.MALE, "email": "wangwu@mail.com", "duty": "主任"}); // 王五.
- addlog(p1.getHello());
- addlog(p2.getHello());
- addlog(p3.getHello());
便可看到——
- Hello Mr. Zhang San
- Hello Ms. Li Si
- Hello Mr. Wang Wu
JavaScript 有个 instanceof 运算符,可用来判断对象的类型。本文的介绍的继承方案,是支持的 instanceof 运算符。包括在使用多层继承时。
测试代码——
- var p1 = new jsnamespace.PersonInfo();
- p1.name = "Zhang San"; // 张三.
- p1.gender = jsnamespace.GenderCode.MALE;
- var p2 = new jsnamespace.Employee({"name": "Li Si", "gender": jsnamespace.GenderCode.FEMALE, "email": "lisi@mail.com"}); // 李四.
- var p3 = new jsnamespace.Staff({"name": "Wang Wu", "gender": jsnamespace.GenderCode.MALE, "email": "wangwu@mail.com", "duty": "主任"}); // 王五.
- addlog(p1.getHello());
- addlog(p2.getHello());
- addlog(p3.getHello());
- // instanceof.
- addlog("// instanceof");
- addlog("p1 instanceof jsnamespace.PersonInfo: " + (p1 instanceof jsnamespace.PersonInfo) );
- addlog("p1 instanceof jsnamespace.Employee: " + (p1 instanceof jsnamespace.Employee) );
- addlog("p1 instanceof jsnamespace.Staff: " + (p1 instanceof jsnamespace.Staff) );
- addlog("p2 instanceof jsnamespace.PersonInfo: " + (p2 instanceof jsnamespace.PersonInfo) );
- addlog("p2 instanceof jsnamespace.Employee: " + (p2 instanceof jsnamespace.Employee) );
- addlog("p2 instanceof jsnamespace.Staff: " + (p2 instanceof jsnamespace.Staff) );
- addlog("p3 instanceof jsnamespace.PersonInfo: " + (p3 instanceof jsnamespace.PersonInfo) );
- addlog("p3 instanceof jsnamespace.Employee: " + (p3 instanceof jsnamespace.Employee) );
- addlog("p3 instanceof jsnamespace.Staff: " + (p3 instanceof jsnamespace.Staff) );
便可看到——
- Hello Mr. Zhang San
- Hello Ms. Li Si
- Hello Mr. Wang Wu
- // instanceof
- p1 instanceof jsnamespace.PersonInfo: true
- p1 instanceof jsnamespace.Employee: false
- p1 instanceof jsnamespace.Staff: false
- p2 instanceof jsnamespace.PersonInfo: true
- p2 instanceof jsnamespace.Employee: true
- p2 instanceof jsnamespace.Staff: false
- p3 instanceof jsnamespace.PersonInfo: true
- p3 instanceof jsnamespace.Employee: true
- p3 instanceof jsnamespace.Staff: true
在浏览器中按 F12 打开开发者工具,在 JavaScript 代码中下断点,便可在旁边的变量面板中查看对象变量的详情。例如可看到对象变量的继承树(其实 JavaScript 的标准术语叫 "原型链")。
可看到——
多态(polymorphism)是 "允许用户将父对象设置成为一个或更多的它的子对象相等的技术,赋值后,基类对象就可以根据当前赋值给它的派生类对象的特性以不同的方式运作"(Charlie Calvert)。多态扩大了对象的适应性,改变了对象单一继承的关系。多态是行为的抽象,它使得同名方法可以有不同的响应方式,我们可以通过名字调用某一方法而无需知道哪种实现将被执行,甚至无需知道执行这个实现的对象类型。
多态性中的最重要的,是覆写(override)机制,它允许子类修改父类。即在父类中定义方法,然后子类覆写同名方法。这样在调用该名字的方法时,不同的对象运行的是各自子类的逻辑。
先前 PersonInfo、Employee、Staff 的 getHello 方法,均是只返回 name、gender 这 2 个属性的值的。但这个不太符合实际需要,因为 Employee、Staff 其实增加了属性。
例如现在想让 Employee 的 getHello 方法还返回该类新增 email 字段的值。这时便可使用覆写机制了,代码如下——
- (function() {
- jsnamespace.Employee.prototype.getHello = function() {
- var rt = jsnamespace.PersonInfo.prototype.getHello.call(this);
- rt = rt + " (" + this.email + ")";
- return rt;
- };
- })();
注:
测试代码——
- var p1 = new jsnamespace.PersonInfo();
- p1.name = "Zhang San"; // 张三.
- p1.gender = jsnamespace.GenderCode.MALE;
- var p2 = new jsnamespace.Employee({"name": "Li Si", "gender": jsnamespace.GenderCode.FEMALE, "email": "lisi@mail.com"}); // 李四.
- addlog(p1.getHello());
- addlog(p2.getHello());
便可看到——
- Hello Mr. Zhang San
- Hello Ms. Li Si (lisi@mail.com)
子类覆写方法时,一般情况是不用重新写一遍文档注释的。使用 @inheritdoc 命令,可让该方法继承其父类的文档注释。
- (function() {
- /** @inheritdoc */
- jsnamespace.Employee.prototype.getHello = function() {
- var rt = jsnamespace.PersonInfo.prototype.getHello.call(this);
- rt = rt + " (" + this.email + ")";
- return rt;
- };
- })();
虽然 JSDuck 提供了 @override 关键字,但是因为 JSDuck 会自动识别覆写关系,故可省略。
JSDuck 所生成的文档中有这些信息——
多层继承时,也可按同样的办法来覆写。
例如给 Staff 的 getHello 返回信息中加上 duty 的内容。
- (function() {
- /** @inheritdoc */
- jsnamespace.Staff.prototype.getHello = function() {
- var rt = jsnamespace.Employee.prototype.getHello.call(this);
- rt = rt + " [" + this.duty + "]";
- return rt;
- };
- })();
测试代码——
- var p1 = new jsnamespace.PersonInfo();
- p1.name = "Zhang San"; // 张三.
- p1.gender = jsnamespace.GenderCode.MALE;
- var p2 = new jsnamespace.Employee({"name": "Li Si", "gender": jsnamespace.GenderCode.FEMALE, "email": "lisi@mail.com"}); // 李四.
- var p3 = new jsnamespace.Staff({"name": "Wang Wu", "gender": jsnamespace.GenderCode.MALE, "email": "wangwu@mail.com", "duty": "主任"}); // 王五.
- addlog(p1.getHello());
- addlog(p2.getHello());
- addlog(p3.getHello());
便可看到——
- Hello Mr. Zhang San
- Hello Ms. Li Si (lisi@mail.com)
- Hello Mr. Wang Wu (wangwu@mail.com) [主任]
接口(interface)多态也是一种常见的多态机制。但是 JavaScript 语法不支持接口,虽然可以用原型链去模拟,但会使程序变得过于复杂,恐怕会降低可读性、可维护性。且 JSDuck 也不支持接口,不利于维护开发文档。
建议采取以下策略来避免接口——
- /*! @file jsnamespace.js
- * 演示JavaScript中如何模拟命名空间,并支持 构造函数法、闭包法 来构造类. 还演示了 jsduck 文档注释.
- *
- * @author zhouyuelin
- * @version v1.0
- */
- /** @class
- * JavaScript的命名空间.
- * @abstract
- */
- var jsnamespace = window.jsnamespace || {};
- /** 继承. 即设置好 Child 的原型为 Parent的原型实例,并设置 uber 属性.
- *
- * @param {Function} Child 子类(构造函数).
- * @param {Function} Parent 父类(构造函数).
- * @static
- */
- jsnamespace.extend = function(Child, Parent) {
- var F = function() {};
- F.prototype = Parent.prototype;
- Child.prototype = new F();
- Child.prototype.constructor = Child;
- Child.uber = Parent.prototype;
- };
- // == enum ==
- /** @enum
- * 性别代码. 枚举类.
- */
- jsnamespace.GenderCode = {
- /** 未知 */
- "UNKNOWN": 0,
- /** 男 */
- "MALE": 1,
- /** 女 */
- "FEMALE": 2
- };
- // == PersonInfo class ==
- /** @class
- * 个人信息. 构造函数法的类.
- */
- jsnamespace.PersonInfo = function(cfg) {
- cfg = cfg || {};
- /** @property {String} 姓名. */
- this.name = cfg["name"] || "";
- /** @property {jsnamespace.GenderCode} 性别. */
- this.gender = cfg["gender"] || jsnamespace.GenderCode.UNKNOWN;
- }; (function() {
- /**
- * 欢迎单词.
- * @static @private
- */
- var m_WordHello = "Hello";
- /**
- * 取得称谓文本.
- *
- * @param {jsnamespace.GenderCode} gender 性别.
- * @return {String} 返回称谓字符串.
- * @static @private
- */
- var m_getAppellationText = function(gender) {
- var rt = "";
- if (jsnamespace.GenderCode.MALE == gender) {
- rt = "Mr.";
- } else if (jsnamespace.GenderCode.FEMALE == gender) {
- rt = "Ms.";
- }
- return rt;
- };
- /**
- * 取得称谓.
- *
- * @return {String} 返回称谓字符串.
- */
- jsnamespace.PersonInfo.prototype.getAppellation = function() {
- var rt = m_getAppellationText(this.gender);
- return rt;
- };
- /**
- * 取得欢迎字符串.
- *
- * @return {String} 返回欢迎字符串.
- */
- jsnamespace.PersonInfo.prototype.getHello = function() {
- var rt = m_WordHello + " " + this.getAppellation() + " " + this.name;
- return rt;
- };
- // -- static method --
- /** 取得欢迎单词.
- *
- * @return {String} 返回欢迎单词.
- * @static
- */
- jsnamespace.PersonInfo.getWordHello = function() {
- return m_WordHello;
- };
- /** 设置欢迎单词.
- *
- * @param {String} v 欢迎单词.
- * @static
- */
- jsnamespace.PersonInfo.setWordHello = function(v) {
- m_WordHello = v;
- };
- })();
- // == Employee class ==
- /** @class
- * 雇员信息. 构造函数法的类.
- *
- * @extends jsnamespace.PersonInfo
- */
- jsnamespace.Employee = function(cfg) {
- cfg = cfg || {};
- jsnamespace.PersonInfo.call(this, cfg);
- // 自身的实例字段.
- /** @property {String} 电子邮箱. */
- this.email = cfg["email"] || "";
- };
- jsnamespace.extend(jsnamespace.Employee, jsnamespace.PersonInfo); (function() {
- /** @inheritdoc */
- jsnamespace.Employee.prototype.getHello = function() {
- var rt = jsnamespace.PersonInfo.prototype.getHello.call(this);
- rt = rt + " (" + this.email + ")";
- return rt;
- };
- })();
- // == Staff class ==
- /** @class
- * 职员信息. 构造函数法的类.
- *
- * @extends jsnamespace.Employee
- */
- jsnamespace.Staff = function(cfg) {
- cfg = cfg || {};
- jsnamespace.Employee.call(this, cfg);
- // 自身的实例字段.
- /** @property {String} 职务称号. */
- this.duty = cfg["duty"] || "";
- };
- jsnamespace.extend(jsnamespace.Staff, jsnamespace.Employee); (function() {
- /** @inheritdoc */
- jsnamespace.Staff.prototype.getHello = function() {
- var rt = jsnamespace.Employee.prototype.getHello.call(this);
- rt = rt + " [" + this.duty + "]";
- return rt;
- };
- })();
- // == PersonInfoUtil class ==
- /** @class
- * 个人信息工具. 闭包法的类.
- */
- jsnamespace.PersonInfoUtil = function() {
- /**
- * 前缀.
- *
- * @static @private
- */
- var _prefix = "[show] ";
- return {
- /** 显示信息.
- *
- * @param {jsnamespace.PersonInfo} p 个人信息.
- * @static
- */
- show: function(p) {
- var s = _prefix;
- if ( !! p) {
- s += p.getHello();
- }
- alert(s);
- },
- /** 版本号. @readonly */
- version: 0x100
- };
- } ();
- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
- <html xmlns="http://www.w3.org/1999/xhtml">
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
- <title>测试JavaScript 命名空间</title>
- </head>
- <body>
- <script type="text/javascript" src="jsnamespace.js"></script>
- <script type="text/javascript" src="jsnamespace_sub.js"></script>
- <script type="text/javascript">
- /** 追加日志.
- *
- * @param {String} str 日志字符串.
- */
- function addlog(str) {
- var txtresult = document.getElementById("txtresult");
- txtresult.value = txtresult.value + str + "\r\n";
- }
- /** 测试. */
- function doTest() {
- var txtresult = document.getElementById("txtresult");
- txtresult.value = "";
- // do
- //alert(jsnamespace);
- var p1 = new jsnamespace.PersonInfo();
- p1.name = "Zhang San"; // 张三.
- p1.gender = jsnamespace.GenderCode.MALE;
- var p2 = new jsnamespace.Employee({"name": "Li Si", "gender": jsnamespace.GenderCode.FEMALE, "email": "lisi@mail.com"}); // 李四.
- var p3 = new jsnamespace.Staff({"name": "Wang Wu", "gender": jsnamespace.GenderCode.MALE, "email": "wangwu@mail.com", "duty": "主任"}); // 王五.
- addlog(p1.getHello());
- addlog(p2.getHello());
- addlog(p3.getHello());
- // setWordHello.
- addlog("// setWordHello");
- jsnamespace.PersonInfo.setWordHello("Welcome");
- addlog(p1.getHello());
- addlog(p2.getHello());
- addlog(p3.getHello());
- // instanceof.
- addlog("// instanceof");
- addlog("p1 instanceof jsnamespace.PersonInfo: " + (p1 instanceof jsnamespace.PersonInfo) );
- addlog("p1 instanceof jsnamespace.Employee: " + (p1 instanceof jsnamespace.Employee) );
- addlog("p1 instanceof jsnamespace.Staff: " + (p1 instanceof jsnamespace.Staff) );
- addlog("p2 instanceof jsnamespace.PersonInfo: " + (p2 instanceof jsnamespace.PersonInfo) );
- addlog("p2 instanceof jsnamespace.Employee: " + (p2 instanceof jsnamespace.Employee) );
- addlog("p2 instanceof jsnamespace.Staff: " + (p2 instanceof jsnamespace.Staff) );
- addlog("p3 instanceof jsnamespace.PersonInfo: " + (p3 instanceof jsnamespace.PersonInfo) );
- addlog("p3 instanceof jsnamespace.Employee: " + (p3 instanceof jsnamespace.Employee) );
- addlog("p3 instanceof jsnamespace.Staff: " + (p3 instanceof jsnamespace.Staff) );
- // PersonInfoUtil.
- //jsnamespace.PersonInfoUtil.show(p1);
- //jsnamespace.PersonInfoUtil.show(p2);
- //jsnamespace.PersonInfoUtil.show(p3);
- }
- </script>
- <h1>测试JavaScript 命名空间</h1>
- <input type="button" value="测试" OnClick="doTest();" title="doTest" />
- <br/>
- 输出:<br/>
- <textarea id="txtresult" rows="12" style="width:95%" readonly ></textarea>
- </body>
- </html>
可用 jsduck 命令来生成文档。对于本文的范例代码,可使用目录中的 "jsduck_make.bat" 来生成文档,随后可通过 "doc" 子目录中的 "index.html" 查看文档。
以下截图,就是 JSDuck 根据上面的范例代码所生成文档。可发现它完美的识别了代码中的类(class),正确的生成了属性、方法等的文档,还能清晰的查看继承树、方法覆盖(override)。
简单来说,本文所介绍的编写类的写法,是分为 3 段来写的——
源码地址:
https://github.com/zyl910/test_jsduck
(完)
来源: http://www.cnblogs.com/zyl910/p/js_class_bestpractice.html