1. 簡介
JS 作為面向?qū)ο蟮囊婚T語言肢预,擁有和其他面向?qū)ο笳Z言一樣的三大特征留夜,即封裝(encapsulation)拂苹、繼承(inheritance )和多態(tài)(polymorphism )艇肴。關(guān)于繼承的概念和實(shí)現(xiàn)估灿,在本系列不在贅述,有興趣的同學(xué)可以看看JS入門難點(diǎn)解析12-原型鏈與繼承出爹。
封裝的目的是將信息隱藏庄吼,狹義的封裝是指封裝數(shù)據(jù),廣義的封裝還包括封裝實(shí)現(xiàn)严就,封裝類型和封裝變化总寻。
2. 狹義的封裝-封裝數(shù)據(jù)
這其實(shí)也是網(wǎng)上各處資料里面對封裝最常見的定義了。主要目的就是隱藏?cái)?shù)據(jù)信息盈蛮,包括屬性和方法的私有化废菱。下面我們以一個(gè)用戶對象的例子,一起來了解一下JS如何進(jìn)行數(shù)據(jù)的封裝抖誉。
2.1 公有屬性和公有方法
假設(shè)我們要開發(fā)一個(gè)網(wǎng)站殊轴,需要一個(gè)用戶對象的構(gòu)造函數(shù)。我們可能會(huì)寫如下代碼:
// version1
function User(name, age) {
// 定義用戶信息
this.name = name;
this.age = age;
// 定義用戶行為
this.sayWords = function(words){console.log(words);}
}
好了袒炉,User構(gòu)造函數(shù)定義好了旁理,我們只要傳入name,age我磁,就可以新建一個(gè)User實(shí)例了孽文,每個(gè)實(shí)例對象擁有自己的name,age夺艰,并且可以發(fā)言:
var user1 = new User('ZhangSan', '23');
console.log(user1.name); // 'ZhangSan'
user1.sayWords('hi'); // 'hi'
var user2 = new User('LiSi', '26');
console.log(user2.name); // 'LiSI'
user1.sayWords('hello'); // 'hello'
另外芋哭,每個(gè)用戶隨時(shí)可以修改自己的名字和年齡,也可以重新定義sayWords方法郁副,且互不干擾:
user1.name = 'Mr. Zhang';
console.log(user1.name); // 'Mr. Zhang';
console.log(user2.name); // 'LiSi'
user1.sayWords = function(words) {console.log(`Mr. Zhang: ${words}`);}
user1.sayWords('hi'); // 'Mr. Zhang: hi'
user2.sayWords('hello'); // 'hello'
大家看到這里其實(shí)很清楚减牺,name,age,sayWords其實(shí)就是實(shí)例屬性和實(shí)例方法拔疚,這些屬性和方法可以被實(shí)例直接訪問和調(diào)用肥隆,所以叫做公有屬性和公有方法。
實(shí)際使用中稚失,我們不會(huì)將實(shí)例方法寫在構(gòu)造函數(shù)中栋艳,因?yàn)榉椒ǖ墓δ苁且粯拥模覀儧]必要定義多次句各,而是將其放在了構(gòu)造函數(shù)的原型中吸占,像下面這樣:
// version2
function User(name, age) {
// 定義用戶信息
this.name = name; // 公有屬性
this.age = age; // 公有屬性
}
// 定義用戶行為
User.prototype.sayWords = function(words){console.log(words);} // 公有方法
需要注意的是,此時(shí)如果在實(shí)例中企圖修改sayWords方法凿宾,并不影響原型中定義的sayWords方法旬昭,而是在實(shí)例中新建了sayWords方法,并覆蓋了原型中的同名方法菌湃。
2.2 私有屬性,私有方法和特權(quán)方法
User對象在目前看來沒有什么問題遍略,但是如何去唯一識(shí)別該用戶呢惧所,用戶的name這里是可以隨意修改的昵稱,無法用來識(shí)別用戶绪杏,所以在創(chuàng)建User實(shí)例的時(shí)候下愈,我們要求用戶輸入唯一的用戶名id,并且不允許改動(dòng)蕾久。如下:
// version3
function User(name, age, id) {
// 定義用戶信息
var id = id;
this.name = name; // 公有屬性
this.age = age; // 公有屬性
}
// 定義用戶行為
User.prototype.sayWords = function(words){console.log(words);} // 公有方法
現(xiàn)在我們再來看一下
var user1 = new User('ZhangSan', '23', 'ZhangSan333');
console.log(user1.id); // undefined
user1.id = 'ZhangSan666';
console.log(user1.id); // 'ZhangSan666'
怎么回事势似,我不僅不能讀取id,反而還能隨意修改id僧著?這和需求完全相反了啊履因。其實(shí)真實(shí)原因是你不僅不能讀取id,也無法操作在構(gòu)造函數(shù)中定義的id盹愚。要驗(yàn)證這點(diǎn)很容易栅迄,首先我們提供一個(gè)方法允許用戶實(shí)例訪問該id,然后驗(yàn)證一下直接使用實(shí)例修改id是否修改了構(gòu)造函數(shù)中的id皆怕。
// version4
function User(name, age, id) {
// 定義用戶信息
var id = id;
this.name = name; // 公有屬性
this.age = age; // 公有屬性
this.getId = function() {return id;}
}
// 定義用戶行為
User.prototype.sayWords = function(words){console.log(words);} // 公有方法
再來看一下:
var user1 = new User('ZhangSan', '23', 'ZhangSan333');
console.log(user1.id); // undefined
console.log(user1.getId()); // 'ZhangSan333'
user1.id = 'ZhangSan666';
console.log(user1.id); // 'ZhangSan666'
console.log(user1.getId()); // 'ZhangSan333'
可以發(fā)現(xiàn)user1.id和user1.getId()的值并不一樣毅舆。事實(shí)上,user1.id只是新建的一個(gè)實(shí)例屬性而已愈腾,并不是構(gòu)造函數(shù)里的變量id憋活。
到這里,我們可以看到虱黄,id只能通過getId方法去訪問悦即。這里的id就是構(gòu)造函數(shù)內(nèi)部的私有屬性,getId就是特權(quán)方法。假設(shè)你要定一個(gè)不允許用戶隨意修改的方法盐欺,也可以參照私有屬性的設(shè)置方法來定義赁豆。另外,一般私有屬性和私有方法冗美,我們會(huì)約定在前面加下環(huán)線來標(biāo)識(shí):
// version5
function User(name, age, id) {
// 定義用戶信息
var _id = id; // 私有屬性
var _sayHi = function(){console.log('hi');} // 私有方法
this.name = name; // 公有屬性
this.age = age; // 公有屬性
this.getId = function() {return id;} // 特權(quán)方法
this.sayHi = function(){return _sayHi;} // 特權(quán)方法
}
// 定義用戶行為
User.prototype.sayWords = function(words){console.log(words);} // 公有方法
私有屬性和私有訪問魔种,是我們可以將一些信息封裝起來,并設(shè)置不同的訪問和操作權(quán)限粉洼。
2.3 靜態(tài)屬性和靜態(tài)方法
現(xiàn)在节预,再考慮一個(gè)新的需求。一群用戶被創(chuàng)建以后属韧,你想按某個(gè)規(guī)律去查看這群用戶的信息安拟,比如說年齡。那么宵喂,這個(gè)排序方法需要讓每個(gè)實(shí)例都擁有嗎糠赦?顯然是不需要的,我們只需要讓構(gòu)造函數(shù)對象擁有該方法即可锅棕。當(dāng)然我們?yōu)榱朔奖愦_定構(gòu)造函數(shù)拙泽,也可以給它起個(gè)名字。就像下面這樣:
// version6
function User(name, age, id) {
// 定義用戶信息
var _id = id; // 私有屬性
var _sayHi = function(){console.log('hi');} // 私有方法
this.name = name; // 公有屬性
this.age = age; // 公有屬性
this.getId = function() {return _id;} // 特權(quán)方法
this.sayHi = function(){return _sayHi;} // 特權(quán)方法
}
User.name = 'User'; // 靜態(tài)屬性
User.sortByAge = function(...arguments){return arguments.sort((a, b)=>{return a.age - b.age;})} // 靜態(tài)方法
// 定義用戶行為
User.prototype.sayWords = function(words){console.log(words);} // 公有方法
我們來一起看一下效果:
var user1 = new User('ZhangSan', '23', 'ZhangSan333');
var user2 = new User('LiSI', '26', 'LiSi666');
var user3 = new User('WangWu','19' , 'WangWu888');
console.log(User.name);
User.sortByAge(user1, user2, user3).forEach(item => {console.log(item.name);}) // 'WangWu' 'ZhangSan' 'LiSi'
當(dāng)然裸燎,有人會(huì)想這里每次實(shí)例化一個(gè)對象顾瞻,都會(huì)新建一個(gè)特權(quán)方法,浪費(fèi)空間德绿。是不是可以把特權(quán)方法也放到原型中呢荷荤?下面我們來用閉包嘗試一下:
// version7
(function() {
// 定義私有屬性和私有方法
var _id; // 私有屬性
var _sayHi = function(){console.log('hi');} // 私有方法
// 定義公有屬性
User = function(name, age, id) {
_id = id;
this.name = name; // 公有屬性
this.age = age; // 公有屬性
}
// 定義靜態(tài)屬性和靜態(tài)方法
User.name = 'User'; // 靜態(tài)屬性
User.sortByAge = function(...arguments){return arguments.sort((a, b)=>{return a.age - b.age;})} // 靜態(tài)方法
// 定義特權(quán)方法和公有方法
User.prototype.getId = function() {return _id;} // 特權(quán)方法
User.prototype.sayHi = function(){return _sayHi;} // 特權(quán)方法
User.prototype.sayWords = function(words){console.log(words);} // 公有方法
})();
我們來一起看一下效果:
var user1 = new User('ZhangSan', '23', 'ZhangSan333');
var user2 = new User('LiSI', '26', 'LiSi666');
var user3 = new User('WangWu','19' , 'WangWu888');
console.log(User.name);
User.sortByAge(user1, user2, user3).forEach(item => {console.log(item.name);}) // 'WangWu' 'ZhangSan' 'LiSi'
我們發(fā)現(xiàn)是沒有問題的。但是需要注意的一點(diǎn)是移稳,這時(shí)候這里的私有屬性和私有方法并不是實(shí)例單獨(dú)擁有蕴纳,而是所有實(shí)例共享的屬性和方法了∶朐#可以看如下代碼:
user1.getId(); // 'WangWu'
所以袱蚓,在《JS高級程序》中也把這里的私有變量和私有方法稱作靜態(tài)私有變量和靜態(tài)私有方法。其實(shí)我覺得這里的定義都是有道理的几蜻,在前面我們將靜態(tài)私有屬性和靜態(tài)私有方法掛載到構(gòu)造函數(shù)上喇潘,所有實(shí)例都無法訪問,和將靜態(tài)私有屬性和靜態(tài)私有方法被所有實(shí)例共享梭稚。本質(zhì)上颖低,都是希望這類屬性和方法記錄的是該類型的類型變量和類型方法。
3 廣義的封裝
封裝的目的是將信息隱藏弧烤,也就是說封裝并不僅僅是指數(shù)據(jù)信息的封裝忱屑,還有實(shí)現(xiàn),隱藏類型以及在設(shè)計(jì)層面上對變化的封裝。
3.1 封裝實(shí)現(xiàn)
這一點(diǎn)其實(shí)很好解釋莺戒。封裝可以使對象內(nèi)部的變化對其他對象而言是透明的伴嗡,對象只對自己的行為負(fù)責(zé)。對象之間通過暴露API接口來進(jìn)行通信从铲,其他對象和用戶不需要關(guān)心API的實(shí)現(xiàn)細(xì)節(jié)瘪校,是的對象之間的耦合變松散。你可以隨意修改一個(gè)API的實(shí)現(xiàn)名段,只要它對外表現(xiàn)的行為一致阱扬。
舉個(gè)很簡單的例子,我們封裝一個(gè)計(jì)算傳入?yún)?shù)2倍的一個(gè)例子:
// 實(shí)現(xiàn)一:
function doubleNum(num) {return num*2;}
// 實(shí)現(xiàn)二:
function doubleNum(num) {return num+num;}
不管你是用哪種實(shí)現(xiàn)方法伸辟,只要調(diào)用該API我能獲得傳入?yún)?shù)的2倍即可麻惶。
3.2 封裝類型
封裝類型是靜態(tài)語言中一種重要的封裝方式,一般而言信夫,封裝類型是通過抽象類和接口進(jìn)行的窃蹋。但是在JavaScript中,并沒有所謂的抽象類和接口静稻,也并不需要脐彩。因?yàn)镴S本身就是一門類型模糊的語言,不需要其使用類型封裝姊扔。
3.3 封裝變化
這一點(diǎn)是從設(shè)計(jì)模式的角度出發(fā),封裝在設(shè)計(jì)模式層面體現(xiàn)為封裝變化梅誓。設(shè)計(jì)模式最重要的一點(diǎn)在于恰梢,找到變化并封裝之。通過對變化的封裝梗掰,把系統(tǒng)中穩(wěn)定不變的部分和容易變化的部分分離開來嵌言,在系統(tǒng)的演變過程中,我們只需要替換那些容易變化的部分及穗,如果這些部分是已封裝好的摧茴,替換起來就相對容易。這可以最大程度地保證程序的穩(wěn)定性和可擴(kuò)展性埂陆。
參考
BOOK-《JavaScript設(shè)計(jì)模式與開發(fā)實(shí)踐》 第一部分
BOOK-《JavaScript高級程序設(shè)計(jì)》第三版 第7章
JS三大特性
JS私有變量和靜態(tài)私有變量
JS中對象中的公有方法苛白、私有方法、特權(quán)方法
百度經(jīng)驗(yàn)-js公有焚虱、私有购裙、靜態(tài)屬性和方法的區(qū)別