天天看點

JavaScript進階程式設計學習(六)之設計模式

每種程式設計語言都有其自己的設計模式。不禁讓人疑惑設計模式是用來做什麼?有什麼用?

簡單的說,設計模式是為了讓代碼更簡潔,更優雅,更完美。

同時設計模式也會讓軟體的性能更好,同時也會讓程式員們更輕松。設計模式可謂是程式設計界的“葵花寶典”或“辟邪劍法”。如果一旦練成,必可在程式設計界中來去自如,遊刃有餘。

下面進入正題

(1)工廠模式

工廠模式是軟體工程領域一種廣為人知的設計模式,這種模式抽象了建立具體對象的過程(本書後 面還将讨論其他設計模式及其在 JavaScript中的實作)。考慮到在 ECMAScript中無法建立類,開發人員 就發明了一種函數,用函數來封裝以特定接口建立對象的細節。

<html>
<meta charset="utf-8">
<head>
<script>
 function createPerson(name,age,job){
 var o = new Object();
 o.name=name;
 o.age=age;
 o.job=job;
 o.sayName=function(){
    alert(this.name);
 };
 return o;
 
 }
 var person1 = createPerson("a",20,"java");
 var person2 = createPerson("b",20,"c++");
alert(person1.name);
alert(person2.name);
</script>
</head>
<body>
 

</body>
</html>      

工廠模式,和Java的工廠模式差異不大,就是為了批量生産對象,上述函數我隻需寫一遍,我就能通過調用函數源源不斷的進行複用傳參。

簡單的說工廠模式就是為了提高函數複用率,寫一遍的東西我不要寫好幾遍就可以調用。

函數 createPerson()能夠根據接受的參數來建構一個包含所有必要資訊的 Person 對象。可以無 數次地調用這個函數,而每次它都會傳回一個包含三個屬性一個方法的對象。工廠模式雖然解決了建立 多個相似對象的問題,但卻沒有解決對象識别的問題(即怎樣知道一個對象的類型)。

任何模式都有其局限性,是以誕生了一個構造函數模式。

(2)構造函數模式

構造函數與其他函數的唯一差別,就在于調用它們的方式不同。不過,構造函數畢竟也是函數,不存在定義構造函數的特殊文法。任何函數,隻要通過 new 操作符來調用,那它就可以作為構造函數;而任何函數,如果不通過 new 操作符來調用,那它跟普通函數也不會有什麼兩樣。例如,前面例子中定義 的 Person()函數可以通過下列任何一種方式來調用。

<html>
<meta charset="utf-8">
<head>
<script>
 function createPerson(name,age,job){
 var o = new Object();
 o.name=name;
 o.age=age;
 o.job=job;
 o.sayName=function(){
    alert(this.name);
 };
 return o;
 
 }
var person = new Person("zs",20,"java");
person.sayName();
Person("ls",20,"c++");
window.sayName();

</script>
</head>
<body>
 

</body>
</html>      

構造函數模式雖然好用,但也并非沒有缺點。使用構造函數的主要問題,就是每個方法都要在每個 執行個體上重新建立一遍。在前面的例子中,person1 和 person2 都有一個名為 sayName()的方法,但那 兩個方法不是同一個 Function 的執行個體。不要忘了——ECMAScript中的函數是對象,是以每定義一個 函數,也就是執行個體化了一個對象

(3)原型模式

我們建立的每個函數都有一個 prototype(原型)屬性,這個屬性是一個指針,指向一個對象, 而這個對象的用途是包含可以由特定類型的所有執行個體共享的屬性和方法。如果按照字面意思來了解,那 麼 prototype 就是通過調用構造函數而建立的那個對象執行個體的原型對象。使用原型對象的好處是可以 讓所有對象執行個體共享它所包含的屬性和方法。換句話說,不必在構造函數中定義對象執行個體的資訊,而是 可以将這些資訊直接添加到原型對象中。

a.了解原型模型

