︿
Top

2015年3月7日 星期六

Javascript: 如何建立物件 (Part 2)


緣起


接續 前一篇, 這裡再補上截至目前所學習到的一些觀念; 本文主要以範例為主, 一些說明都寫在程式註解.

完整範例, 請由此下載.


資訊隱藏 (封裝) (Encapsulation)


參考 維基百科, 封裝是指:
"(1) 一種將抽象性函式介面的實作細節部份包裝, 隱藏起來的方法."
(2) 它也是一種防止外界呼叫端, 去存取物件內部實作細節的手段. 這個手段是由程式語言本身來提供的.
適當的封裝, 可以將物件使用介面的程式實作部份隱藏起來, 不讓使用者看到, 同時確保使用者無法任意更改物件內部的重要資料."

Javascript 本身並沒有像 C# 一樣, 有 public, protected, private, internal 的存取子; 而是透過 Closure 的機制 ( W3CSchool, Gossip@Openhome) , 去模擬 private 屬性.

範例 1.1: 利用 private varialble 的機制, 外界只能使用 getXXXX, setXXXX method; 但對使用元件的人會很麻煩 


// =================================================
// 資訊隱藏 (封裝)
// =================================================
function testEncapsulation01() {
    /// <summary>利用 private varialble 的機制, 外界只能使用 getXXXX, setXXXX method; 但對使用元件的人會很麻煩 </summary>
    // ==========================
    // Closure 機制: 參數或本地變數, 在執行完後, 按理應該消失; 但卻會保留在 內部函式 裡
    // ==========================
    // ** 下列範例, 外界完全只能用 getXXXX, setXXXX 作處理; 而不能直接存取屬性

    function Person(name, age) {
        var occupation;                                     // private variable (or private property)
        this.getOccupation = function () { return occupation; };
        this.setOccupation = function (newOcc) {
            occupation = newOcc;
        };

        // accessors for name and age
        this.getName = function () { return name; };
        this.setName = function (newName) { name = newName; };
        this.getAge = function () { return age; };
        this.setAge = function (newAge) { age = newAge; };
    }

    var jasper = new Person("Jasper", 46);
    jasper.setOccupation("IT");
    dispLog("name=" + jasper.getName() + " age=" + jasper.getAge() + " occpupation=" + jasper.getOccupation());

    // 執行結果:
    // 09:29.411 name=Jasper age=46 occpupation=IT
}

範例 1.2: 利用 Closure 的機制, 進行屬性封裝 for getters and setters


