封裝(Encapsulation)就是把對象的內部屬性和方法隱藏起來,外部代碼訪問該對象只能通過特定的介面訪問,這也是面向介面編程思想的一部分。 ...
本章預設大家已經看過作者的前一篇文章 《JavaScript面向對象輕鬆入門之抽象》
為什麼要封裝?
封裝(Encapsulation)就是把對象的內部屬性和方法隱藏起來,外部代碼訪問該對象只能通過特定的介面訪問,這也是面向介面編程思想的一部分。
封裝是面向對象編程里非常重要的一部分,讓我們來看看沒有封裝的代碼是什麼樣的:
1 function Dog(){ 2 this.hairColor = '白色';//string 3 this.breed = '貴賓';//string 4 this.age = 2;//number 5 } 6 var dog = new Dog(); 7 console.log(dog.breed);//log: '貴賓'
看似沒有什麼問題,但如果breed屬性名修改了怎麼辦?比如換成this.type = ‘貴賓’,那所有使用Dog類的代碼都要改變。
如果類的代碼和使用類的代碼都是你寫的,並且使用這個類的地方不多,你這麼寫無所謂。
但如果使用這個類的地方比較多,或者協同開發時其它人還要使用你的類,那這樣做就會讓代碼很難維護,正確的做法是:
1 function Dog(){ 2 this.hairColor = '白色';//string 3 this.age = 2;//number 4 this._breed = '貴賓';//string 5 } 6 Dog.prototype.getBreed = function(){ 7 return this._breed; 8 } 9 Dog.prototype.setBreed = function(val){ 10 this._breed = val; 11 } 12 var dog = new Dog(); 13 console.log(dog.getBreed());//log: '貴賓' 14 dog.setBreed('土狗');
getBreed()就是介面,如果內部的屬性變化了,比如breed換成了type ,那隻需要改變getBreed()里的代碼就可以了,並且你可以監聽到所有獲取這個屬性的操作。
所以封裝有很多好處:
1、只要介面不改變,內部的實現可以任意改變;
2、使用者使用起來很方便,不用關係內部是如何實現;
3、降低代碼之間的耦合;
4、滿足大型應用程式和多人協同開發;
用getter/setter來封裝私有屬性
其實還有另一種封裝屬性的方法,那就是用getter/setter,如下demo,本章不講原理,只講使用,原理可自行查資料:
1 function Dog(){ 2 this.hairColor = '白色';//string 3 this.age = 2;//number 4 this._breed = '貴賓';//string 5 Object.defineProperty(this, 'breed', {//傳入this和屬性名 6 get : function () { 7 console.log('監聽到了有人調用這個get breed') 8 return this._breed; 9 }, 10 set : function (val) { 11 this._breed = val; 12 /* 13 如果不設置setter的話預設這個屬性是不可設置的 14 但有點讓人詬病的是,瀏覽器並不會報錯 15 所以即使你想讓breed是只讀的,你也應該設置一個setter讓其拋出錯誤: 16 throw 'attribute "breed" is read only!'; 17 */ 18 } 19 }); 20 } 21 var dog = new Dog(); 22 console.log(dog.breed); 23 /*log: 24 '監聽到了有人調用這個get breed介面' 25 '貴賓' 26 */ 27 dog.breed = '土狗'; 28 console.log(dog.breed); 29 /*log: 30 '監聽到了有人調用這個get breed介面' 31 '土狗' 32 */
但這種方法寫起來比較繁瑣,作者一般是用getBreed()這種方法,getter/setter一般用在readonly的屬性和一些比較重要的介面,以及重構沒有封裝介面的屬性操作。
還可以用閉包封裝私有屬性,是最安全的,但會產生額外的記憶體開銷,所以作者不是很喜歡用,大家可自行瞭解。
公有/私有概念
前兩小節我們簡單的瞭解了下封裝,但這些肯定是不夠用的,下麵的我們先來瞭解下幾個概念:
私有屬性:即只能在類的內部調獲取、修改的屬性,不允許外部訪問。
私有方法:僅供類內部調用的方法,禁止外部調用。
公有屬性:可供類外部獲取、修改的屬性。理論上講類的所有屬性都應該是私有屬性,只能通過封裝的介面訪問,但一些比較小的類,或者使用次數比較少的類,你覺得比較方便的話也可以不封裝介面。
公有方法:可供外部調用的方法,實現介面的方法如getBreed()就是公有方法,以及對外暴露的行為方法。
靜態屬性、靜態方法:類本身的屬性和方法。這個就沒必要區分公有私有了,所有的靜態屬性、靜態方法都必須是私有的,一定要通過封裝介面訪問,這也是上一章中作者為什麼要用getInstanceNumber()來訪問Dog.instanceNumber屬性。
ES5 demo如下:
1 function Dog(){ 2 /*公有屬性*/ 3 this.hairColor = null;//string 4 this.age = null;//number 5 /*私有屬性,人們共同約定私有屬性、私有方法前面加上_以便區分*/ 6 this._breed = null;//string 7 this._init(); 8 /*屬性的初始化最好放一個私有方法里,構造函數最好只用來聲明類的屬性和調用方法*/ 9 Dog.instanceNumber++; 10 } 11 /*靜態屬性*/ 12 Dog.instanceNumber = 0; 13 /*私有方法,只能類的內部調用*/ 14 Dog.prototype._init = function(){ 15 this.hairColor = '白色'; 16 this.age = 2; 17 this._breed = '貴賓'; 18 } 19 /*公有方法:獲取屬性的介面方法*/ 20 Dog.prototype.getBreed = function(){ 21 console.log('監聽到了有人調用這個getBreed()介面') 22 return this._breed; 23 } 24 /*公有方法:設置屬性的介面方法*/ 25 Dog.prototype.setBreed = function(breed){ 26 this._breed = breed; 27 return this; 28 /*這是一個小技巧,可以鏈式調用方法,只要公有方法沒有返回值都建議返回this*/ 29 } 30 /*公有方法:對外暴露的行為方法*/ 31 Dog.prototype.gnawBone = function() { 32 console.log('這是本狗最幸福的時候'); 33 return this; 34 } 35 /*公有方法:對外暴露的靜態屬性獲取方法*/ 36 Dog.prototype.getInstanceNumber = function() { 37 return Dog.instanceNumber;//也可以this.constructor.instanceNumber 38 } 39 var dog = new Dog(); 40 console.log(dog.getBreed()); 41 /*log: 42 '監聽到了有人調用這個getBreed()介面' 43 '貴賓' 44 */ 45 /*鏈式調用,由於getBreed()不是返回this,所以getBreed()後面就不可以鏈式調用了*/ 46 var dogBreed = dog.setBreed('土狗').gnawBone().getBreed(); 47 /*log: 48 '這是本狗最幸福的時候' 49 '監聽到了有人調用這個getBreed()介面' 50 */ 51 console.log(dogBreed);//log: '土狗' 52 console.log(dog);
ES6 demo(新手可不看ES6和TypeScrpt實現部分):
1 class Dog{ 2 constructor(){ 3 this.hairColor = null;//string 4 this.age = null;//number 5 this._breed = null;//string 6 this._init(); 7 Dog.instanceNumber++; 8 } 9 _init(){ 10 this.hairColor = '白色'; 11 this.age = 2; 12 this._breed = '貴賓'; 13 } 14 get breed(){ 15 /*其實就是通過getter實現的,只是ES6寫起來更簡潔*/ 16 console.log('監聽到了有人調用這個get breed介面'); 17 return this._breed; 18 } 19 set breed(breed){ 20 /*跟ES5一樣,如果不設置的話預設breed無法被修改,而且不會報錯*/ 21 console.log('監聽到了有人調用這個set breed介面'); 22 this._breed = breed; 23 return this; 24 } 25 gnawBone() { 26 console.log('這是本狗最幸福的時候'); 27 return this; 28 } 29 getInstanceNumber() { 30 return Dog.instanceNumber; 31 } 32 } 33 Dog.instanceNumber = 0; 34 var dog = new Dog(); 35 console.log(dog.breed); 36 /*log: 37 '監聽到了有人調用這個get breed介面' 38 '貴賓' 39 */ 40 dog.breed = '土狗';//log:'監聽到了有人調用這個set breed介面' 41 console.log(dog.breed); 42 /*log: 43 '監聽到了有人調用這個get breed介面' 44 '土狗' 45 */
ES5、ES6中雖然我們把私有屬性和方法用“_”放在名字前面以區分,但外部還是可以訪問到屬性和方法的。
TypeScrpt中就比較規範了,可以聲明私有屬性,私有方法,並且外部是無法訪問私有屬性、私有方法的:
1 class Dog{ 2 public hairColor: string; 3 readonly age: number;//可聲明只讀屬性 4 private _breed: string;//雖然聲明瞭private,但還是建議屬性名加_以區分 5 static instanceNumber: number = 0;//靜態屬性 6 constructor(){ 7 this._init(); 8 Dog.instanceNumber++; 9 } 10 private _init(){ 11 this.hairColor = '白色'; 12 this.age = 2; 13 this._breed = '貴賓'; 14 } 15 get breed(){ 16 console.log('監聽到了有人調用這個get breed介面'); 17 return this._breed; 18 } 19 set breed(breed){ 20 console.log('監聽到了有人調用這個set breed介面'); 21 this._breed = breed; 22 } 23 public gnawBone() { 24 console.log('這是本狗最幸福的時候'); 25 return this; 26 } 27 public getInstanceNumber() { 28 return Dog.instanceNumber; 29 } 30 } 31 let dog = new Dog(); 32 console.log(dog.breed); 33 /*log: 34 '監聽到了有人調用這個get breed介面' 35 '貴賓' 36 */ 37 dog.breed = '土狗';//log:'監聽到了有人調用這個set breed介面' 38 console.log(dog.breed); 39 /*log: 40 '監聽到了有人調用這個get breed介面' 41 '土狗' 42 */ 43 console.log(dog._breed);//報錯,無法通過編譯 44 dog._init();//報錯,無法通過編譯
註意事項:
1、暴露給別人的類,多個類組合成一個類時,所有屬性一定都要封裝起來;
2、如果你來不及封裝屬性,可以後期用getter/setter彌補;
3、每個公有方法,最好註釋一下含義;
4、在重要的類前面最好用註釋描述所有的公有方法;
後話
如果你喜歡作者的文章,記得收藏,你的點贊是對作者最大的鼓勵;
作者會儘量每周更新一章,下一章是講繼承;
大家有什麼疑問可以留言或私信作者,作者儘量第一時間回覆大家;
如果老司機們覺得那裡可以有不恰當的,或可以表達的更好的,歡迎指出來,我會儘快修正、完善。