作者: "zyl910" [TOC] 一、緣由 由於在ES6之前,JavaScript中沒有定義類(class)語法。導致大家用各種五花八門的辦法來定義類,代碼風格不統一。而且對於模擬面向對象的三大支柱“封裝”、“繼承”、“多態”,更是有許多專門的深度研究,實現辦法更加複雜,不利於JavaScrip ...
作者: zyl910
[TOC]
一、緣由
由於在ES6之前,JavaScript中沒有定義類(class)語法。導致大家用各種五花八門的辦法來定義類,代碼風格不統一。而且對於模擬面向對象的三大支柱“封裝”、“繼承”、“多態”,更是有許多專門的深度研究,實現辦法更加複雜,不利於JavaScript新手使用。
於是我將這些優秀方法提煉總結,化繁為簡。目標是——就算是JavaScript新手,只要有一點的面向對象編程經驗(即有Java、C#等面向對象編程經驗),也能按照本文的辦法,輕鬆的在JavaScript中定義類,完整的使用封裝、繼承、多態等特性來組織代碼。
其次,該方案還有這些優點——
- 相容所有的瀏覽器。目前ES6尚未普及,很多瀏覽器尚不支持。而本方法因其採用了簡單的語法(應該是在ES3的範圍內),故相容目前所有的瀏覽器。實測在 IE5~11、Edge、Chrome、Firefox等瀏覽器中均測試通過。
- 相容命名空間方案(JavaScript 實現命名空間(namespace)的最佳方案 ),便於管理大型代碼。
- 使用起來與其他面向對象編程語言非常相似。僅是在定義時的寫法稍有區別。
- 支持用JSDuck生成文檔。且JSDuck能完美的識別本文所介紹的面向對象特性,生成有用的文檔。便於理解,提高可維護性。
- 相容ES6。因ES6的class關鍵字實際上只是一個語法糖,它內部仍是靠prototype(原型)機制。
- 有利於代碼風格統一。
二、定義類的基本寫法
2.1 使用構造函數法來定義類
(在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);
2.2 編寫方法
定義類之後,便可為它編寫方法了。具體辦法是在構造函數的原型中增加函數。例如我們給 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());
2.3 增加addlog函數簡化測試
目前是用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());
}
2.4 小結
以上便是普通JavaScript教程中所講的定義類的辦法。該辦法能組織實例欄位,能編寫方法,能都滿足很多簡單的需求了。
但該辦法的缺點也很明顯——
- 所有內容都定義在全局變數空間。可能會造成全局名稱污染,不利於大型項目。
- 封裝性差。所有成員都暴露在對象實例里或是原型(prototype)里,所有人都能訪問,一不小心可能會弄亂數據。
- 不支持繼承。JavaScript沒有繼承語法,無法直接定義子類。
- 不支持多態。由於無法實現繼承,導致它不支持多態。
下列章節將解決這些問題。
三、基本寫法的改進
3.1 使用命名空間來避免全局名稱污染
為了避免全局名稱污染,可使用命名空間(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());
3.2 改進構造函數
3.2.1 構造函數參數
一般來說,可以通過構造函數參數的辦法,來簡化對象的創建、賦值。
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點不足——
- 隨著以後對該類的改進,可能會增加更多的實例欄位,那可能會導致參數列表的頻繁改動。
- 非常依賴參數順序,萬一傳參時某個參數的順序寫錯,便會引發數據問題。
3.2.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 參數。因構造函數中的 this.gender = cfg["gender"] || 0
語句,故 gender 屬性會賦值為預設值0。
另外,拷貝構造函數更適合於在繼承的場合下使用,詳見後面的章節。
3.3 使用JSDuck文檔註釋來改進代碼的可讀性
對於大型代碼來說,即使寫了註釋,閱讀代碼也是非常費神的。
這時可編寫文檔註釋,然後用工具將其生成為參考文檔。有組織的文檔,比代碼更易讀。且有了文檔註釋後,代碼也更易讀懂了。
且文檔註釋的一些標記能進一步加強代碼的可讀性。例如(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文檔註釋標記說明——
@class
: 表示這是一個類。@abstract
: 該類(或方法)是抽象的。由於JSDuck沒有命名空間的關鍵字,於是習慣上用 @class @abstract 組合表示命名空間。@property
: 屬性。其格式為“@property {類型} 說明”。@cfg
: 構造函數cfg中的參數。其格式為“@cfg {類型} 說明”。一般情況下,cfg參數與公開的屬性(@property
)相同,這時只用@property
就行了,個人覺得不用寫@cfg
了。@return
: 返回值。其格式為“@return {類型} 說明”。
若想知道JSDuck的文檔註釋的寫法的更多說明,可參考其官網wiki ( https://github.com/senchalabs/jsduck/wiki ),或是查看網上教程 (詳見“參考文獻”)。
對於其生成的文檔,詳見“8.2 用JSDuck生成文檔”。
3.4 枚舉
之前對於性別,是直接用數值代碼來表示。數值代碼的可讀性差,且不易維護,很多編程語言有“定義枚舉”語法來解決該問題。
雖然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());
3.4.1 應用:將稱謂文本加到歡迎字元串中
有了性別代碼枚舉後,便可考慮將稱謂文本加到歡迎字元串中,使歡迎文本更有意義。
具體辦法是可以寫一個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個方面——
- 對外隱藏實現細節,只能用專門約定的界面方法去操作。
- 對內能實現變數的共用,用於實現一些複雜邏輯。
4.1 私有靜態變數
在 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)只能在這個立即執行函數的內部使用,不會暴露到外部。
- 實現了變數共用。使getHello方法能訪問到私有靜態變數m_WordHello。
- 同一個類的方法定義都寫在一個大括弧中、使用同一層縮進,可提高代碼的可讀性。且有利於編輯器的代碼摺疊功能。
按照面向對象編程的定義,m_WordHello實際上是一個靜態私有變數。故在它的文檔註釋中加上“@static @private”標記。
對於私有成員命名,建議使用“m_”首碼。這樣能與公開成員區分開,提高代碼的可讀性。
JSDuck文檔註釋標記說明——
@static
: 靜態。@private
: 私有。
對於JSDuck生成的文檔,註意它預設是不顯示私有級別的。可點擊“Show”,在下拉菜單中勾選“Private”,便可顯示私有成員。
4.2 私有靜態函數
有些時候我們重構代碼時,會將一些責任移到私有靜態函數,使主要邏輯更短更易讀。另外還可將各方法之間的重覆代碼移到私有靜態函數中,避免重覆。
例如可重構 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文檔註釋標記說明——
@param
: 參數說明。其格式為“@param {類型} 參數名 說明”。
將代碼改成這樣後,原先的測試代碼依然能正常工作。
註意m_getAppellationText是將一個函數表達式賦值給它,而沒有使用函數聲明。這樣做有3個好處——
- 可讀性高。若是立即執行函數里再套一個函數聲明,有可能看不太明白代碼運行順序的脈絡,可能不少JavaScript新手會覺得很暈。但像這樣寫成“函數表達式賦值給變數”,可簡單的看成代碼順序運行,只是做了變數綁定、方法綁定操作,很容易理解。
- 有了函數變數後,便於以後做一些用到函數變數的工作。例如可考慮將m_getAppellationText函數變數傳給其他地方。
4.3 公開靜態成員
靜態成員是屬於整個類的而不是某個對象實例的。故有些時候,是需要將靜態成員公開給外部使用的。
對於大多數的面向對象編程語言,可使用“類.成員”的語法,來使用靜態成員。故我們也應該相容該語法。
對於JavaScript來說,類的構造函數也是一個 Function,Function也是一種Object,並且Object可隨時在它上面增加欄位或函數。即,在構造函數上增加欄位或函數,就是給類綁定公開的靜態屬性、靜態方法。
4.3.1 公開靜態方法
例如對於上面的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
4.3.2 公開靜態屬性
雖然可通過“給構造函數這個對象增加欄位”的辦法來模擬靜態成員屬性,但是在一般情況並不推薦這樣做。因為JavaScript中沒有對屬性進行讀寫控制的語法,故一般情況下建議參考上一節的辦法,做一對 get/set 方法。
除非是無需讀寫控制的欄位,才可考慮“直接給構造函數增加欄位”的辦法。
4.4 私有實例成員
JavaScript中無法實現實例欄位、對象方法(綁定到prototype的函數)的private封裝。
有一種變通策略,就是給這些私有實例欄位、對象方法加上“m_”首碼,提醒它們是私有的,外部不要訪問。
由於這些實例欄位、對象方法在業務上不應訪問,但語法上能夠訪問(且很多時候,子類需要訪問它們,後面的章節會詳述)。故我建議給它們的JSDuck文檔註釋中加上 @protected 標記。這樣還有助於在JSDuck生成的文檔中用“Show”篩選可見性。
五、繼承
繼承(inherit)也稱為派生(extend),在UML里稱為泛化(generalization)。繼承關係中,被繼承的稱為父類(或基類),從父類繼承而得的被稱為子類(或派生類)。繼承是保持對象差異性的同時共用對象相似性的復用。能夠被繼承的類總是含有並只含有它所抽象的那一類事務的共同特點。繼承提供了實現復用,只要從一個類繼承,我們就擁有了這個類的所有行為。語義上的“繼承”表示“是一種(is-a)”的關係。
5.1 轉發構造函數,繼承實例欄位
在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"] || "";
};
對上面代碼的解釋——
- 對 cfg 變數進行規範化。
- 使用call調用父類的構造函數(PersonInfo),這樣它便會給this對象 增加父類的實例欄位(name、gender)。
- 父類(PersonInfo)構造函數調用完成後,便可添加自己(Employee)的實例變數(email)了。
這裡便可看出“拷貝構造函數”寫法的優點——
- 只有一個 cfg 參數,故可以很簡單的通過 call 調父類。就算使用多層繼承,也一樣簡單,各個類只轉發它父類的構造函數就行。
- 能很方便的將 cfg 參數傳遞給父類,使父類也能用到參數來初始化變數。
測試代碼——
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": "[email protected]"}); // 李四.
表明現在已成功的繼承了實例欄位。
5.2 綁定原型鏈,繼承方法
剛纔僅是繼承了實例欄位,還缺方法的繼承。這時得使用JavaScript的原型鏈機制。
5.2.1 定義extend函數
由於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命名空間中。
5.2.2 使用extend
有了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": "[email protected]"}); // 李四.
addlog(p1.getHello());
addlog(p2.getHello());
便可看到——
Hello Mr. Zhang San
Hello Ms. Li Si
5.3 改進子類的JSDuck文檔註釋,申明繼承關係
做好剛纔的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);
5.4 多層繼承
現在來做一個綜合練習吧,測試一下多層繼承。具體來說,即新增一個 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": "[email protected]"}); // 李四.
var p3 = new jsnamespace.Staff({"name": "Wang Wu", "gender": jsnamespace.GenderCode.MALE, "email": "[email protected]", "duty": "主任"}); // 王五.
addlog(p1.getHello());
addlog(p2.getHello());
addlog(p3.getHello());
便可看到——
Hello Mr. Zhang San
Hello Ms. Li Si
Hello Mr. Wang Wu
5.5 instanceof
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": "[email protected]"}); // 李四.
var p3 = new jsnamespace.Staff({"name": "Wang Wu", "gender": jsnamespace.GenderCode.MALE, "email": "[email protected]", "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) );
便可看到——