// =================================================
// 資訊隱藏 (封裝)
// =================================================
function testEncapsulation02() {
    /// <summary>利用 Closure 的機制, 進行屬性封裝 for getters and setters </summary>
    // ==========================
    // Closure: 參數或本地變數, 在執行完後, 按理應該消失; 但卻會保留在 內部函式 裡
    // ==========================
    // ** 下列範例, 外界無法直接存取到 _petName, _petAge (資訊隱藏了...), 必須透過 petName, petAge 屬性作處理 
    //      這樣作的好處是可以在使用者異動屬性值時, 可以預作檢查, 以避免傳入不合法的值

    function Pet(pName, pAge) {

        //如果傳入的 物件實例 (this) , 不是繼承 Pet, 則建立一個新的 ...
        if (!(this instanceof Pet)) {
            return new Pet(pName, pAge);
        }

        //// This is a workaround for an error in the ECMAScript Language Specification
        //// which causes 'this' to be set incorrectly for inner functions.
        // "local variable": to keep the original instance
        var self = this;                            

        // "private properties" ?! 以 "_" 開頭的名稱,作為 private 
        //註: 不能用 var, 不然透過 constructor 直接指定, 會失效, 造成仍然是 undefined 的狀況
        //      --> 原因推測: 因為 var 宣告的 _petName, _petAge 沒有綁定在任何 method, 導致建構子結束, 變數亦跟著消失
        //      --> 相對的, var self 有其它 method 綁定, 所以不會出問題
        this._petName = pName;
        this._petAge = pAge;
        //var _petName = pName;
        //var _petAge = pAge;

        // "privileged method" (看不出與 public method 有何差異 @@)
        // 這裡可以供外部呼叫, 以存取屬性
        // 注意: petName, age 這2個屬性, 在後面另以 prototype 的方式定義
        // 注意: 這樣的寫法, 每一個 instance, 都會有一份該方法 ...
        this.getDetail = function () {     
            return "petName=" + self.petName + " petAge=" + self.petAge;
        }
    }

    //當然, 這樣的寫法, 也是很今人厭煩的, 但筆者目前找不到其它替代方案 @@
    Pet.prototype = {
        //定義 petName 屬性
        get petName() {
            return this._petName;
        },
        set petName(val) {
            this._petName = val;
        },
        //定義 petAge 屬性
        get petAge() {
            return this._petAge;
        },
        set petAge(val) {
            this._petAge = val;
        },
    }
    //Pet.prototype.age = 1;
    //
    var pet1 = new Pet("Tony1", 1);
    dispLog(pet1.getDetail());

    //
    var pet2 = new Pet();
    pet2.petName = "Tony2";
    pet2.petAge = 5;
    dispLog(pet2.getDetail());
    //
    pet2.petName = "Tony2-1";
    pet2.petAge = 6;
    dispLog(pet2.getDetail());

    // 執行結果:
    // 09:29.411 petName=Tony1 petAge=1
    // 09:29.411 petName=Tony2 petAge=5
    // 09:29.411 petName=Tony2-1 petAge=6
}

繼承 (Inheritance)


參考 維基百科 繼承是指:
"在某種情況下, 一個類別會有「子類別」, 子類別比原本的類別 (稱為父類別) 要更加具體化."
"子類別會繼承父類別的屬性和行為, 並且也可包含它們自己的."
例如: 某公司的 Product 類別 (含有 name, price 這2個屬性), 其下可以區分為  Food 及 Tool 這2個類別, 而 Food 之下, 又分為 Wine 及 SportDrink 這2個類別. 至於 taiwan beer 及 pocarri sweet 則可視為其實際案例 (instance)

範例 2.1: 利用換預設 prototype 的機制, 模擬繼承; 但沒有作方法的 override 


