2016年2月24日 星期三

JavaScript 原型式的物件導向繼承機制

在前一篇文章中我們提到唯有先搞清楚 JavaScript 中原型式的物件導向觀念與作法,才能在使用 JavaScript 這門語言時游刃有餘。但什麼是原型式的物件導向呢?
在大多數程式語言裡,每一個物件都是一個相關類別的實例(instance) ,這個類別提供了它所有實例所共用的程式碼。但在 JavaScript 裡是沒有類別這種東西的,取而代之是透過繼承其它物件來達到程式碼的共用。也就是說每個物件都會與其它的物件有關,這個關聯的物件就被稱為原型(雖然可以建立沒有原型的物件,但大部份而言,還是遵循上述規則)
以實字物件與內建建構式來說:
Var obj1 = new Object();
Var obj2 = {};

執行上述任一行程式,執行引擎會自動將 Object.prototype 設為新物件的原型
我們可以透過 ECMAScript  5 提供的 Object.getPrototypeOf 來查看這二個物件的原型:

console.log(Object.getPrototypeOf(obj1) === Object.prototype); //true
console.log(Object.getPrototypeOf(obj2) === Object.prototype); //true

也就是說不管使用實字物件方法或內建建構式中的任一種方法來建立新物件,新物件的原型一律是 Object.prototype,這會產生一種現象,當我們動態為任一物件的原型新增一個屬性或方法時,也會直接影影子到其它透過實字物件或內建建構式建立出來的物件,如下範例:

var obj1 = {};       //實字物件
           var obj2= new Object(); //內建建構式
           //動態透過 obj 物件在其原型上新增一個  say 方法
        obj1.constructor.prototype.say=function()  //constructor 後面在說明
          {
                alert('I am a prototype method');
          };
          obj1.say();    //output I am a prototype method
          obj2.say();    //I am a prototype method

 由以上的結果可以看出二個現象:
1.  在物件建立之後,再動態的在其原型物件上新增屬性或方法,仍然有效。
2. 原型上的屬性或方法是透過附加的方式,加到新物件上並非是採複製的方式,所以就算只修改任一物件的原型上的方法,也能立即反應在其它物件上,表示它們確實是共用一份程式碼,它們的關係如下:

  

  

,這個 Object prototype   JavaScript 所有物件之母

如果我們是透過自訂建構式函式建立新的物件又如何呢?

    function  Dog(name){
         this.name;
};

Dog.prototype.getName = function()[
     return this.name;
};

var dog = new Dog(‘harry’);
console.log(Object.getPrototypeOf(dog) ===Dog.prototype); //true
Dog 建構函式內建了一個預設的 prototype 屬性含有一個  
物件,它一 開始大致上是空的,它就是所謂的  dog 物件的   
原型,在上面的範例中,我們新增了一個方法到 
Dog.prototype 物件中:getName。當我們使用 new 建立了 Dog 
的一個實例時, dog 物件的原型就自動被設為儲存在 
Dog.prototype 中的物件,下圖顯示了它們的關係:
     
               

它一樣遵循著在物件建立之後,再動態的在其原型物件上新增屬性或方法,對已建立之物件仍然有效及原型上的屬性或方法是透過附加的方式,加到新物件上並非是採複製的方式,這二項規則,下列程式碼可以證明:


var Dog = function(name){
         this.name = "marry";
    };

    Dog.prototype.getName = function(){     
         return this.name;
    };
          
    var obj01 = new Dog("harry");
    var obj02 = new Dog("marry");
          
//在物件建立後才去修改原型裡的方法
    Dog.prototype.getName = function(){
   
     return  "harry" +"   "+"marry";
    }
console.log(obj01.getName()); // harry” +”   “+”marry”
console.log(obj02.getName());//“harry” +”   “+”marry”

二個新物件的 getName 方法都是輸出 “harry” +”   “+”marry”
這個特點和 Java C# 這種  class-base   OOPL 有很大的不同,也是 javaScript 最大的特點。因為在傳統的物件導向系統裡,我們是無法在物件產生之後再去修改類別並讓已建立的物件能立即發揮作用的。