無論什麼時候,隻要建立了一個新函數,就會根據一組特定的規則為該函數建立一個 prototype 屬性,這個屬性指向函數的原型對象。在預設情況下,所有原型對象都會自動獲得一個 constructor (構造函數)屬性,這個屬性包含一個指向 prototype 屬性所在函數的指針。就拿前面的例子來說, Person.prototype. constructor 指向 Person。而通過這個構造函數,我們還可繼續為原型對象 添加其他屬性和方法。 建立了自定義的構造函數之後,其原型對象預設隻會取得 constructor 屬性;至于其他方法,則 都是從 Object 繼承而來的。當調用構造函數建立一個新執行個體後,該執行個體的内部将包含一個指針(内部 屬性),指向構造函數的原型對象。ECMA-262第 5版中管這個指針叫[[Prototype]]。雖然在腳本中 沒有标準的方式通路[[Prototype]],但 Firefox、Safari 和 Chrome 在每個對象上都支援一個屬性 __proto__;而在其他實作中,這個屬性對腳本則是完全不可見的。不過,要明确的真正重要的一點就 是,這個連接配接存在于執行個體與構造函數的原型對象之間,而不是存在于執行個體與構造函數之間。

b. 原型與 in 操作符

有兩種方式使用 in 操作符:單獨使用和在 for-in 循環中使用。在單獨使用時,in 操作符會在通 過對象能夠通路給定屬性時傳回 true,無論該屬性存在于執行個體中還是原型中。

<html>
<meta charset="utf-8">
<head>
<script>
function Person(){ } 
 
Person.prototype.name = "Nicholas"; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer";
 Person.prototype.sayName = function(){    
 alert(this.name); 
 }; 
 
var person1 = new Person(); 
var person2 = new Person(); 
 
alert(person1.hasOwnProperty("name"));  //false 
alert("name" in person1);  //true 
 
person1.name = "Greg"; 
alert(person1.name);   //"Greg" ——來自執行個體 
alert(person1.hasOwnProperty("name"));  //true 
alert("name" in person1);  //true 
 
alert(person2.name);   //"Nicholas" ——來自原型 
alert(person2.hasOwnProperty("name"));  //false 
alert("name" in person2);  //true 
 
delete person1.name; 
alert(person1.name);   //"Nicholas" ——來自原型 
alert(person1.hasOwnProperty("name"));  //false 
alert("name" in person1);  //true 


</script>
</head>
<body>
 

</body>
</html>      

在以上代碼執行的整個過程中,name 屬性要麼是直接在對象上通路到的,要麼是通過原型通路到 的。是以,調用"name" in person1 始終都傳回 true,無論該屬性存在于執行個體中還是存在于原型中。 同時使用 hasOwnProperty()方法和 in 操作符,就可以确定該屬性到底是存在于對象中,還是存在于原型中。

由于 in 操作符隻要通過對象能夠通路到屬性就傳回 true,hasOwnProperty()隻在屬性存在于 執行個體中時才傳回 true,是以隻要 in 操作符傳回 true 而 hasOwnProperty()傳回 false,就可以确 定屬性是原型中的屬性。

c.更簡單的原型文法

<html>
<meta charset="utf-8">
<head>
<script>
function Person(){ } 
 
Person.prototype = {   
  name : "Nicholas",     
  age : 29,    
  job: "Software Engineer",   
  sayName : function () {   
  alert(this.name);   
  } 
  }; 

</script>
</head>
<body>
 

</body>
</html>      

d.原型的動态性

由于在原型中查找值的過程是一次搜尋,是以我們對原型對象所做的任何修改都能夠立即從執行個體上 反映出來——即使是先建立了執行個體後修改原型也照樣如此。

var friend = new Person(); 
 
Person.prototype.sayHi = function(){
     alert("hi");
 }; 
 
friend.sayHi();   //"hi"(沒有問題!)       