// =================================================
// 繼承 
// =================================================
function testInherit01() {
    /// <summary>利用換預設 prototype 的機制, 模擬繼承; 但沒有作方法的 override </summary>
    // ** 下列範例的類別結構 
    //          Object
    //          Product
    //          Food
    //  Wine            SportDrink

    function Product(name, price) {

        // 如果傳入的 物件實例 (this) , 不是繼承 Product, 則建立一個新的 ...
        if (!(this instanceof Product)) {
            return new Product(name, price);
        }

        //public properties
        this.name = name;
        this.price = price;

        if (price < 0) {
            throw RangeError('Cannot create product ' +
                              this.name + ' with a negative price');
        }

        //// 建立一個方法 (這個會造成每個 instance 都有一份)
        //// 執行 iterateObjectProperties(cheese, true); 程式段, 會顯示 getDetail 的內容 (代表屬於各自的 instance)
        //// 執行 iterateObjectProperties(cheese2, true); 程式段 會顯示 getDetail 的內容 (代表屬於各自的 instance)
        //this.getDetail = function () {
        //    return "name=" + this.name + " price=" + this.price;
        //}

        return this;
    }
    // 建立一個方法  (這個會造成所有 instance 共用一份)
    //// 執行 iterateObjectProperties(cheese, true); 程式段, 不會顯示 getDetail 的內容 (代表共用 Product.prototype.getDetail)
    //// 執行 iterateObjectProperties(cheese2, true); 程式段, 不會顯示 getDetail 的內容 (代表共用 Product.prototype.getDetail)
    Product.prototype.getDetail = function () {
        return "name=" + this.name + " price=" + this.price;
    }

    // ------------------------------------------
    function Food(name, price) {
        // 如果傳入的 物件實例 (this) , 不是繼承 Food, 則建立一個新的 ...
        if (!(this instanceof Food)) {
            return new Food(name, price);
        }
        //
        Product.call(this, name, price);
        this.category = 'food';
    }
    // Food.prototype 原本是 Food, 但強制轉換為 Product 
    Food.prototype = Object.create(Product.prototype);
    Food.prototype.constructor = Food;      //把 constructor 改回正確值, 原來的範例沒有這一列?述

    // ------------------------------------------
    function Wine(name, price) {
        // 如果傳入的 物件實例 (this) , 不是繼承 Wine, 則建立一個新的 ...
        if (!(this instanceof Wine)) {
            return new Wine(name, price);
        }
        //
        Food.call(this, name, price);
        this.subCategory = 'wine';
    }
    Wine.prototype = Object.create(Food.prototype);
    Wine.prototype.constructor = Wine;      //把 constructor 改回正確值, 原來的範例沒有這一列?述

    // ------------------------------------------
    function SportDrink(name, price) {
        // 如果傳入的 物件實例 (this) , 不是繼承 SportDrink, 則建立一個新的 ...
        if (!(this instanceof SportDrink)) {
            return new SportDrink(name, price);
        }
        //
        Food.call(this, name, price);
        this.subCategory = 'sport drink';
    }
    SportDrink.prototype = Object.create(Food.prototype);
    SportDrink.prototype.constructor = SportDrink;      //把 constructor 改回正確值, 原來的範例沒有這一列?述

    // ------------------------------------------
    var cheese = new Food('feta', 5);
    var cheese2 = new Food('cheddar ', 6);
    var beer = new Wine('taiwan beer', 30);
    var pocarri = new SportDrink('pocarri sweet', 25);

    //
    dispLog("-------");
    dispLog(cheese.getDetail());
    dispLog(cheese2.getDetail());
    dispLog(beer.getDetail());
    dispLog(pocarri.getDetail());
    dispLog("-------");
    //
    dispLog('Is cheese an instance of Food? ' + (cheese instanceof Food));
    dispLog('Is cheese an instance of Product? ' + (cheese instanceof Product));
    dispLog('Is beer an instance of Wine? ' + (beer instanceof Wine));
    dispLog('Is beer an instance of Food? ' + (beer instanceof Food));
    dispLog('Is beer an instance of Product? ' + (beer instanceof Product));
    //
    dispLog("-------");
    iterateObjectProperties(cheese, true);
    dispLog("-------");
    iterateObjectProperties(cheese2, true);
    dispLog("-------");
    iterateObjectProperties(beer);
    dispLog("-------");
    iterateObjectProperties(pocarri);

    // 執行結果:
    //09:31.231 -------
    //09:31.278 name=feta price=5
    //09:31.286 name=cheddar  price=6
    //09:31.296 name=taiwan beer price=30
    //09:31.306 name=pocarri sweet price=25
    //09:31.317 -------
    //09:31.323 Is cheese an instance of Food? true
    //09:31.330 Is cheese an instance of Product? true
    //09:31.338 Is beer an instance of Wine? true
    //09:31.345 Is beer an instance of Food? true
    //09:31.353 Is beer an instance of Product? true
    //09:31.360 -------
    //09:31.368 obj[name] = feta
    //09:31.376 obj[price] = 5
    //09:31.385 obj[category] = food
    //09:31.392 -------
    //09:31.399 obj[name] = cheddar 
    //09:31.407 obj[price] = 6
    //09:31.415 obj[category] = food
    //09:31.422 -------
    //09:31.430 obj[name] = taiwan beer
    //09:31.437 obj[price] = 30
    //09:31.445 obj[category] = food
    //09:31.453 obj[subCategory] = wine
    //09:31.461 obj[constructor] = function Wine(name, price) {
    //    // 如果傳入的 物件實例 (this) , 不是繼承 Wine, 則建立一個新的 ...
    //    if (!(this instanceof Wine)) {
    //        return new Wine(name, price);
    //    }
    //    //
    //    Food.call(this, name, price);
    //    this.subCategory = 'wine';
    //}
    //09:31.469 obj[getDetail] = function () {
    //    return "name=" + this.name + " price=" + this.price;
    //}
    //09:31.480 -------
    //09:31.491 obj[name] = pocarri sweet
    //09:31.502 obj[price] = 25
    //09:31.513 obj[category] = food
    //09:31.524 obj[subCategory] = sport drink
    //09:31.533 obj[constructor] = function SportDrink(name, price) {
    //    // 如果傳入的 物件實例 (this) , 不是繼承 SportDrink, 則建立一個新的 ...
    //    if (!(this instanceof SportDrink)) {
    //        return new SportDrink(name, price);
    //    }
    //    //
    //    Food.call(this, name, price);
    //    this.subCategory = 'sport drink';
    //}
    //09:31.541 obj[getDetail] = function () {
    //    return "name=" + this.name + " price=" + this.price;
    //}

}

