緣起
接續 前一篇, 這裡再補上截至目前所學習到的一些觀念; 本文主要以範例為主, 一些說明都寫在程式註解.
完整範例, 請由此下載.
資訊隱藏 (封裝) (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個部份, (封裝) (繼承 + 多型) 各一; 日後筆者功力如有進步, 再作補充.
參考文件
- 使用物件導向技術來建立進階 Web 應用程式
- JavaScript 物件繼承機制
- JavaScript 語言核心(18)模擬類別的封裝與繼承
- {小朱} [JavaScript] JavaScript 的物件導向設計 (1):體驗篇
- {小朱} [JavaScript] JavaScript 物件導向設計 (2): 繼承篇
- {小朱} [JavaScript] JavaScript 物件導向設計 (3) : 多型與介面篇
- {Book}JavaScript: The Definitive Guide, 6th Edition
- Understanding JavaScript Object Creation Patterns
- Object.create(): the New Way to Create Objects in JavaScript
- Mozilla Developer Network : Object.create()
沒有留言:
張貼留言