以上代碼先建立了 Person 的一個執行個體,并将其儲存在 person 中。然後,下一條語句在 Person. prototype 中添加了一個方法 sayHi()。即使 person 執行個體是在添加新方法之前建立的,但它仍然可 以通路這個新方法。其原因可以歸結為執行個體與原型之間的松散連接配接關系。當我們調用 person.sayHi() 時,首先會在執行個體中搜尋名為 sayHi 的屬性,在沒找到的情況下,會繼續搜尋原型。因為執行個體與原型 之間的連接配接隻不過是一個指針,而非一個副本,是以就可以在原型中找到新的 sayHi 屬性并傳回儲存 在那裡的函數。 盡管可以随時為原型添加屬性和方法,并且修改能夠立即在所有對象執行個體中反映出來,但如果是重 寫整個原型對象,那麼情況就不一樣了。我們知道,調用構造函數時會為執行個體添加一個指向初原型的 [[Prototype]]指針,而把原型修改為另外一個對象就等于切斷了構造函數與初原型之間的聯系。 請記住:執行個體中的指針僅指向原型,而不指向構造函數。

例如:

function Person(){ } 
 
var friend = new Person();     
 Person.prototype = {   
  constructor: Person,    
 name : "Nicholas",  
   age : 29,   
  job : "Software Engineer",    
 sayName : function () {        
 alert(this.name);  
   } }; 
 
friend.sayName();   //error 
       

看圖:

JavaScript進階程式設計學習(六)之設計模式

e.原型對象問題

原型模式也不是沒有缺點。首先,它省略了為構造函數傳遞初始化參數這一環節,結果所有執行個體在 預設情況下都将取得相同的屬性值。雖然這會在某種程度上帶來一些不友善,但還不是原型的大問題。 原型模式的大問題是由其共享的本性所導緻的。 原型中所有屬性是被很多執行個體共享的,這種共享對于函數非常合适。對于那些包含基本值的屬性倒 也說得過去,畢竟(如前面的例子所示),通過在執行個體上添加一個同名屬性,可以隐藏原型中的對應屬 性。然而,對于包含引用類型值的屬性來說,問題就比較突出了.

<html>
<meta charset="utf-8">
<head>
<script>

 function Person(){ 
 } 
 
Person.prototype = {   
  constructor: Person,    
  name : "Nicholas",     
  age : 29,    
  job : "Software Engineer",    
  friends : ["Shelby", "Court"],   
  sayName : function () {      
  alert(this.name);   
  } 
  }; 
 
var person1 = new Person();
var person2 = new Person(); 
 
person1.friends.push("Van"); 
 
alert(person1.friends);    //"Shelby,Court,Van" 
alert(person2.friends);    //"Shelby,Court,Van"
alert(person1.friends === person2.friends);  //true 
</script>
</head>
<body>
 

</body>
</html>      

在此,Person.prototype 對象有一個名為 friends 的屬性,該屬性包含一個字元串數組。然後, 建立了 Person 的兩個執行個體。接着,修改了 person1.friends 引用的數組,向數組中添加了一個字元 串。由于 friends 數組存在于 Person.prototype 而非 person1 中,是以剛剛提到的修改也會通過 person2.friends(與 person1.friends 指向同一個數組)反映出來。假如我們的初衷就是像這樣 在所有執行個體中共享一個數組,那麼對這個結果我沒有話可說。可是,執行個體一般都是要有屬于自己的全部 屬性的。而這個問題正是我們很少看到有人單獨使用原型模式的原因所在。

(4)組合使用構造函數模式和原型模式

建立自定義類型的常見方式,就是組合使用構造函數模式與原型模式。構造函數模式用于定義實 例屬性,而原型模式用于定義方法和共享的屬性。結果,每個執行個體都會有自己的一份執行個體屬性的副本, 但同時又共享着對方法的引用,大限度地節省了記憶體。另外,這種混成模式還支援向構造函數傳遞參 數;可謂是集兩種模式之長。

<html>
<meta charset="utf-8">
<head>
<script>

function Person(name, age, job){   
  this.name = name;   
  this.age = age;    
  this.job = job;    
  this.friends = ["Shelby", "Court"]; 
  } 
 
