推薦一下,如果有客倌也想好好仔細的研究一下JavaScript的物件導向,可以參考這兩篇文章哦!
這兩篇文章真的寫的很好,完全把JavaScript的物件導向的理論和應用作一個徹底的分析!
以下就針對在物件導向的世界裡,JavaScript所提供的一些方法範例來做說明:
1. 「變數定義」
2.「變數類型」
3.「變數作用域(範圍鏈)」
4.封裝
1).「私有物件成員」
2).「公有物件成員」
3).「公有類別成員」
幾個基本概念
變數定義
在 JavaScript 語言中,是通過 var 關鍵字來定義變數的。
但是如果我們直接給一個沒有使用 var 定義的變數賦值,那麼這個變數就會成為全局變數。
一般情況下,我們應該避免使用沒有用 var 定義的變數,主要原因是它會影響程式的執行效率,因為存取全局變數速度比局部變數要慢得多。
但是這種用法可以保證我們的變數一定是全局變數。
另外,為了「保證速度」,我們在使用全局變數時,可以經由「 var 定義一個局部變數」,然後將全局變數賦予之,由此可以得到一個全局變數的局部引用。
變數類型
沒有定義的變數,類型為 undefined。
變數的值可以是函數。
函數在 JavaScript 中可以充當類別的角色。
變數作用域(範圍鏈)
「變數作用域(範圍鏈)」是指「變數生存週期的有效範圍」。(請參考大爺的另一篇文章:Javascript 函數整理裡面會有提到關於範圍鏈的一些概念。)
單純用 { } 建立的區塊「不能新增作用域」。
with 將它包含的對象作用域添加到當前作用域鏈(範圍鏈)中,但 with 不創建新的作用域(範圍鏈)。with 塊結束後,會將對象作用域從當前作用域鏈(範圍鏈)中刪除。
try-catch 中,catch 的錯誤對象只在 catch 塊中有效,但 catch 塊中定義的變數屬於當前作用域(範圍鏈)。
其它如 if、for、for-in、while、do-while、switch 等控制語法建立的區塊也不能建立新的作用域(範圍鏈)。
用 function 建立的函數,會新增一個新的作用域(範圍鏈)添加到當前作用域(範圍鏈)中。
封裝
下面我們就來討論具體的封裝。首先說一下大家最熟悉的幾種封裝:私有物件成員、公有物件成員和公有靜態成員。最後會討論一下大家所不熟悉的「私有靜態成員」和「靜態類別」的封裝辦法。因為下面要討論的是物件導向程式,所有當函數作為類別來定義和使用時,我們暫且將其成為類別。
宣告「私有」物件成員
範例1,宣告私有物件成員範例,
- //因為沒有加上var,所以是全域的物件
- class1 = function(){
- //宣告私有物件變數
- var p_first=1;
- var p_second=2;
- //宣告私有物件方法(一),匿名方法
- function get_p_first(){
- alert("p_first =" + p_first);
- }
- //宣告私有物件方法(二),將方法指定給一個變數
- var get_p_second = function(){
- alert("p_second = " + p_second);
- }
- //建構子
- {
- alert("init class");
- get_p_first();
- get_p_second();
- }
- }
- //新增我們定義的物件變數
- var obj1 = new class1();
- //無法存取物件成員和方法,以下會出現執行錯誤訊息
- alert(o.p_frist);
- o.get_p_first();
(一).這邊可以看到宣告物件的方法有兩種方式,一種是「匿名方法」,另一種則是「將匿名方法指定給一個私有變數」,不論是方式(一),還是方式(二),這兩種宣告的方式都差不多,都是屬於私有方法,無法從外部來存取,但都可以存取物件類的私有成員。不過第二種方法會比較靈活。
(二).這邊要注意的是「建構子必須放在最後面」,因為JavaScript是「解釋型語言」,所以程式是「由上到下的執行」。,如果建構子程式碼放在其它方法的定義前面,則執行到時會找不到要引用的方法,就會發生執行上的錯誤。
(三).在上面的範例程式可以看到我們用「大括號將建構子包圍起來」,不過這麼做其實「也是不會改變程式的作用域(範圍鏈)」,也就是說如果在這個括孤裡面定義變數,也將會是類別的私有成員,而不是區域變數,如果我們想要定義區域變數,則就要換個方式來定義建構子了:
範例2,修改建構子的宣告方式
- //這邊一樣沒有加上var,所以是全域的物件
- class1 = function(){
- //宣告私有物件變數
- var p_first=1;
- var p_second=2;
- //宣告私有物件方法(一),匿名方法
- function get_p_first(){
- alert("p_first =" + p_first);
- }
- //宣告私有物件方法(二),將方法指定給一個變數
- var get_p_second = function(){
- alert("p_second = " + p_second);
- }
- //這邊就是我們要修改的建構子,將建構子宣告成私有的匿名方法
- function constructor(){
- alert("init class");
- get_p_first();
- get_p_second();
- }
- constructor();//然後再呼叫建構子,記得建構子得在程式的最後面呼叫才不會出錯
- }
- //新增我們定義的物件變數
- var obj1 = new class1();
- //無法存取物件成員和方法,以下會出現執行錯誤訊息
- alert(o.p_frist);
- o.get_p_first();
我們可以看到,在這個範例中,大爺將原本的「建構子的區塊範圍」改成用「方法」來定義,並且在程式的最後面呼這個建構子方法。
宣告「公有」物件成員
範例3:宣告公有的物件成員 - 1
- class3 = function() {
- // private fields
- var m_first = 1;
- var m_second = 2;
- // private methods
- function method1() {
- alert("私有變數 m_first =" + m_first);
- }
- var method2 = function() {
- alert("私有變數 m_second =" + m_second);
- }
- // public fields
- this.first = "first";
- this.second = ['s','e','c','o','n','d'];
- // public methods
- this.method1 = method2; //呼叫私有方法 method2
- this.method2 = function() {
- alert("公有方法 method2 =" + this.second);
- }
- // constructor
- {
- method1();
- method2();
- }
- }
- //宣告公有物件方法的另一種方式
- class3.prototype.method3 = function() {
- alert("公有方法 method3 =" + this.first);
- }
- var o = new class3();
- o.method1();
- o.method2();
- o.method3();
- alert(o.first);
1、「prototype 方式」只應該在「類別外定義」。「this 方式」只能在「類別內定義」。
2、「prototype 方式」如果在「類別內定義」時,則在「存取私有物件成員」時,總是會「存取最後一個物件變數中的私有物件成員」。
3、「prototype 方式」定義的「公有物件成員」是建立在「類別的原型」上面的成員。而「this 方式」定義的「公有物件成員」,是「直接建立在類別的物件導向上的成員」。
基於前兩點區別,我們可以得到這樣的結論:如果要在「公有物件方法中」存取「私有的物件成員」,那麼必須用 「this 方式」來定義。
關於第三點區別,我們後面在討論繼承時再對它進行更深入的剖析。這裡只要知道有這個區別就可以了。
(三).我們還會發現,公有物件成員和私有物件成員「名字是可以相同的」,這樣不會有衝突嗎?
當然不會。原因在於它們的「存取方式不同」,公有物件成員在類別中存取時,必須要使用「關鍵子this.」來引用。而「私有物件成員」在類別中存取時,不使用也不能夠使用關鍵字 this. 來存取。而在「類別外」存取時,只有「公有成員」是可以用「類別的物件變數」來存取的,而私有成員是被無法存取。
範例3:宣告公有的物件成員 -2
- //公有成員
- class2 = function(){
- //宣告私有物件變數
- var pri_first=1;
- var pri_second=2;
- //宣告私有物件方法(一),匿名方法
- function get_pri_first(){
- alert("pri_first = " + pri_first);
- }
- //宣告私有物件方法(二),將方法指定給一個變數
- var get_pri_second = function(){
- alert("pri_second = " + pri_second);
- }
- //宣告公有物件變數
- this.pub_first = "first public variable";
- this.pub_second = ['s','e','c','o','n','d'];
- //宣告公有物件方法
- this.pub_first_method = get_pri_first; //呼叫匿名私有方法get_pri_first
- this.pub_second_method = get_pri_second //呼叫私有方法變數get_pri_first
- this.pub_third_method = function(){
- alert("公有方法 pub_third_method = " + this.pub_second);
- }
- //宣告一個名叫建構子的方法
- function constructor(){
- alert("init class");
- get_pri_first();
- get_pri_second();
- }
- //呼叫建構子
- constructor();
- }
- //宣告公有物件方法的另一種方式
- class2.prototype.pub_forth_method = function() {
- alert("公有方法 pub_forth_method =" + this.pub_first);
- }
- //新增定義的物件變數
- var class2obj=new class2();//新增物件
- class2obj.pub_first_method();//呼叫公有方法
- class2obj.pub_second_method();//呼叫公有方法
- class2obj.pub_third_method();//呼叫公有方法
- class2obj.pub_forth_method();//呼叫公有方法
- class2obj.this.pub_first;//呼叫公有變數
- //以下會發生錯誤
- class2obj.pri_first//呼叫私有變數,發生錯誤
- class2obj.this.pri_first;//呼叫私有變數,發生錯誤
這個例子和上面的那個例子大同小異,大爺只是將變數換了名稱,其餘的大致相同。
宣告公有「靜態」成員
範例4:宣告公有「靜態」成員
- class4 = function() {
- //private fields
- var m_first = 1;
- var m_second = 2;
- // private methods
- function method1() {
- alert(m_first);
- }
- var method2 = function() {
- alert(m_second);
- }
- //constructor
- {
- method1();
- method2();
- }
- }
- //宣告公有靜態成員
- class4.field1 = 1; //宣告公有靜態變數
- //宣告公有靜態方法
- class4.method1 = function() {
- alert(class4.field1);
- }
- class4.method1(); //呼叫公有靜態方法,不需新增物件就可直接引用
上面的這個範例是參考JavaScript 物件導向程式設計-封裝這個網站裡面的範例,並且加上大爺自已的註釋。讓我們好好來研究一下這個程式吧。
這個例子和上面的 class1 很像。不同的是在範例的後面,我們定義了一個「靜態變數」和「靜態方法」。
定義靜態成員的方式就是「類別名稱.成員名稱」( className.memberName )來宣告。
這裡定義的「靜態變數」和「靜態方法」都是可以直接使用「類別名稱」引用來存取的(在這個範範例就是class4),而不需要建立物件。因此它們是公有靜態成員。
不過有點要記住,千萬不要將「公有靜態成員」定義在「它的類別內部」,否則會得到「非預期的結果」。我們可以看下面這個例子:
- class4 = function() {
- // private fields
- var m_first = 1;
- var m_second = 2;
- var s_second = 2;
- // private methods
- function method1() {
- alert(m_first);
- }
- var method2 = function() {
- alert(m_second);
- }
- class4.method1 = function() {
- s_second++;
- }
- class4.method2 = function() {
- alert(s_second);
- }
- }
- var o1 = new class4();
- class4.method2(); // 顯示s_second=2
- class4.method1(); //s_second + 1
- class4.method2(); //s_second = 3
- var o2 = new class4();
- class4.method2(); // 顯示s_second 依然等於 2 ,並沒有依照預期的等於3
- class4.method1(); //s_second + 1
- class4.method2(); //s_second = 3
上面的這個範例是參考JavaScript 物件導向程式設計-封裝這個網站裡面的範例,並且加上大爺自已的註釋。讓我們好好來研究一下這個程式吧。
這個例子中,我們期望 s_second 能夠扮演一個「私有靜態成員」的角色,但是輸出結果卻不是我們所期望的。我們會發現 s_second 實際上是 class4 的一個「私有物件成員」,而不是「私有靜態成員」。而 class4 的 method1 和 method2 所存取的私有成員總是類別的最後一個物件變數中的這個私有物件成員。
那到底問題是出在哪呢?
問題出在每次經由 new class4() 建立一個物件時,class4 中的所有程式都會重新執行,因此,s_second 被重置了,並成為新的物件中的一個「私有物件成員」,而不是我們預期的「私有靜態成員」。而 class4.method1 和 class4.method2 也被重新定義了,而這個定義也將它們的變數作用域(範圍鏈)切換到了最後一個變數上來。這與把經由prototype 方式建立的公有物件方法定義在類別的內部而產生的錯誤是一樣的。
所以,一定不要將「公有靜態成員」定義在它所在的類別的內部!也不要把經由「prototype 方式」建立的「公有物件方法」定義在類別的內部!
私有靜態成員 (closure)
(請參考大爺的另一篇文章:Javascript 函數整理裡面會有提到關於closure的一些概念。)
那要如何定義一個私有靜態成員呢?
前面在基本概念裡我們已經清楚了,只有用「function 建立函數」,才能建立一個「新的作用域(範圍鏈)」,而要建立私有成員(不論是靜態成員,還是實例成員),都需要經由「建立新的作用域(範圍鏈)才能夠起到資料隱藏的目的」。下面改採用的方法就是基於這一點來實現的。
實現私有靜態成員是經由「建立一個匿名函數函數」來創建一個新的作用域(範圍鏈)來實現的。
通常我們使用匿名函數時都是將它指定值給一個變數,然後經由這個變數引用該匿名函數。這種情況下,該匿名函數可以被反覆引用或者作為類別去建立變數。
而在這裡,我們建立的匿名函數不指定給任何變數,在它建立後立即執行,或者立即實例化為一個物件,並且該物件也不指定值給任何變數,這種情況下,該函數本身或者它實例化後的物件都不能夠被再次存取,因此它唯一的作用就是建立了一個新的作用域(範圍鏈),並且隔離了它內部的所有區域變數和函數。因此,這些區域變數和函數就成了我們所需要的「私有靜態成員」。而這個「立即執行的匿名函數或者立即實例化的匿名函數」我們稱它為「靜態封裝環境」。
下面我們先來看經由直接引用匿名函數方式來建立擁有私有靜態成員的類別的例子:
- class5 = (function() {
- // private static fields
- var s_first = 1;
- var s_second = 2;
- // private static methods
- function s_method1() {
- s_first++;
- }
- var s_second = 2;
- function constructor() {
- // private fields
- var m_first = 1;
- var m_second = 2;
- // private methods
- function method1() {
- alert(m_first);
- }
- var method2 = function() {
- alert(m_second);
- }
- // public fields
- this.first = "first";
- this.second = ['s','e','c','o','n','d'];
- // public methods
- this.method1 = function() {
- s_second--;
- }
- this.method2 = function() {
- alert(this.second);
- }
- //類別建構子,建立物件會先從這裡開始執行
- {
- s_method1();
- this.method1();
- }
- }
- // public static methods
- constructor.method1 = function() {
- s_first++;
- alert(s_first);
- }
- constructor.method2 = function() {
- alert(s_second);
- }
- return constructor;
- })();
- var o1 = new class5(); //新增物件。 執行建構子 s_first++ 和 s_second-- 。s_first=2 、 s_second=1
- class5.method1(); //呼叫公有靜態方法 constructor.method1。 s_first=3
- class5.method2(); //呼叫公有靜態方法 constructor.method2。 s_second = 1
- o1.method2(); //呼叫公有方法 method2。 顯示 's','e','c','o','n','d'
- var o2 = new class5(); //新增物件。 執行建構子 s_first++ 和 s_second-- 。s_first=5、 s_second=0
- class5.method1(); //呼叫公有靜態方法 constructor.method1。 s_first=5
- class5.method2(); //呼叫公有靜態方法 constructor.method2。 s_second = 0
- o2.method2(); //呼叫公有方法 method2。 顯示 's','e','c','o','n','d'
上面的這個範例是參考JavaScript 物件導向程式設計-封裝這個網站裡面的範例,並且加上大爺自已的註釋。讓我們好好來研究一下這個程式吧。
這個例子中,經由
- (function() {
- ...
- function contructor () {
- ...
- }
- return constructor;
- })();
為了區分私有靜態成員和私有實例成員,我們在私有靜態成員前面用了 s_ 前綴,在私有實例成員前面加了 m_ 前綴,這樣避免了重名,因此在對象中總是可以存取私有靜態成員的。
但是這種命名方式不是必須的,只是推薦的,私有靜態成員可以跟私有實例成員同名,在重覆命名的情況下,在「類別建構子」和在「類別中」定義的「物件方法中」存取的都是「私有物件成員」,在「靜態方法(不論是公有靜態方法還是私有靜態方法)」中存取的都是「私有靜態成員」。
這邊要注意以下兩點:
(一).在「類別外(constructor)」並且「在靜態封裝(closure)環境中」經由「prototype 方式」定義的「公有物件方」法存取的是「私有靜態成員」。
(二).在「靜態封裝環境外(closure)」定義的「公有靜態方法」和經由「prototype 方式」定義的「公有物件方法」無法直接存取「私有靜態成員」。
另外一種方式經由直接「實例化匿名函數」方式來建立擁有「私有靜態成員」的類別的例子跟上面的例子很相似:
- new function() {
- // private static fields
- var s_first = 1;
- var s_second = 2;
- // private static methods
- function s_method1() {
- s_first++;
- }
- var s_second = 2;
- class6 = function() {
- // private fields
- var m_first = 1;
- var m_second = 2;
- // private methods
- function method1() {
- alert(m_first);
- }
- var method2 = function() {
- alert(m_second);
- }
- // public fields
- this.first = "first";
- this.second = ['s','e','c','o','n','d'];
- // public methods
- this.method1 = function() {
- s_second--;
- }
- this.method2 = function() {
- alert(this.second);
- }
- // constructor
- {
- s_method1();
- this.method1();
- }
- }
- // public static methods
- class6.method1 = function() {
- s_first++;
- alert(s_first);
- }
- class6.method2 = function() {
- alert(s_second);
- }
- };
- var o1 = new class6();
- class6.method1();
- class6.method2();
- o1.method2();
- var o2 = new class6();
- class6.method1();
- class6.method2();
- o2.method2();
這個例子的結果跟經由第一種方式創建的例子是相同的。只不過它的靜態封裝環境是這樣的:
- new function() {
- ...
- };
在這裡,該函數沒有返回值,並且對於 class6 的定義是直接在靜態封裝環境內部經由一個沒有用 var 宣告的變數的方式來實作的。
當然,也完全可以在
- (function() {
- ...
- })();
這種方式中,不給該函數定義返回值,而直接在靜態封裝環境內部中經由給一個沒有用 var 宣告的變數指定的方式來實作擁有私有靜態成員的類別的定義。
這兩種方式在這裡是相同的。
靜態類別
所謂的靜態類別,是一種不能夠被實例化,並且「只包含有靜態成員」的類別。
在 JavaScript 中我們經由直接「實例化一個匿名函數的變數」,就可以實現靜態類別了。例如:
- class7 = new function() {
- // private static fields
- var s_first = 1;
- var s_second = 2;
- // private static method
- function method1() {
- alert(s_first);
- }
- // public static method
- this.method1 = function() {
- method1();
- alert(s_second);
- }
- }
- class7.method1();
大家會發現,class7 其實就是個「物件」,只不過這個物件所屬的是「匿名類別」,該類別在建立完 class7 這個物件後,就不能再被使用了。而 class7 不是一個 function,所以不能夠作為一個類別被實例化,因此,這裡它就相當於一個「靜態類別」了。
留言列表