JavaScript實現類的private、protected、public、static以及繼承

發表於2015-08-24

基礎知識

JavaScript中的類

JavaScript實際上是一種弱型別語言,與C++和Java等語言不同。因此,在JavaScript中,沒有強調類(class)這一概念,但實際運用中,類還是很重要的,比如寫一款遊戲,如果我們不停地呼叫函式來完成建立角色,移動角色的話,那會是什麼樣的呢?可能會出現非常多的重複程式碼,因此我們需要一個類來統一這些程式碼。所謂的類,就是把程式中的程式碼分類,比如說遊戲中的關於角色的程式碼算作一類,遊戲背景算作一類,遊戲特效又是一類。這樣一來,我們對類進行操作,就不會使程式碼顯得很凌亂,冗雜。雖然Js是弱型別語言,但是也提供了類這一概率。
定義Js中的類,實際上用的是function,總所周知,這個語法其實是用來定義函式的。不用於定義函式的是,我們可以在function中通過this.xxx的方式來定義屬性和方法。比如說:

使用的時候使用new

可以看到,這樣就可以使用到我們定義的類和類中的方法了。
也許你會問this.xxx只能定義公有屬性和方法,那私有屬性和方法怎麼辦呢?這個可以用到js閉包的知識來解決:

 

可以看到,這裡的age就是一個私有屬性了。

JavaScript中的prototype

上面的程式碼美中不足的地方就是,如果一個類有很多方法,同時用到這個類的地方又有很多(也就是new出來的物件有很多),那麼用上面的程式碼就會出現記憶體佔用過剩的問題。問題的根本原因在於,每次例項化一個物件,這個類就會執行構造器裡的程式碼(以People類為例就是function People () {…}執行的程式碼),因此每當這個類被例項化的時候,這些方法和屬性就會被拷貝到例項化出來的物件中。這樣一來,就會造成“吃”記憶體的現象。
於是js中的prototype就誕生了。prototype的作用通常是給一個類新增一系列常量或者方法。 每當一個類被例項化之後,例項化出來的物件會自動獲取類的prototype中定義的方法和屬性。只不過這裡的獲取類似於C++裡面的引用,不會在記憶體裡對這些方法和屬性進行復制,而是指向這些方法和屬性。示例:

 

這種方法雖然可以節約記憶體,但是,美中不足的是,無法定義私有屬性。

類的繼承

Javascript沒有提供繼承的函式,所以只有自己寫了。這裡借用lufylegend.js中的繼承方法向大家展示如何實現繼承:

這裡的base就是繼承函式了。繼承函式的原理莫過於複製類的方法和屬性。因此,只要做到這點,就可以實現類的繼承了。可以在上面的程式碼中看見,我們通過遍歷prototype來獲取原型鏈中定義的方法和屬性。通過apply呼叫父類的構造器進行構造器中屬性和方法的複製。使用示例:

 

靜態屬性和方法的定義

靜態屬性和方法以及靜態類在js中的定義非常簡單,先來看靜態類:

這麼寫不是在定義一個Object嗎?是的,不錯,不過js中的靜態類也是可以這樣定義的。如果要新增靜態類中的方法和屬性,就可以這麼寫:

如果是要向類中新增靜態屬性或者方法,可以採用這種寫法:

 

實現一個功能豐富的類

我們在上文中提到了,節省記憶體和定義私有屬性兩者無法兼得,是啊,和“魚和熊掌不可兼得”是一個道理,在通常的使用過程中,我們需要對這兩項進行取捨。但是現在這個年代,哪有不可兼得的呢?魚和熊掌不能同時吃?當然不行……因為吃熊掌是違法的(有待考證)?不過至少雞和魚是可以同時吃的吧。
由於js沒有實現私有屬性的定義,所以這其實是一個沒有頭緒的工作,因為在標準的做法中,我們除了閉包可以阻止外部訪問,沒有別的辦法了。所以這裡我們要用點歪門邪道的方法了。

JavaScript Set/Get訪問器

什麼是set/get訪問器呢?如果你熟悉python,那麼你可以理解為@property@xxx.setter,但是簡陋的js裡也有?當然有,只不過是ES5的標準,可以採用這種寫法:

 

具體有什麼用呢?大致就是this.name屬性在被獲取的時候呼叫get訪問器,在被更改值的時候呼叫set
你可以從上面的程式碼瞭解大致的寫法,不過如果你想深究,可以參考這篇文章:http://blog.csdn.net/teajs/article/details/22738851

注意以上的這種用法會有相容性問題,瀏覽器支援情況如下:

PC端

Firefox Google Chrome Internet Explorer Opera Safari
4.0 5 9 11.6 5.1

移動端

Firefox Mobile Android IE Mobile Opera Mobile Safari Mobile
4.0 Yes 9 11.5 Yes

來自: https://developer.mozilla.org/…/defineProperty#Browser_compatibility

如何“歪門邪道”地做到禁止訪問私有和保護屬性?

這是個比較頭疼的問題,正如本節開篇所說,我們在常規開發下,只能通過閉包來阻止某變數的訪問。可是如果你使用了prototype,那麼閉包這條路就走不通了。在這種情況下,我們的Object.defineProperty就出場了。我們知道,通過這個函式可以設定獲取屬性時返回的值,也可以設定更改屬性時設定的值。有了這個函式,我們可以隨時跟蹤到某個屬性是不是在被獲取,或者是不是在被更改。我們還需要一個開關,我們在類內部的方法呼叫時,把這個開關開啟,表明是在內部執行,方法呼叫結束後將開關關閉,表明回到外部執行狀態。有了這兩個狀態,我們就可以跟蹤privateprotected屬性和方法了,一旦他們在開關關閉的時候被使用,就終止這個屬性或方法的獲取或設定。
於是乎,大難題就快解決了。

開源庫件jpp.js

秉著這個歪門邪道的思想,我把這個功能封裝到jpp.js這個庫件中,庫件的github地址如下:
https://github.com/yuehaowang/jpp.js
當然這個庫件不限於建立一個類,還可以實現函式的過載等。目前庫件還處於開發階段,歡迎各位提交建議。

使用jpp.js建立一個類

 

對上面的程式碼進行分析:
使用jpp.class函式建立一個類,函式的引數是一個Object,這個Object可新增的屬性如下:

  • extends 繼承時的父類
  • private 裝載私有屬性,裡面定義的成員外部不可使用且不能繼承給子類
  • protected 裝載保護屬性,裡面定義的成員外部不可使用但可以繼承給子類
  • public 裝載公有屬性
  • static 裝載靜態方法和屬性

在建立類的過程中,在public中新增constructor方法初始化構造器,this.super可訪問父類構造器。

執行程式碼,可以看到瀏覽器正常執行前5個alert,而最後一個執行的時候瀏覽器報錯:

具體的實現過程有點複雜,不過原理在上文已經詳細講述了。程式碼可以在github裡參看,歡迎各位研究。

相關文章