Person.prototype = { 
    constructor : Person, 
    sayName : function(){        
    alert(this.name);   
    } 
    } 
 
var person1 = new Person("Nicholas", 29, "Software Engineer");
 var person2 = new Person("Greg", 27, "Doctor"); 
 
person1.friends.push("Van"); alert(person1.friends);    //"Shelby,Count,Van" 
alert(person2.friends);    //"Shelby,Count" 
alert(person1.friends === person2.friends);    //false 
alert(person1.sayName === person2.sayName);    //true 


</script>
</head>
<body>
 

</body>
</html>      

在這個例子中,執行個體屬性都是在構造函數中定義的,而由所有執行個體共享的屬性 constructor 和方 法 sayName()則是在原型中定義的。而修改了 person1.friends(向其中添加一個新字元串),并不 會影響到 person2.friends,因為它們分别引用了不同的數組。 這種構造函數與原型混成的模式,是目前在 ECMAScript中使用廣泛、認同度高的一種建立自 定義類型的方法。可以說,這是用來定義引用類型的一種預設模式。

(5)動态原型模式

有其他 OO語言經驗的開發人員在看到獨立的構造函數和原型時,很可能會感到非常困惑。動态原 型模式正是緻力于解決這個問題的一個方案,它把所有資訊都封裝在了構造函數中,而通過在構造函數 中初始化原型(僅在必要的情況下),又保持了同時使用構造函數和原型的優點。換句話說,可以通過 檢查某個應該存在的方法是否有效,來決定是否需要初始化原型。

示例如下:

<html>
<meta charset="utf-8">
<head>
<script>

function Person(name, age, job){ 
 
    //屬性     
    this.name = name;    
    this.age = age;     
    this.job = job; 
      //------
      if (typeof this.sayName != "function"){   
      Person.prototype.sayName = function(){        
      alert(this.name);     
      };            
      }
      } 
      //------
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName(); 
 

</script>
</head>
<body>
 

</body>
</html>      

注意構造函數代碼中//------部分。這裡隻在 sayName()方法不存在的情況下,才會将它添加到原 型中。這段代碼隻會在初次調用構造函數時才會執行。此後,原型已經完成初始化,不需要再做什麼修 改了。不過要記住,這裡對原型所做的修改,能夠立即在所有執行個體中得到反映。是以,這種方法确實可 以說非常完美。其中,if 語句檢查的可以是初始化之後應該存在的任何屬性或方法——不必用一大堆 if 語句檢查每個屬性和每個方法;隻要檢查其中一個即可。對于采用這種模式建立的對象,還可以使 用 instanceof 操作符确定它的類型。

(6)寄生構造函數模式

通常,在前述的幾種模式都不适用的情況下,可以使用寄生(parasitic)構造函數模式。這種模式 的基本思想是建立一個函數,該函數的作用僅僅是封裝建立對象的代碼,然後再傳回新建立的對象;但 從表面上看,這個函數又很像是典型的構造函數。

<html>
<meta charset="utf-8">
<head>
<script>

function Person(name, age, job){    
 var o = new Object();    
 o.name = name;    
 o.age = age;     
 o.job = job;    
 o.sayName = function(){    
 alert(this.name);   
 };     
 return o; 
 } 
 
var friend = new Person("Nicholas", 29, "Software Engineer");
 friend.sayName();  //"Nicholas" 
 

</script>
</head>
<body>
 

</body>
</html>      

在這個例子中,Person 函數建立了一個新對象,并以相應的屬性和方法初始化該對象,然後又返 回了這個對象。除了使用 new 操作符并把使用的包裝函數叫做構造函數之外,這個模式跟工廠模式其實 是一模一樣的。構造函數在不傳回值的情況下,預設會傳回新對象執行個體。

而通過在構造函數的末尾添加一個 return 語句,可以重寫調用構造函數時傳回的值。 這個模式可以在特殊的情況下用來為對象建立構造函數。假設我們想建立一個具有額外方法的特殊 數組。由于不能直接修改 Array 構造函數,是以可以使用這個模式。

