基礎知識
JavaScript中的類
JavaScript實際上是一種弱型別語言,與C++和Java等語言不同。因此,在JavaScript中,沒有強調類(class)這一概念,但實際運用中,類還是很重要的,比如寫一款遊戲,如果我們不停地呼叫函式來完成建立角色,移動角色的話,那會是什麼樣的呢?可能會出現非常多的重複程式碼,因此我們需要一個類來統一這些程式碼。所謂的類,就是把程式中的程式碼分類,比如說遊戲中的關於角色的程式碼算作一類,遊戲背景算作一類,遊戲特效又是一類。這樣一來,我們對類進行操作,就不會使程式碼顯得很凌亂,冗雜。雖然Js是弱型別語言,但是也提供了類這一概率。
定義Js中的類,實際上用的是function
,總所周知,這個語法其實是用來定義函式的。不用於定義函式的是,我們可以在function
中通過this.xxx
的方式來定義屬性和方法。比如說:
1 2 3 4 5 6 7 |
function People () { this.name = "Yorhom"; this.getName = function () { return this.name }; } |
使用的時候使用new
:
1 2 3 |
var yorhom = new People(); // "Yorhom" alert(yorhom.getName()); |
可以看到,這樣就可以使用到我們定義的類和類中的方法了。
也許你會問this.xxx
只能定義公有屬性和方法,那私有屬性和方法怎麼辦呢?這個可以用到js閉包的知識來解決:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function People () { this.name = "Yorhom"; var age = 16; this.getName = function () { return this.name }; this.getAge = function () { return age; }; } var yorhom = new People(); // undefined alert(yorhom.age); // 16 alert(yorhom.getAge()); |
可以看到,這裡的age就是一個私有屬性了。
JavaScript中的prototype
上面的程式碼美中不足的地方就是,如果一個類有很多方法,同時用到這個類的地方又有很多(也就是new
出來的物件有很多),那麼用上面的程式碼就會出現記憶體佔用過剩的問題。問題的根本原因在於,每次例項化一個物件,這個類就會執行構造器裡的程式碼(以People類為例就是function People () {…}執行的程式碼),因此每當這個類被例項化的時候,這些方法和屬性就會被拷貝到例項化出來的物件中。這樣一來,就會造成“吃”記憶體的現象。
於是js中的prototype
就誕生了。prototype
的作用通常是給一個類新增一系列常量或者方法。 每當一個類被例項化之後,例項化出來的物件會自動獲取類的prototype
中定義的方法和屬性。只不過這裡的獲取類似於C++裡面的引用,不會在記憶體裡對這些方法和屬性進行復制,而是指向這些方法和屬性。示例:
1 2 3 4 5 6 7 8 9 10 11 |
function People () { this.name = "Yorhom"; } People.prototype.getName = function () { return this.name; }; var yorhom = new People(); // "Yorhom" alert(yorhom.getName()); |
這種方法雖然可以節約記憶體,但是,美中不足的是,無法定義私有屬性。
類的繼承
Javascript沒有提供繼承的函式,所以只有自己寫了。這裡借用lufylegend.js中的繼承方法向大家展示如何實現繼承:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function base (d, b, a) { var p = null, o = d.constructor.prototype, h = {}; for (p in o) { h[p] = 1; } for (p in b.prototype) { if (!h[p]) { o[p] = b.prototype[p]; } } b.apply(d, a); } |
這裡的base就是繼承函式了。繼承函式的原理莫過於複製類的方法和屬性。因此,只要做到這點,就可以實現類的繼承了。可以在上面的程式碼中看見,我們通過遍歷prototype
來獲取原型鏈中定義的方法和屬性。通過apply
呼叫父類的構造器進行構造器中屬性和方法的複製。使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function People () { this.name = "Yorhom"; } People.prototype.getName = function () { return this.name; }; function Student () { base(this, People, []); } var yorhom = new Student(); // "Yorhom" alert(yorhom.getName()); |
靜態屬性和方法的定義
靜態屬性和方法以及靜態類在js中的定義非常簡單,先來看靜態類:
1 |
var StaticClass = {}; |
這麼寫不是在定義一個Object
嗎?是的,不錯,不過js中的靜態類也是可以這樣定義的。如果要新增靜態類中的方法和屬性,就可以這麼寫:
1 2 3 4 5 6 |
var StaticClass = { id : 5, sayHello : function () { alert("Hello"); } }; |
如果是要向類中新增靜態屬性或者方法,可以採用這種寫法:
1 2 3 4 5 6 7 8 9 10 11 12 |
function People () { this.name = "Yorhom"; } People.prototype.getName = function () { return this.name; }; People.TYPE = "people"; People.sayHello = function () { alert("Hello"); }; |
實現一個功能豐富的類
我們在上文中提到了,節省記憶體和定義私有屬性兩者無法兼得,是啊,和“魚和熊掌不可兼得”是一個道理,在通常的使用過程中,我們需要對這兩項進行取捨。但是現在這個年代,哪有不可兼得的呢?魚和熊掌不能同時吃?當然不行……因為吃熊掌是違法的(有待考證)?不過至少雞和魚是可以同時吃的吧。
由於js沒有實現私有屬性的定義,所以這其實是一個沒有頭緒的工作,因為在標準的做法中,我們除了閉包可以阻止外部訪問,沒有別的辦法了。所以這裡我們要用點歪門邪道的方法了。
JavaScript Set/Get訪問器
什麼是set/get訪問器呢?如果你熟悉python,那麼你可以理解為@property
和@xxx.setter
,但是簡陋的js裡也有?當然有,只不過是ES5的標準,可以採用這種寫法:
1 2 3 4 5 6 7 8 9 |
Object.defineProperty(this, "name", { get : funtion () { return name; }, set : function (v) { name = v; } }); |
具體有什麼用呢?大致就是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
就出場了。我們知道,通過這個函式可以設定獲取屬性時返回的值,也可以設定更改屬性時設定的值。有了這個函式,我們可以隨時跟蹤到某個屬性是不是在被獲取,或者是不是在被更改。我們還需要一個開關,我們在類內部的方法呼叫時,把這個開關開啟,表明是在內部執行,方法呼叫結束後將開關關閉,表明回到外部執行狀態。有了這兩個狀態,我們就可以跟蹤private
和protected
屬性和方法了,一旦他們在開關關閉的時候被使用,就終止這個屬性或方法的獲取或設定。
於是乎,大難題就快解決了。
開源庫件jpp.js
秉著這個歪門邪道的思想,我把這個功能封裝到jpp.js這個庫件中,庫件的github地址如下:
https://github.com/yuehaowang/jpp.js
當然這個庫件不限於建立一個類,還可以實現函式的過載等。目前庫件還處於開發階段,歡迎各位提交建議。
使用jpp.js建立一個類
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
var People = jpp.class({ extends : null, private : { id : null, hobby : null }, protected : { money : null, phoneNumber : null }, public : { firstName : null, lastName : null, age : null, birthday : null, occupation : null, constructor : function (name, id) { if (name) { var nameArray = name.split(" "); this.firstName = nameArray[0]; this.lastName = nameArray[1]; } if (id) { this.id = id; } }, setBirthday : function (date) { if (date) { this.birthday = date; } }, getBirthday : function () { return this.birthday; }, askForId : function () { return this.id; }, findHobby : function () { return this.hobby; } }, static : { OCCUPATION_PROGRAMMER : "programmer", OCCUPATION_ARTIST : "artist", OCCUPATION_MUSICIAN : "musician", OCCUPATION_STUDENT : "student" } }); var peter = new People("Peter Wong", 543232123565); peter.occupation = People.OCCUPATION_PROGRAMMER; peter.setBirthday("19980727"); // result: Peter alert(peter.firstName); // result: 19990727 alert(peter.getBirthday()); // result: 51092028 alert(peter.askForId()); // result: null alert(peter.findHobby()); // result: programmer alert(peter.occupation); // error alert(peter.id); |
對上面的程式碼進行分析:
使用jpp.class
函式建立一個類,函式的引數是一個Object,這個Object可新增的屬性如下:
- extends 繼承時的父類
- private 裝載私有屬性,裡面定義的成員外部不可使用且不能繼承給子類
- protected 裝載保護屬性,裡面定義的成員外部不可使用但可以繼承給子類
- public 裝載公有屬性
- static 裝載靜態方法和屬性
在建立類的過程中,在public
中新增constructor
方法初始化構造器,this.super
可訪問父類構造器。
執行程式碼,可以看到瀏覽器正常執行前5個alert
,而最後一個執行的時候瀏覽器報錯:
具體的實現過程有點複雜,不過原理在上文已經詳細講述了。程式碼可以在github裡參看,歡迎各位研究。