可以參考以下3張取自 Visual Studio 2013 debugging 的截圖, 可以明顯看出繼承的結構.
Food

Wine

SportDrink


多型 (Polymorphism)


參考 維基百科 多型 是指:
經由繼承而產生的相關的不同的類別, 其物件對同一訊息會做出不同的響應.
參考 小朱的 [JavaScript] JavaScript 物件導向設計 (3) : 多型與介面篇 多型 是指:
相同的行為 (behavior), 在不同的物件上會有不同的反應.

接續前例: 在 Product, Food, Wine, SportDrink 都有各自的 getDetail() 方法. 會依實際上由那個類別建立的物件, 而有不同的反應.

範例 3.1: 利用換預設 prototype 的機制, 模擬繼承; 有作方法的 override; 可以呈現多型的效果

本範例與上述範例雷同, 但在 Food, Wine, SportDrink 這 3 個類別, 分別加上 getDetail() method, 覆寫(override) 其祖先提供的 method.

// =================================================
// 繼承 + 多型
// =================================================
function testPolymorphism01() {
    /// <summary>利用換預設 prototype 的機制, 模擬繼承; 有作方法的 override; 可以呈現多型的效果 </summary>
    // ** 下列範例的類別結構 
    //          Object
    //          Product
    //          Food
    //  Wine            SportDrink

    function Product(name, price) {

        // 如果傳入的 物件實例 (this) , 不是繼承 Product, 則建立一個新的 ...
        if (!(this instanceof Product)) {
            return new Product(name, price);
        }

        //public properties
        this.name = name;
        this.price = price;

        if (price < 0) {
            throw RangeError('Cannot create product ' +
                              this.name + ' with a negative price');
        }

        //// 建立一個方法 (這個會造成每個 instance 都有一份)
        //// 執行 iterateObjectProperties(cheese, true); 程式段, 會顯示 getDetail 的內容 (代表屬於各自的 instance)
        //// 執行 iterateObjectProperties(cheese2, true); 程式段 會顯示 getDetail 的內容 (代表屬於各自的 instance)
        //this.getDetail = function () {
        //    return "name=" + this.name + " price=" + this.price;
        //}

        return this;
    }
    // 建立一個方法  (這個會造成所有 instance 共用一份)
    //// 執行 iterateObjectProperties(cheese, true); 程式段, 不會顯示 getDetail 的內容 (代表共用 Product.prototype.getDetail)
    //// 執行 iterateObjectProperties(cheese2, true); 程式段, 不會顯示 getDetail 的內容 (代表共用 Product.prototype.getDetail)
    Product.prototype.getDetail = function () {
        return "name=" + this.name + " price=" + this.price;
    }

    // ------------------------------------------
    function Food(name, price) {
        // 如果傳入的 物件實例 (this) , 不是繼承 Food, 則建立一個新的 ...
        if (!(this instanceof Food)) {
            return new Food(name, price);
        }
        //
        Product.call(this, name, price);
        this.category = 'food';
    }
    // Food.prototype 原本是 Food, 但強制轉換為 Product 
    Food.prototype = Object.create(Product.prototype);
    Food.prototype.constructor = Food;      //把 constructor 改回正確值, 原來的範例沒有這一列?述
    Food.prototype.getDetail = function () {        // 建立一個方法 (override 祖先)
        return "name=" + this.name + " price=" + this.price + " category=" + this.category;
    }

    // ------------------------------------------
    function Wine(name, price) {
        // 如果傳入的 物件實例 (this) , 不是繼承 Wine, 則建立一個新的 ...
        if (!(this instanceof Wine)) {
            return new Wine(name, price);
        }
        //
        Food.call(this, name, price);
        this.subCategory = 'wine';
    }
    Wine.prototype = Object.create(Food.prototype);
    Wine.prototype.constructor = Wine;      //把 constructor 改回正確值, 原來的範例沒有這一列?述
    Wine.prototype.getDetail = function () {        // 建立一個方法 (override 祖先)
        return "name=" + this.name + " price=" + this.price + " category=" + this.category + " subcategory=" + this.subCategory;
    }

    // ------------------------------------------
    function SportDrink(name, price) {
        // 如果傳入的 物件實例 (this) , 不是繼承 SportDrink, 則建立一個新的 ...
        if (!(this instanceof SportDrink)) {
            return new SportDrink(name, price);
        }
        //
        Food.call(this, name, price);
        this.subCategory = 'sport drink';
    }
    SportDrink.prototype = Object.create(Food.prototype);
    SportDrink.prototype.constructor = SportDrink;      //把 constructor 改回正確值, 原來的範例沒有這一列?述
    // 建立一個方法 (override 祖先)
    SportDrink.prototype.getDetail = function () {
        return "name=" + this.name + " price=" + this.price + " category=" + this.category + " subcategory=" + this.subCategory;
    }

    // ------------------------------------------
    var cheese = new Food('feta', 5);
    var cheese2 = new Food('cheddar ', 6);
    var beer = new Wine('taiwan beer', 30);
    var pocarri = new SportDrink('pocarri sweet', 25);

    //
    dispLog("-------");
    dispLog(cheese.getDetail());
    dispLog(cheese2.getDetail());
    dispLog(beer.getDetail());
    dispLog(pocarri.getDetail());
    dispLog("-------");
    //
    dispLog('Is cheese an instance of Food? ' + (cheese instanceof Food));
    dispLog('Is cheese an instance of Product? ' + (cheese instanceof Product));
    dispLog('Is beer an instance of Wine? ' + (beer instanceof Wine));
    dispLog('Is beer an instance of Food? ' + (beer instanceof Food));
    dispLog('Is beer an instance of Product? ' + (beer instanceof Product));
    //
    dispLog("-------");
    iterateObjectProperties(cheese, true);
    dispLog("-------");
    iterateObjectProperties(cheese2, true);
    dispLog("-------");
    iterateObjectProperties(beer);
    dispLog("-------");
    iterateObjectProperties(pocarri);

    // 執行結果:
    //09:31.573 -------
    //09:31.581 name=feta price=5 category=food
    //09:31.589 name=cheddar  price=6 category=food
    //09:31.597 name=taiwan beer price=30 category=food subcategory=wine
    //09:31.605 name=pocarri sweet price=25 category=food subcategory=sport drink
    //09:31.620 -------
    //09:31.628 Is cheese an instance of Food? true
    //09:31.635 Is cheese an instance of Product? true
    //09:31.643 Is beer an instance of Wine? true
    //09:31.651 Is beer an instance of Food? true
    //09:31.659 Is beer an instance of Product? true
    //09:31.667 -------
    //09:31.676 obj[name] = feta
    //09:31.684 obj[price] = 5
    //09:31.692 obj[category] = food
    //09:31.711 -------
    //09:31.720 obj[name] = cheddar 
    //09:31.728 obj[price] = 6
    //09:31.736 obj[category] = food
    //09:31.744 -------
    //09:31.752 obj[name] = taiwan beer
    //09:31.761 obj[price] = 30
    //09:31.769 obj[category] = food
    //09:31.778 obj[subCategory] = wine
    //09:31.786 obj[constructor] = function Wine(name, price) {
    //    // 如果傳入的 物件實例 (this) , 不是繼承 Wine, 則建立一個新的 ...
    //    if (!(this instanceof Wine)) {
    //        return new Wine(name, price);
    //    }
    //    //
    //    Food.call(this, name, price);
    //    this.subCategory = 'wine';
    //}
    //09:31.797 obj[getDetail] = function () {        // 建立一個方法 (override 祖先)
    //    return "name=" + this.name + " price=" + this.price + " category=" + this.category + " subcategory=" + this.subCategory;
    //}
    //09:31.805 -------
    //09:31.813 obj[name] = pocarri sweet
    //09:31.821 obj[price] = 25
    //09:31.829 obj[category] = food
    //09:31.837 obj[subCategory] = sport drink
    //09:31.846 obj[constructor] = function SportDrink(name, price) {
    //    // 如果傳入的 物件實例 (this) , 不是繼承 SportDrink, 則建立一個新的 ...
    //    if (!(this instanceof SportDrink)) {
    //        return new SportDrink(name, price);
    //    }
    //    //
    //    Food.call(this, name, price);
    //    this.subCategory = 'sport drink';
    //}
    //09:31.854 obj[getDetail] = function () {
    //    return "name=" + this.name + " price=" + this.price + " category=" + this.category + " subcategory=" + this.subCategory;
    //}
}


