JavaScript 定義類的最佳寫法——完整支持面向對象(封裝、繼承、多態),相容所有瀏覽器,支持用JSDuck生成文檔

来源:http://www.cnblogs.com/zyl910/archive/2017/12/24/js_class_bestpractice.html
-Advertisement-
Play Games

作者: "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點不足——

  1. 隨著以後對該類的改進,可能會增加更多的實例欄位,那可能會導致參數列表的頻繁改動。
  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個方面——

  1. 對外隱藏實現細節,只能用專門約定的界面方法去操作。
  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方法綁定代碼都放到立即執行函數中了。該寫法的優點有——

  1. 實現了封裝性。私有變數(如m_WordHello)只能在這個立即執行函數的內部使用,不會暴露到外部。
  2. 實現了變數共用。使getHello方法能訪問到私有靜態變數m_WordHello。
  3. 同一個類的方法定義都寫在一個大括弧中、使用同一層縮進,可提高代碼的可讀性。且有利於編輯器的代碼摺疊功能。

按照面向對象編程的定義,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個好處——

  1. 可讀性高。若是立即執行函數里再套一個函數聲明,有可能看不太明白代碼運行順序的脈絡,可能不少JavaScript新手會覺得很暈。但像這樣寫成“函數表達式賦值給變數”,可簡單的看成代碼順序運行,只是做了變數綁定、方法綁定操作,很容易理解。
  2. 有了函數變數後,便於以後做一些用到函數變數的工作。例如可考慮將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"] || "";
};

對上面代碼的解釋——

  1. 對 cfg 變數進行規範化。
  2. 使用call調用父類的構造函數(PersonInfo),這樣它便會給this對象 增加父類的實例欄位(name、gender)。
  3. 父類(PersonInfo)構造函數調用完成後,便可添加自己(Employee)的實例變數(email)了。

這裡便可看出“拷貝構造函數”寫法的優點——

  1. 只有一個 cfg 參數,故可以很簡單的通過 call 調父類。就算使用多層繼承,也一樣簡單,各個類只轉發它父類的構造函數就行。
  2. 能很方便的將 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) );

便可看到——



您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 在框架規劃時,就有提到過這個框架的一些常用功能需要支持 環境下的調用,也就是需要實現API的多平臺支撐 為什麼要多平臺支撐?核心仍然是復用代碼,比如在微信下,在釘釘下,在quick容器下, 如果沒有多平臺支撐,那麼 只能用於quick容器下,釘釘和微信下就得分別用其它代碼實現, 代碼復用率低, ...
  • 前言 一切就緒,開始規劃API,這裡在規劃前對API進行了一次分類:__短期API、長期API__ 首先申明下,這個是在實際框架演變過程中自創的一個概念,其它混合框架可能也會有這個概念,但應該是會在原生底層來實現,而不是前端實現。。 而這裡由於是 ,所以相比其它混合框架,前端多了一個處理引擎(包括多 ...
  • 1、 添加自定義屬性 page 2、 為 ztree 每個樹形節點,添加點擊事件 ...
  • 在每個節點添加 id 和 pid, id 表示當前節點編號,pid 表示父節點編號 第一步:在頁面顯示菜單位置,添加 ul設置 class=”ztree” 第二步:開啟簡單數據格式支持 第三步:編寫樹形菜單數據 第四步:生成樹形菜單 ...
  • 匿名函數:創建函數時,不指定函數名的函數。此種函數只能運行一次,可以避免變數全局污染,保護變數。 匿名函數可以回調使用,比如 btn.addEventListener("click",function(){...}) 也可以用匿名函數聲明函數名,比如var abc=function (){conso ...
  • 1、 對選項卡面板區域 div 設置 class=”easyui-tabs” 2、 對選項卡面板區域添加多個 div,每個 div 就是一個選項卡(每個面板一定設置 title) 3、 設置面板 fit 為 true ,自適應父容器大小 4、 設置選項卡 closable 為 true ,添加可關閉 ...
  • 1、對摺疊面板區域 div 設置 class=”easyui-accordion” 2、在區域添加多個 div, 每個 div 就是一個面板 (每個面板一定要設置 title 屬性)。 3、設置面板屬性 fit 為 true,自適應父容器大小 ...
  • 一、前言 前段時間寫博客分享和介紹了阿裡雲的UI框架NG-ZORRO(博客請查看:http://www.cnblogs.com/donaldtdz/p/7892960.html),結合近段時間對.Net開源框架ABP的學習。完成將ABP前端框架替換成阿裡雲的NG-ZORRO。 二、替換說明 ABP版 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...