示例:

<html>
<meta charset="utf-8">
<head>
<script>


 function SpecialArray(){ 
 
    //建立數組   
    var values = new Array(); 
 
    //添加值     
    values.push.apply(values, arguments); 
 
    //添加方法     
    values.toPipedString = function(){        
    return this.join("|");    
    };          //傳回數組    
    return values; 
    } 
 
var colors = new SpecialArray("red", "blue", "green"); 
alert(colors.toPipedString()); //"red|blue|green" 
</script>
</head>
<body>
 

</body>
</html>      

在這個例子中,我們建立了一個名叫 SpecialArray 的構造函數。在這個函數内部,首先建立了 一個數組,然後 push()方法(用構造函數接收到的所有參數)初始化了數組的值。随後,又給數組實 例添加了一個 toPipedString()方法,該方法傳回以豎線分割的數組值。後,将數組以函數值的形 式傳回。接着,我們調用了 SpecialArray 構造函數,向其中傳入了用于初始化數組的值,此後又調 用了 toPipedString()方法。 關于寄生構造函數模式,有一點需要說明:首先,傳回的對象與構造函數或者與構造函數的原型屬 性之間沒有關系;也就是說,構造函數傳回的對象與在構造函數外部建立的對象沒有什麼不同。為此, 不能依賴 instanceof 操作符來确定對象類型。由于存在上述問題,我們建議在可以使用其他模式的情 況下,不要使用這種模式。

(7) 穩妥構造函數模式

道格拉斯·克羅克福德(Douglas Crockford)發明了 JavaScript中的穩妥對象(durable objects)這 個概念。所謂穩妥對象,指的是沒有公共屬性,而且其方法也不引用 this 的對象。穩妥對象适合在 一些安全的環境中(這些環境中會禁止使用 this 和 new),或者在防止資料被其他應用程式(如 Mashup 程式)改動時使用。穩妥構造函數遵循與寄生構造函數類似的模式,但有兩點不同:一是新建立對象的 執行個體方法不引用 this;二是不使用 new 操作符調用構造函數。按照穩妥構造函數的要求,可以将前面 的 Person 構造函數重寫如下

<html>
<meta charset="utf-8">
<head>
<script>

function Person(name, age, job){        
  //建立要傳回的對象    
 var o = new Object(); 
  //可以在這裡定義私有變量和函數 
 
    //添加方法    
    o.sayName = function(){     
    alert(name);    
    };              //傳回對象   
    return o; 
    } 
 
</script>
</head>
<body>
 

</body>
</html>      

注意,在以這種模式建立的對象中,除了使用 sayName()方法之外,沒有其他辦法通路 name 的值。 可以像下面使用穩妥的 Person 構造函數。

var friend = Person("Nicholas", 29, "Software Engineer");
 friend.sayName();  //"Nicholas"       

這樣,變量 friend 中儲存的是一個穩妥對象,而除了調用 sayName()方法外,沒有别的方式可 以通路其資料成員。即使有其他代碼會給這個對象添加方法或資料成員,但也不可能有别的辦法通路傳 入到構造函數中的原始資料。穩妥構造函數模式提供的這種安全性,使得它非常适合在某些安全執行環 境——例如,ADsafe(www.adsafe.org)和 Caja(http://code.google.com/p/google-caja/)提供的環境—— 下使用。

注意:

與寄生構造函數模式類似,使用穩妥構造函數模式建立的對象與構造函數之間也 沒有什麼關系,是以 instanceof 操作符對這種對象也沒有意義。

js的工廠模式,構造函數模式,原型模式,組合使用構造函數合原型模式,動态原型模式,寄生構造函數模式,穩妥構造函數模式等

大家有沒有從中發現,js的設計模式基本圍繞着構造和原型這兩個主要點。

該篇隻是對js的設計模式作出相應的講解和知識普及,不熟悉該概念的可以參考此文,大家可以将上述例子,自己動手敲着看,哪怕有經驗的Java開發或者js,一起敲敲吧,保證有不一樣的體會。