命名空間

Javascript 預設建立的類別是在 Global, 故難免會發生同名的衝突狀況; 為解決該問題, 故採用了物件屬性的方式, 模擬命名空間.

範例 4.1: 利用物件的屬性來模擬命名空間 


// ===============================================
// 命名空間 (namespace)
// =================================================
function testNamespace01() {
    /// <summary>利用物件的屬性來模擬命名空間 </summary>
    var MSDNMagNS = {};
    MSDNMagNS.Examples = {};    // nested namespace "Examples", 注意, 前端不需 var

    MSDNMagNS.Examples.Pet = function (name, age) {
        this.name = name;
        this.age = age;
    };

    MSDNMagNS.Examples.Pet.prototype.getDetail = function () {
        return "name=" + this.name + " age=" + this.age;
    };

    var pet01 = new MSDNMagNS.Examples.Pet("Tony", 2);
    dispLog("pet01: " + pet01.getDetail());

    // 命名空間太長了, 所以用縮寫
    // MSDNMagNS.Examples and Pet definition...
    // think  "using Eg = MSDNMagNS.Examples;" 
    var Eg = MSDNMagNS.Examples;
    var pet02 = new Eg.Pet("Tony2", 3);
    dispLog("pet02: " + pet02.getDetail());

    // 執行結果
    // 09:31.889 pet01: name=Tony age=2
    // 09:31.897 pet02: name=Tony2 age=3

}

總結


經由這段期間的研讀, 終於有一點小小的概念, 如果有錯, 還請各位指點.
其實, Javascript 只能說是 OO-Like 的程式語言, 很多物件導向的特性, 需經由模擬, 過程有些煩瑣.
原本很想作一個包含 封裝 + 繼承 + 多型 的範例, 但發現有一些問題無法克服, 故將範例拆解為2個部份, (封裝) (繼承 + 多型) 各一; 日後筆者功力如有進步, 再作補充.


參考文件


沒有留言:

張貼留言