也許我們又會追問,那函式預設的原型即然也是一個物件,按照定義,在 JavaScript 裡物件都有個原型,那這個函式的原型物件的原型又是什麼?透過下列實驗我們可以看出,Object.prototype 就是函式的原型物件的原型:

alert(Object.getPrototypeOf(Dog.prototype) === Object.prototype); //true

或者透過下列程式來驗證:
var Dog = function(name){
         this.name = name;
    };

    Dog.prototype.getName = function(){   
        return this.name;
    };
       
        var Cat = function(name){
         this.name = name;
    };
       
        Cat.prototype.getName = function(){    
         return this.name;
    };
       
       
    var obj01 = new Dog("I am dog");
    var obj02 = new Cat("I am cat");
       
        Object.getPrototypeOf(Dog.prototype).getdis = function(){
           alert('I am Object.prototype');
        }
         
       
        obj01.getdis(); //'I am Object.prototyp
    obj02.getdis();//'I am Object.prototyp

JavaScript 所有物件的原型基本上都可以追溯到這個物件,Object.prototype JavaScript 裡的萬物之母(基本上,在 JavaScript 裡除了這個物件沒有原型外,其它的物件都有原型)。這種一層一層往上委託的機制就是 JavaScript 裡所謂的物件的原型鏈,當我們需要存取任一物件的屬性或方法時,物件會先在本身定義的屬性裡尋找,如果沒有找到就往其原型裡找,如果還沒找到就再往其原型的原型裡找,直到找到最頂端,也就是  Object.prototype,如果都沒找到,就返回 undefined

至此我們可以稍微整理一下幾個重要的事實:
1. JavaScript 裡的所有函式都有一個 prototype 屬性,它存放一個稍為原型的物件。
2. JavaScript 的每個物件都擁有一個名為 constructor 的隱性屬性,指向當初用來建構該物件的建構式並且由於 prototype 原型屬性存在於建構式裡,所以我們可得到如下的關係圖:
    
    


  
  一個物件的原型除了可透過dog.constructor.prototype 取得外,也可利用ES5 提供 getPrototypeOf 方法取得,另外在某些瀏覽器裡,如 chrom   firefox ie9 以上,也為所有物件提供了一個 __proto__ 的屬性,當物件被建立時,這個 __proto__ 屬性的值就是它的構造函數的 prototype ,所以

   Object. getPrototypeOf(dog) === dog.constructor.prototype === dog.__proto__      === Dog.prototype
另外,一個物件如果己建立,緃使我們不知它原來的建構式為何?我們仍能透過 dog.constructor 來得知它原來的建構式是什麼,甚至透過它建立另一個實例,如下:
var dog2 = dog.constructor();


   prototype JavaScript 裡是如此的重要,以致於有那麼多種方式可以去存取到它, prototype 屬性類似於  class-base 等語言中的Class 做為物件的藍圖的角色,是實作 OOP 中繼承的機制 ,先了解這種基於原型的繼承機制我們才能繼續探索 JavaScript 中的封裝、多型及使用  JavaScript 特有原型物件導向機制來實作各種設計模式。

2016年2月15日 星期一

JavaScript 的命名慣例

1.建構式的名字首字母為大寫一般函式和方法首字母為小寫

2016年2月14日 星期日

JavaScript 建立物件的幾種方法與優缺點分析

    JavaScript 是一種物件導向的程式語言,但確不存在類別的概念,這跟我們以往在 JavaC++C# 等語言上所學到的物件導向觀念有很大的不同,也是一般JavaScript 初學者所要適應與轉變的觀念。談 OOP 總離不開封裝、繼承與多型等基本觀念,而 JavaScript 在實作這三者時與JavaC++C# 等語言也有著截然不同的作法,唯有先搞清楚 JavaScript 中原型式的物件導向觀念與作法,才能在使用 JavaScript 這門語言時游刃有餘。首先我們將透過如何在 JavaScript 中建立物件的幾種方式來展開 JavaScript  OOP 之旅

   JavaScript 中要建立一個物件,非常簡單,不像JavaC++C# 等語言要透過  Class 才有辦法建立一個物件
第一種方法是透過所謂的實字符號模式(literal notation patterns) ,快速的建立一個物件,如下程式片段:

var dog = {};     //空物件
dog.name = “harry”;
dog.getName = function(){
     return dog.name;
};
也可以這樣建立一個新物件:
var dog = {
     name: “harry”,
     getName: function(){
          return this.name;   //or return name
}
};

當我們只想為物件建立一個執行個體時,會使用此方式,此方式不適合於需建立多個相同物件模板的運用
第二種方式是透過內建的建構式(constructor functions)來建立一個新的物件:

var dog = new Object();//空物件,但會自動從 Object.prototype 承屬性與方法
console.log(dog.constructor === Object)’ //true
dog.name = “harry”;

此種方式也只適用於想為物件建立一個執行個體時使用
另外需注意的是: Object 可以接受一個參數此時傳回的就不一定是 Object 如下例:

var obj = new Object(1);
console.log(o.constructor === Number); //true
var obj = new Object(“harry”);
console.log(o.constructor === String); //true
var obj = new Object(true”);
console.log(o.constructor === Boolean); //true

 所以在使用上時要別別小心,尤其是在動態建立物件的情況下,可能很容易得到不如預期的結果。
第三種方法是透過自訂建構式函式來建立新物件,透過這種方式建立物件,JavScript 引擎會在背後做一些事,如下例:

var Dog = function(name){
     //使用物件實字自動建立一個空物件
     // var this = Object.create(Dog.prototype);
     this.name = name;
     this.getName = function(){
          return this.name;
};
// return this;  //隱式傳回 this 物件
};

var dog = new Dog(“harry”);  //雖然很像 Java c# 中透過類別建立物件的方式,但它並不是,它稱為函式建構子
dog.getName; //harry

這種建立物件的方式有一個缺點,就是你每建立一個新的物件,就會有一個新的屬性及方法被建立在記憶體中,對於物件的屬性來說是合理,因為每個物件的屬性值本來就會有所不同,但對於可重覆利用的方法來說,很明顯這是一種記憶體上的浪費,應該透過如下的程式片段來達成方法的共用:

var Dog = function(name){
     this.name = name;    
};
Dog.prototype.getName = function(){
     return this.name;
};

這樣當透過自訂建構式函式建立二個以上的物件時,實際上只會在記憶體建立一份方法:

var dog1 = new Dog(“harry”);
dog1.getName; //harray
var dog2 = new Dog(“marry”);
dog2.getName; // marry

自訂建構式函式時,也可以透如下的方式定義屬性和方法
var Dog = function({   
};
Dog.name = “harray”;
Dog.getName = function(){
     return this.name;
};

但需注意的是透過這種方式宣告的屬性與方法稱為靜態屬性與方法,有如下的特性:

console.log(Dog.name); //harry
console.log(typeof Dog. getName); //function
           
var o=new Dog ();
console.log(o.name); //undefined
console.log(typeof o. getName); //undefined

另外關於自訂建構式函式需要注意的是一定要使用 new 來建立物件,如果忘了使用 new  在建構式中的 this 就會指向全域物件,也就是在瀏覽器中, this 就指向 window

var dog = Dog();
console.log(typeof dog); // undefined 忘了new, 不會有物件this回傳 變成全域
console.log(window.name); //全域變數

可以透過下列的技巧讓自訂建構式函式無論使用那一種方式,一定會回傳物件:


優點
缺點
實字物件建立方法
快速而簡潔的建立單一物件。
無法透過它去完成建立同一類型物件的多個實例。
內建建構式函式
可透過參數建立不同型別的物件
1.      無法透過它去完成建立同一類型物件的多個實例。
2.      當傳入不同參數會得到不同的預期結果。所以通常不建議採用此方法來建立新物件,可直接採用實字法。
自訂建構式函式
1.      可以建立同一類型物件的多個實例。
2.      可以建立靜態屬性與方法,使之成為工具類別,服務整個應用。
建立方式比較複雜,需了解不同建立方式間的區別。


在上面三種建立物件的方法時,有二個  keyword 出現,一個是 prototype constructor,下一篇文章,我們將介紹這二個 keyword 讓我們向 JavaScript 式的 OOP 更接近一步。
參考文件: