JavaScript 面向對象程序設計-封裝

作者:不詳 來源: 日期:2008-1-15

本文寫得非常精彩,所以轉載過來推薦大家閱讀。


JavaScript 是一種非常靈活的面向對象程序設計語言,它與傳統的強類型的面向對象程序設計語言(如 C++,Java,C# 等)有很大不同,所以要實現如 C++、java、C# 當中的一些特性就需要換一種思考方式來解決。今天主要討論如何在 JavaScript 腳本中實現數據的封裝(encapsulation)。

數據封裝說的簡單點就是把不希望調用者看見的內容隱藏起來。它是面向對象程序設計的三要素之首,其它兩個是繼承和多態,關于它們的內容在后面再討論。

關于數據封裝的實現,在 C++、Java、C# 等語言中是通過 public、private、static 等關鍵字實現的。在 JavaScript 則采用了另外一種截然不同的形式。在討論如何具體實現某種方式的數據封裝前,我們先說幾個簡單的,大家所熟知卻又容易忽略的 JavaScript 的概念。

1 幾個基本概念

1.1 變量定義

在 JavaScript 語言中,是通過 var 關鍵字來定義變量的。

但是如果我們直接給一個沒有使用 var 定義的變量賦值,那么這個變量就會成為全局變量。

一般情況下,我們應該避免使用沒有用 var 定義的變量,主要原因是它會影響程序的執行效率,因為存取全局變量速度比局部變量要慢得多。

但是這種用法可以保證我們的變量一定是全局變量。

另外,為了保證速度,我們在使用全局變量時,可以通過 var 定義一個局部變量,然后將全局變量賦予之,由此可以得到一個全局變量的局部引用。

1.2 變量類型

沒有定義的變量,類型為 undefined。

變量的值可以是函數。

函數在 JavaScript 中可以充當類的角色。

1.3 變量作用域

變量作用域是指變量生存周期的有效范圍。

單純用 { } 創建的塊不能創建作用域。

with 將它包含的對象作用域添加到當前作用域鏈中,但 with 不創建新的作用域。with 塊結束后,會將對象作用域從當前作用域鏈中刪除。

try-catch 中,catch 的錯誤對象只在 catch 塊中有效,但 catch 塊中定義的變量屬于當前作用域。

其它如 if、for、for-in、while、do-while、switch 等控制語句創建的塊不能創建作用域。

用 function 創建的函數,會創建一個新的作用域添加到當前作用域中。

2 封裝

下面我們就來討論具體的封裝。首先說一下大家最熟悉的幾種封裝:私有實例成員、公有實例成員和公有靜態成員。最后會討論一下大家所不熟悉的私有靜態成員和靜態類的封裝辦法。因為下面要討論的是面向對象編程,所有當函數作為類來定義和使用時,我們暫且將其成為類。

2.1 私有實例成員

私有實例成員在 JavaScript 中實際上可以用函數內的局部變量來實現,它相當于類的私有實例成員。例如:

  1. class1 = function() {
  2.     // private fields
  3.     var m_first = 1;
  4.     var m_second = 2;
  5.     // private methods
  6.     function method1() {
  7.         alert(m_first);
  8.     }
  9.     var method2 = function() {
  10.         alert(m_second);
  11.     }
  12.     // constructor
  13.     {
  14.         method1();
  15.         method2();
  16.     }
  17. }
  18. var o = new class1();
  19. // error
  20. alert(o.m_first);
  21. o.method1();

這里 m_first 和 m_second 是 class1 的兩個私有實例字段,method1 和 method2 是兩個私有實例方法。他們只能在該類的對象內部被使用,在對象外無法使用。

這里大家會發現創建私有方法有兩種方式,一種是直接在類中定義方法,另一種是先定義一個局部變量(私有實例字段),然后定義一個匿名方法賦值給它。

直接在類中定義方法,則該方法的作用域就是這個類,因此這個方法在此類外不能夠被訪問,而它又可以存取類中所有的私有實例字段,這就保證了這是個私有實例方法。

第二種創建私有實例方法的方式跟第一種方式的效果是一樣的,但是第二種方式更靈活一些。

你應該還會注意到,class1 中把構造器代碼用 { } 括起來了,這樣做雖然沒有必要,但是代碼看上去更加清晰。

關于這段構造器代碼,還有兩點需要說明的地方:

1、構造器代碼必須放在整個類定義的最后,這樣做是為了保證在它當中被調用的方法都已經被定義了。因為 JavaScript 是解釋型語言,所以,它會按照從上到下的順序執行,因此,如果構造器代碼放在其它方法定義的前面,則執行到調用語句時找不到要調用的方法,就會出錯。

2、我們已經知道 { } 創建的塊不會改變作用域,因此如果在這樣的構造器代碼中創建局部變量,實際上是在整個類中創建私有實例成員,所以,如果需要用到局部變量,應當定義一個私有實例方法,例如可以命名為 constructor(),在 constructor() 這個私有實例方法中定義局部變量和原來 { } 構造器中要執行的代碼,然后在類的最后直接調用它就可以了。所以更好的寫法是這樣的:

  1. class1 = function() {
  2.     // private fields
  3.     var m_first = 1;
  4.     var m_second = 2;
  5.     // private methods
  6.     function constructor() {
  7.         method1();
  8.         method2();
  9.     }
  10.     function method1() {
  11.         alert(m_first);
  12.     }
  13.     var method2 = function() {
  14.         alert(m_second);
  15.     }
  16.     constructor();
  17. }
  18. var o = new class1();
  19. // error
  20. alert(o.m_first);
  21. o.method1();

最后,你可能還會發現 class1 的定義我們沒有用 var,這樣做我們就可以保證它是個全局的類了。

2.2 公有實例成員

公有實例成員可以通過兩種方式來創建,我們先來看下面這個例子:

  1. class2 = function() {
  2.     // private fields
  3.     var m_first = 1;
  4.     var m_second = 2;
  5.     // private methods
  6.     function method1() {
  7.         alert(m_first);
  8.     }
  9.     var method2 = function() {
  10.         alert(m_second);
  11.     }
  12.     // public fields
  13.     this.first = "first";
  14.     this.second = ['s','e','c','o','n','d'];
  15.     // public methods
  16.     this.method1 = method2;
  17.     this.method2 = function() {
  18.         alert(this.second);
  19.     }
  20.     // constructor
  21.     {
  22.         method1();
  23.         method2();
  24.     }
  25. }
  26. // public method
  27. class1.prototype.method3 = function() {
  28.     alert(this.first);
  29. }
  30. var o = new class2();
  31. o.method1();
  32. o.method2();
  33. o.method3();
  34. alert(o.first);

我們發現這個例子是在 class1 的例子上做了一些補充。給它添加了公有實例字段和公有實例方法,我們把它們通稱為公有實例成員。

我們應該已經發現,創建公有實例成員其實很簡單,一種方式是通過在類中給 this.memberName 來賦值,如果值是函數之外的類型,那就是個公有實例字段,如果值是函數類型,那就是公有實例方法。另外一種方式則是通過給 className.prototype.memberName 賦值,可賦值的類型跟 this.memberName 是相同的。

到底是通過 this 方式定義好呢,還是通過 prototype 方式定義好呢?

其實它們各有各的用途,它們之間不是誰比誰更好的關系。在某些情況下,我們只能用其中特定的一種方式來定義公有實例成員,而不能夠使用另一種方式。原因在于它們實際上是有區別的:

1、prototype 方式只應該在類外定義。this 方式只能在類中定義。

2、prototype 方式如果在類中定義時,則存取私有實例成員時,總是存取最后一個對象實例中的私有實例成員。

3、prototype 方式定義的公有實例成員是創建在類的原型之上的成員。this 方式定義的公有實例成員,是直接創建在類的實例對象上的成員。

基于前兩點區別,我們可以得到這樣的結論:如果要在公有實例方法中存取私有實例成員,那么必須用 this 方式定義。

關于第三點區別,我們后面在討論繼承時再對它進行更深入的剖析。這里只要知道有這個區別就可以了。

我們還會發現,公有實例成員和私有實例成員名字是可以相同的,這樣不會有沖突嗎?

當然不會。原因在于它們的存取方式不同,公有實例成員在類中存取時,必須要用 this. 前綴來引用。而私有實例成員在類中存取時,不使用也不能夠使用 this. 前綴來存取。而在類外存取時,只有公有成員是可以通過類的實例對象存取的,私有成員無法存取。

2.3 公有靜態成員

公有靜態成員的定義很簡單,例如:

  1. class3 = function() {
  2.     // private fields
  3.     var m_first = 1;
  4.     var m_second = 2;
  5.     // private methods
  6.     function method1() {
  7.         alert(m_first);
  8.     }
  9.     var method2 = function() {
  10.         alert(m_second);
  11.     }
  12.     // constructor
  13.     {
  14.         method1();
  15.         method2();
  16.     }
  17. }
  18. // public static field
  19. class3.field1 = 1;
  20. // public static method
  21. class3.method1 = function() {
  22.     alert(class3.field1);
  23. }
  24. class3.method1();

這個例子的 class3 跟 class1 很像。不同的是 class3 的外面,我們又給 class3 定義了一個靜態字段和靜態方法。

定義的方式就是給 className.memberName 直接賦值。

這里定義的靜態字段和靜態方法都是可以被直接通過類名引用來存取的,而不需要創建對象。因此它們是公有靜態成員。

不過有點要記住,一定不要將公有靜態成員定義在它所在的類的內部,否則你會得到非你所期望的結果。我們可以看下面這個例子:

  1. class4 = function() {
  2.     // private fields
  3.     var m_first = 1;
  4.     var m_second = 2;
  5.     var s_second = 2;
  6.     // private methods
  7.     function method1() {
  8.         alert(m_first);
  9.     }
  10.     var method2 = function() {
  11.         alert(m_second);
  12.     }
  13.     class4.method1 = function() {
  14.         s_second++;
  15.     }
  16.     class4.method2 = function() {
  17.         alert(s_second);
  18.     }
  19. }
  20. var o1 = new class4();
  21. class4.method2();          // 2
  22. class4.method1();
  23. class4.method2();          // 3
  24. var o2 = new class4();
  25. class4.method2();          // 2
  26. class4.method1();
  27. class4.method2();          // 3

這個例子中,我們期望 s_second 能夠扮演一個私有靜態成員的角色,但是輸出結果卻不是我們所期望的。我們會發現 s_second 實際上是 class4 的一個私有實例成員,而不是私有靜態成員。而 class4 的 method1 和 method2 所存取的私有成員總是類的最后一個實例對象中的這個私有實例成員。

問題出在哪兒呢?

問題出在每次通過 new class4() 創建一個對象實例時,class4 中的所有語句都會重新執行,因此,s_second 被重置,并成為新對象中的一個私有實例成員。而 class4.method1 和 class4.method2 也被重新定義了,而這個定義也將它們的變量作用域切換到了最后一個對象上來。這與把通過 prototype 方式創建的公有實例方法定義在類的內部而產生的錯誤是一樣的。

所以,一定不要將公有靜態成員定義在它所在的類的內部!也不要把通過 prototype 方式創建的公有實例方法定義在類的內部!

那如何定義一個私有靜態成員呢?

2.4 私有靜態成員

前面在基本概念里我們已經清楚了,只有用 function 創建函數,才能創建一個新的作用域,而要創建私有成員(不論是靜態成員,還是實例成員),都需要通過創建新的作用域才能夠起到數據隱藏的目的。下面所采用的方法就是基于這一點來實現的。

實現私有靜態成員是通過創建一個匿名函數函數來創建一個新的作用域來實現的。

通常我們使用匿名函數時都是將它賦值給一個變量,然后通過這個變量引用該匿名函數。這種情況下,該匿名函數可以被反復調用或者作為類去創建對象。而這里,我們創建的匿名函數不賦值給任何變量,在它創建后立即執行,或者立即實例化為一個對象,并且該對象也不賦值給任何變量,這種情況下,該函數本身或者它實例化后的對象都不能夠被再次存取,因此它唯一的作用就是創建了一個新的作用域,并隔離了它內部的所有局部變量和函數。因此,這些局部變量和函數就成了我們所需要的私有靜態成員。而這個立即執行的匿名函數或者立即實例化的匿名函數我們稱它為靜態封裝環境。

下面我們先來看通過直接調用匿名函數方式來創建帶有私有靜態成員的類的例子:

  1. class5 = (function() {
  2.     // private static fields
  3.     var s_first = 1;
  4.     var s_second = 2;
  5.     // private static methods
  6.     function s_method1() {
  7.         s_first++;
  8.     }
  9.     var s_second = 2;
  10.     function constructor() {
  11.         // private fields
  12.         var m_first = 1;
  13.         var m_second = 2;
  14.         // private methods
  15.         function method1() {
  16.             alert(m_first);
  17.         }
  18.         var method2 = function() {
  19.             alert(m_second);
  20.         }
  21.         // public fields
  22.         this.first = "first";
  23.         this.second = ['s','e','c','o','n','d'];
  24.         // public methods
  25.         this.method1 = function() {
  26.             s_second--;
  27.         }
  28.         this.method2 = function() {
  29.             alert(this.second);
  30.         }
  31.         // constructor
  32.         {
  33.             s_method1();
  34.             this.method1();
  35.         }
  36.     }
  37.     // public static methods
  38.     constructor.method1 = function() {
  39.         s_first++;
  40.         alert(s_first);
  41.     }
  42.     constructor.method2 = function() {
  43.         alert(s_second);
  44.     }
  45.     return constructor;
  46. })();
  47. var o1 = new class5();
  48. class5.method1();
  49. class5.method2();
  50. o1.method2();
  51. var o2 = new class5();
  52. class5.method1();
  53. class5.method2();
  54. o2.method2();

這個例子中,通過

  1. (function() {
  2.     ...
  3.     function contructor () {
  4.         ...
  5.     }
  6.     return constructor;
  7. })();

來創建了一個靜態封裝環境,實際的類是在這個環境中定義的,并且在最后通過 return 語句將最后的類返回給我們的全局變量 class5,然后我們就可以通過 class5 來引用這個帶有靜態私有成員的類了。

為了區分私有靜態成員和私有實例成員,我們在私有靜態成員前面用了 s_ 前綴,在私有實例成員前面加了 m_ 前綴,這樣避免了重名,因此在對象中總是可以存取私有靜態成員的。

但是這種命名方式不是必須的,只是推薦的,私有靜態成員可以跟私有實例成員同名,在重名的情況下,在類構造器和在類中定義的實例方法中存取的都是私有實例成員,在靜態方法(不論是公有靜態方法還是私有靜態方法)中存取的都是私有靜態成員。

在類外并且在靜態封裝環境中通過 prototype 方式定義的公有實例方法存取的是私有靜態成員。

在靜態封裝環境外定義的公有靜態方法和通過 prototype 方式定義的公有實例方法無法直接存取私有靜態成員。

另外一種方式通過直接實例化匿名函數方式來創建帶有私有靜態成員的類的例子跟上面的例子很相似:

  1. new function() {
  2.     // private static fields
  3.     var s_first = 1;
  4.     var s_second = 2;
  5.     // private static methods
  6.     function s_method1() {
  7.         s_first++;
  8.     }
  9.     var s_second = 2;
  10.     class6 = function() {
  11.         // private fields
  12.         var m_first = 1;
  13.         var m_second = 2;
  14.         // private methods
  15.         function method1() {
  16.             alert(m_first);
  17.         }
  18.         var method2 = function() {
  19.             alert(m_second);
  20.         }
  21.         // public fields
  22.         this.first = "first";
  23.         this.second = ['s','e','c','o','n','d'];
  24.         // public methods
  25.         this.method1 = function() {
  26.             s_second--;
  27.         }
  28.         this.method2 = function() {
  29.             alert(this.second);
  30.         }
  31.         // constructor
  32.         {
  33.             s_method1();
  34.             this.method1();
  35.         }
  36.     }
  37.     // public static methods
  38.     class6.method1 = function() {
  39.         s_first++;
  40.         alert(s_first);
  41.     }
  42.     class6.method2 = function() {
  43.         alert(s_second);
  44.     }
  45. };
  46. var o1 = new class6();
  47. class6.method1();
  48. class6.method2();
  49. o1.method2();
  50. var o2 = new class6();
  51. class6.method1();
  52. class6.method2();
  53. o2.method2();

這個例子的結果跟通過第一種方式創建的例子是相同的。只不過它的靜態封裝環境是這樣的:

  1. new function() {
  2.    ...
  3. };

在這里,該函數沒有返回值,并且對于 class5 的定義是直接在靜態封裝環境內部通過給一個沒有用 var 定義的變量賦值的方式實現的。

當然,也完全可以在

  1. (function() {
  2.    ...
  3. })();

這種方式中,不給該函數定義返回值,而直接在靜態封裝環境內部通過給一個沒有用 var 定義的變量賦值的方式來實現帶有私有靜態成員的類的定義。

這兩種方式在這里是等價的。

2.5 靜態類

所謂的靜態類,是一種不能夠被實例化,并且只包含有靜態成員的類。

在 JavaScript 中我們通過直接實例化一個匿名函數的對象,就可以實現靜態類了。例如:

  1. class7 = new function() {
  2.     // private static fields
  3.     var s_first = 1;
  4.     var s_second = 2;
  5.     // private static method
  6.     function method1() {
  7.         alert(s_first);
  8.     }
  9.     // public static method
  10.     this.method1 = function() {
  11.         method1();
  12.         alert(s_second);
  13.     }
  14. }
  15. class7.method1();

大家會發現,class7 其實就是個對象,只不過這個對象所屬的是匿名類,該類在創建完 class7 這個對象后,就不能再被使用了。而 class7 不是一個 function,所以不能夠作為一個類被實例化,因此,這里它就相當于一個靜態類了。


你前面那位網友看了:百度 hao123 與 hao123.cn 事件-hao123.cn 歷年的“關于我們”

▲▲▲嘿,歡迎轉載傳播本站原創文章,盡量保留來源噢。▲▲▲

文章評論
標題:必填
內容:
最新22选5开奖公告