JS進(jìn)階系列02-JS面向?qū)ο蟮娜筇卣髦庋b

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ū)別

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市鹃栽,隨后出現(xiàn)的幾起案子躏率,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件薇芝,死亡現(xiàn)場離奇詭異蓬抄,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)夯到,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門嚷缭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人黄娘,你說我怎么就攤上這事峭状。” “怎么了逼争?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵优床,是天一觀的道長。 經(jīng)常有香客問我誓焦,道長胆敞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任杂伟,我火速辦了婚禮移层,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘赫粥。我一直安慰自己观话,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布越平。 她就那樣靜靜地躺著频蛔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪秦叛。 梳的紋絲不亂的頭發(fā)上晦溪,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天,我揣著相機(jī)與錄音挣跋,去河邊找鬼三圆。 笑死,一個(gè)胖子當(dāng)著我的面吹牛避咆,可吹牛的內(nèi)容都是我干的舟肉。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼查库,長吁一口氣:“原來是場噩夢啊……” “哼度气!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起膨报,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤磷籍,失蹤者是張志新(化名)和其女友劉穎适荣,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體院领,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡弛矛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了比然。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丈氓。...
    茶點(diǎn)故事閱讀 38,064評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖强法,靈堂內(nèi)的尸體忽然破棺而出万俗,到底是詐尸還是另有隱情,我是刑警寧澤饮怯,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布闰歪,位于F島的核電站,受9級特大地震影響蓖墅,放射性物質(zhì)發(fā)生泄漏库倘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一论矾、第九天 我趴在偏房一處隱蔽的房頂上張望教翩。 院中可真熱鬧,春花似錦贪壳、人聲如沸饱亿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽路捧。三九已至,卻和暖如春传黄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背队寇。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工膘掰, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人佳遣。 一個(gè)月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓识埋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親零渐。 傳聞我的和親對象是個(gè)殘疾皇子窒舟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容