類/繼承描述了一種代碼的組織結構形式--一種在軟件中對真實世界中問題領域的建模方法。編程
面向對象編程強調的是數據和操做數據的行爲本質上是互相關聯的(固然,不一樣的數據有不一樣的行爲),所以好的設計就是把數據以及和它相關的行爲打包(或者說封裝)起來,這在正式的計算機科學中有時被稱爲數據結構。設計模式
舉例來講,用來表示一個單詞或者短語的一串字符一般被稱爲字符串。字符就是數據。可是你關心的每每不是數據是什麼,而是能夠對數據作什麼,因此能夠應用在這種數據上的行爲(計算長度、添加數據、搜索等等)都被設計成String類的方法。數據結構
全部字符串都是String類的一個實例,也就是說它是一個包裹,包含字符數據和咱們能夠應用在數據上的函數。框架
咱們來看一個常見的例子,「汽車」能夠被看做「交通工具」的一種特例,後者是更普遍的類。函數
咱們能夠在軟件中定義一個Vehicle類和一個Car類來對這種關係進行建模。工具
Vehicle的定義可能包含推動器(好比引擎)、載人能力等,這些都是Vehicle的行爲。咱們在Vehicle中定義的是(幾乎)全部類型的交通工具(飛機、火車和汽車)都包含的東西。ui
在咱們的軟件中,對不一樣的交通工具重複定義「載人能力」是沒有意義的。相反咱們只在Vehicle中定義一次,定義Car時,只要聲明它繼承(或者擴展)了Vehicle的這個基礎定義就行。Car的定義就是對通用Vehicle定義的特殊化。this
雖然Vehicle和Car會定義相同的方法,可是實例中的數據多是不一樣的,好比每輛車獨一無二的VIN(車輛識別號碼),等待。spa
這就是類、繼承和實例化。設計
類的另外一個核心概念是多態,這個概念是說父類的通用行爲能夠被子類用更特殊的行爲重寫。實際上,相對多態性容許咱們從重寫行爲中引用基礎行爲。
類理論強烈建議父類和子類使用相同的方法名來表示特定的行爲,從而讓子類重寫父類。咱們以後會看到,在JavaScript代碼中這樣作會下降代碼的可讀性和健壯性。
JavaScript屬於哪一類呢?在至關長的一段時間裏,JavaScript只有一些近似類的語法元素,(好比new和instanceof),不過在後來的ES6中新增了一些元素,好比class關鍵字。
這是否是意味着JavaScript中實際上有類呢?簡單來講,不是。
因爲類是一種設計模式,因此你能夠用一些方法近似實現類的功能。爲了知足對於類設計模式的最廣泛需求,JavaScript提供了一些近似類的語法。
雖然有近似類的語法,可是JavaScript的機制彷佛一直在阻止你使用類設計模式。在近似類的表象之下,JavaScript的機制其實和類徹底不一樣。語法糖和JavaScript「類」庫試圖掩蓋這個現實,可是你早晚會面對它:其餘語言中的類和JavaScript中的「類」並不同。
總結一下,在軟件設計中類是一種可選的模式,你須要本身決定是否在JavaScript中使用它。因爲許多開發者都很是喜歡面向類的軟件設計,以後咱們會介紹在JavaScript中實現類以及存在的一些問題。
在許多面向類的語言中,「標準庫」會提供Stack類,它是一種「棧」數據結構(支持壓入、彈出,等等)。Stack類內部會有一些變量來存儲數據,同時會提供一些公有的可訪問的行爲(「方法」),從而讓你的代碼能夠和(隱藏的)數據進行交互(好比添加、刪除數據)。
可是在這些語言中,你實際上並非直接操做Stack(除非建立一個靜態類成員引用),Stack類僅僅是一個抽象的表示,它描述了全部「棧」須要作的事,可是它自己並非一個「棧」。你必須實例化Stack類而後才能對它進行操做。
「類」和「實例」的概念來源於房屋建造。
建築師會規劃出一個建築的全部特性:多寬、多高、多少個窗戶以及窗戶的位置,甚至連建造牆和房頂須要的材料都要計劃好。在這個階段他並不須要關心建築會被建在哪,也不須要關心會建造多少個這樣的建築。
建築師也不太關心建築裏的內容--傢俱、壁紙、吊扇等--他只關心須要用什麼結構來容納它們。
建築藍圖只是建築計劃,它們並非真正的建築,咱們還須要一個建築工人來建造建築。建築工人會按照藍圖建造建築。實際上,他會把規劃好的特性從藍圖中複製到現實世界的建築中。
完成後,建築就成爲了藍圖的物理實例,本質上就是對藍圖的複製。以後建築工人就能夠到下一個地方,把全部工做都重複一遍,再建立一份副本。
建築和藍圖之間的關係是間接的。你能夠經過藍圖瞭解建築的結構,只觀察建築自己是沒法得到這些信息的。可是若是你想打開一扇門,那就必須接觸真實的建築才行--藍圖只能表示門應該在哪,但並非真正的門。
一個類就是一張藍圖。爲了得到真正能夠交互的對象,咱們必須按照類來建造(也能夠說實例化)一個東西,這個東西一般被稱爲實例,有須要的話,咱們能夠直接在實例上調用方法並訪問其全部公有數據屬性。
這個對象就是類中描述的全部特性的一份副本。
你走進一棟建築時,它的藍圖不太可能掛在牆上(儘管這個藍圖可能會保存在公共檔案館中)。相似地,你一般也不會是要一個實例對象來直接訪問並操做它的類,不過只是能夠判斷出這個實例對象來自哪一個類。
把類和實例對象之間的關係看做是直接關係而不是間接關係一般更有助於理解。類經過複製操做被實例化爲對象形式。
類實例是由一個特殊的類方法構造的,這個方法名一般和類名相同,被稱爲構造函數。這個方法的任務就是初始化實例須要的全部信息(狀態)。
舉例來講,思考下面這個關於類的僞代碼(編造出來的語法):
class CoolGuy {
specialTrick = nothing;
CoolGuy(trick) {
specialTrick = trick;
}
showOff() {
output("Here's my trick: ",specialTrick);
}
}
複製代碼
咱們能夠調用類構造函數來生成一個CoolGuy實例:
Joe = new CoolGuy('jumping rope');
Joe.showOff(); // Here's my trick: specialTrick
複製代碼
注意,CoolGuy類有一個CoolGuy()構造函數,執行new CoolGuy()時實際上調用的就是它。構造函數會返回一個對象(也就是類的一個實例),以後咱們能夠在這個對象上調用showOff()方法,來輸出指定CoolGuy的特長。
顯然,跳繩讓喬成爲了一個很是酷的傢伙。
類構造函數屬於類,並且一般和類同名。此外,構造函數大多須要用new來調,這樣語言引擎才知道你想要構造一個新的類實例。
在面向類的語言中,你能夠先定義一個類,而後定義一個繼承前者的類。
後者一般被稱爲「子類」,前者一般被稱爲「父類」。
定義好一個子類以後,相對於父類來講它就是一個獨立而且徹底不一樣的類。子類會包含父類行爲的原始副本,可是也能夠重寫全部繼承的行爲甚至定義新行爲。
接下來,講解一個稍有不一樣的例子:不一樣類型的交通工具。
首先回顧一下本章前面部分提出的Vehicle和Car類。思考下面關於繼承的僞代碼:
class Vehicle {
engines = 1
ignition() {
output('Turning on my engine.')
}
drive() {
ignition()
output('Steering and moving forward!')
}
}
class Car inherits Vehicle {
wheels = 4
dirve() {
inherited:drive()
output('Rolling on all ', wheels, "wheels!")
}
}
class SpeedBoat inherits Vehicle {
engines = 2
ignition() {
output('Turning on my ', engines, "engines.")
}
pilot() {
inherited:drive()
output('Speeding through the water with ease!')
}
}
複製代碼
咱們經過定義Vehicle類來假設一種發動機,一種點火方式,一種駕駛方法。可是你不可能製造一個通用的「交通工具」,由於這個類只是一個抽象的概念。
接下來,咱們定義了兩類具體的交通工具:Car和SpeedBoat。它們都從Vehicle繼承了通用的特性並根據自身類別修改了某些特性。汽車須要四個輪子,快艇須要兩個發動機,所以它必須啓動兩個發動機的點火裝置。
Car重寫了繼承自父類drive()方法,可是以後Car調用了inherited:drive()方法,這代表Car能夠引用繼承來的原始drive()方法。快艇的pilot()方法一樣引用了原始的drive()方法。
這個技術被稱爲多態或者虛擬多態。在本例中,更恰當的說法是相對多態。
多態是一個很是普遍的話題,咱們如今所說的「相對」只是多態的一個方面:任何方法均可以引用繼承層次中高層的方法(不管高層的方法名和當前方法名是否相同)。之因此說「相對」是由於咱們並不會定義想要訪問的絕對繼承層次(或者說類),而是使用相對引用「查找上一層」。
在許多語言中可使用super來代替本例中的inherited:,它的含義是「超類」(superclass),表示當前類的父類/祖先類。
多態的另外一個方面是,在繼承鏈的不一樣層次中一個方法名能夠被屢次定義,當調用方法時會自動選擇合適的定義。
在子類(而不是它們建立的實例對象!)中也能夠相對引用它繼承的父類,這種相對引用一般被稱爲super。
多態並不表示子類和父類有關聯,子類獲得的只是父類的一份副本。類的繼承其實就是複製。
有些面向類的語言容許你繼承多個「父類」。多重繼承意味着全部父類的定義都會被複制到子類中。
相比之下,JavaScript要簡單得多:它自己並不提供「多重繼承」功能。許多人認爲這是件好事,由於使用多重繼承的代價過高。然而這沒法阻擋開發者們的熱情,他們會嘗試各類各樣的方法來實現多重繼承,咱們立刻就會看到。
在繼承或者實例化時,JavaScript的對象機制並不會自動執行復制行爲。簡單來講,JavaScript中只有對象,並不存在能夠實例化的「類」。一個對象並不會被複制到其餘對象,它們會被關聯起來。
因爲在其餘語言中類表現出來的都是複製行爲,所以JavaScript開發者也想出了一個方法來模擬類的複製行爲,這個方法就是混入。接下來,咱們會看到兩種類型的混入:顯式和隱式。
首先咱們來回顧一下以前提到的Vehicle和Car。因爲JavaScript不會自動實現Vehicle到Car的複製行爲,因此咱們須要手動實現複製功能。這個功能在許多庫和框架中被稱爲extend(...),可是爲了方便理解咱們稱之爲mixin(...)。
// 很是簡單的mixin(...)例子:
Function mixin(sourceObj, targetObj) {
for(var key in sourceObj) {
// 只會在key不存在的狀況下複製
if(!key in targetObj) {
targetObj[key] = sourceObj[key]
}
}
return targetObj;
}
var Vehicle = {
engines: 1,
ignition: function() {
console.log("Turning on my engine.");
},
drive: function() {
this.ignition();
console.log("Steering and moving forward!");
}
}
var Car = mixin(Vehicle,{
wheels: 4,
drive: function() {
Vehicle.drive.call(this);
console.log("Rolling on all " + this.wheels + "wheels!");
}
})
複製代碼
注意:咱們處理的已經再也不是類了,由於在JavaScript中不存在類,Vehicle和Car都是對象,供咱們分別進行復制和粘貼。
如今Car中就有了一份Vehicle屬性和函數的副本了。從技術角度來講,函數實際上沒有被複制,複製的是函數的引用。因此,Car中的屬性ignition只是從Vehicle中複製過來的對應ignition()函數的引用。相反,屬性engines就是直接從Vehicle中複製了值1。
Car中已經有了drive屬性(函數),因此這個屬性引用並無被mixin重寫,從而保留了Car中定義的同名屬性,實現了「子類」對「父類」屬性的重寫。
思考下面的代碼:
var Something = {
cool: function() {
this.greeting = "Hello World!";
this.count = this.count ? this.count + 1 : 1;
}
}
Something.cool();
Something.greeting; // "Hello World!"
Something.count; // 1
var Another = {
cool: function() {
// 隱式把Something混入Another
Something.cool.call(this);
}
}
Another.cool();
Another.greeting; // "Hello World"
Another.count; // 1(count不是共享狀態)
複製代碼
經過構造函數調用或者方法調用中使用Something.cool.call(this),咱們實際上「借用」了函數Something.cool()並在Another的上下文中調用了它。最終的結果是Something.cool()中的賦值操做都會應用在Another對象上而不是Something對象上。
所以,咱們把Something的行爲「混入」到了Another中。
雖然這類技術利用了this的從新綁定功能,可是Something.cool.call(this)仍然沒法變成相對(並且更靈活的)引用,因此使用時千萬要當心。一般來講,儘可能避免使用這樣的結構,以保證代碼的整潔和可維護性。
類是一種設計模式。許多語言提供了對於面向類軟件設計的原生語法。JavaScript也有相似的語法,可是和其餘語言中的類徹底不一樣。
類意味着複製。
傳統的類被實例化時,它的行爲會被複制到實例中。類被繼承時,行爲也會被複制到子類中。
多態(在繼承鏈的不一樣層次名稱相同可是功能不一樣的函數)看起來彷佛是從子類引用父類,可是本質上引用的實際上是複製的結果。
JavaScript並不會(像類那樣)自動建立對象的副本。
混入模式(不管顯式仍是隱式)能夠用來模擬類的複製行爲,可是一般會產生醜陋而且脆弱的語法,好比顯式僞多態(OtherObj.methodName.call(this,...)),這會讓代碼更加難懂而且難以維護。
此外,顯式混入實際上沒法徹底模擬類的複製行爲,由於對象(和函數!別忘了函數也是對象)只能複製引用,沒法複製被引用的對象或者函數自己。忽視這一點會致使許多問題。
總地來講,在JavaScript中模擬類是得不償失的,雖然能解決當前的問題,可是可能會埋下更多的隱患。
最近懶癌犯了,感受又很久沒有更新,一開始寫這個的初衷也是但願聚沙成塔,量變引發質變,但是總由於各類各樣的緣由耽擱了,我慢慢的開始發現,不少時候難以堅持,實際上是由於原本的做息被打斷,我本身是這樣的緣由,好比說,我通常九點開始寫這個,若是我下班晚了,就職性的不寫了,若是我下班早了,極可能也不寫了,由於躺着躺着就睡着了,仍是但願本身能養成習慣吧,多看書,多寫代碼。