【 js 基礎 】Javascript 「繼承」

【 js 基礎 】Javascript 「繼承」

 

是時候寫一寫 「繼承」了,爲何加引號,由於當你閱讀完這篇文章,你會知道,說是 繼承 實際上是不許確的。javascript

1、類
一、傳統的面向類的語言中的類:
類/繼承 描述了一種代碼的組織結構形式。舉個例子:
「汽車」能夠被看做是「交通工具」的一種特例。
咱們能夠定義一個 Vehicle 類和一個 Car 類來對這種關係進行描述。
Vehicle 的定義可能包含引擎、載人能力等,也就是 全部交通工具,好比飛機、火車和汽車等都有的通用的功能描述。
在對 Car 類進行定義的時候,重複定義「載人能力」是沒有意義的,咱們只須要聲明 Car 類繼承了 Vehicle 的這個基礎類就能夠了。 Car 其實是對通用 Vehicle 定義的特殊化。
Car 如今只是個類,當咱們把它更加形象化,好比說是 保時捷、奔馳的時候,就是一個實例化的過程。
咱們也有可能 會在 Car 中定義一個 和 Vehicle 中相同的方法,這是子類對父類針對特定方法的重寫,爲了能夠更加特殊化,更加符合對子類的描述,這個被稱做是 多態。html

以上就是 類、繼承、實例化和多態。java

再舉個例子:好比房屋的構建
建築師設計出來建築藍圖,而後由建築工人按照建築藍圖建造出真正的建築。建築就是藍圖的物理實例,本質上是對建築藍圖的複製。以後建築工人就能夠到下一個地方,把全部的工做重複一遍,再建立一份副本。
建築和藍圖之間的關係是間接的。你能夠經過藍圖瞭解建築的結構,只觀察建築自己是沒法得到這些信息的。可是若是你想打開一扇門,那就必須接觸真實的建築才行--藍圖只能表示門應該在哪,但並非真正的門。
一個類就是一張藍圖。爲了得到真正能夠交互的對象,咱們必須按照類來建造(實例化)一個東西,這個東西一般被稱爲實例。這個對象就是類中描述的全部特性的一份副本。設計模式

在傳統的面向類的語言中,類的繼承,實例化其實就是複製,用一張圖:jsp

箭頭表示複製操做。ide

子類 Bar 相對於 父類 Foo 來講是一個獨立並徹底不一樣的類。子類會包含父類行爲的副本,也能夠經過在子類中定義於父類中相同的方法名來改寫某個繼承的行爲,這種改寫不會影響父類中的方法,這兩個方法互不影響。對於 類 Bar 和 實例 b1 、b2 之間也一樣是 類經過複製操做被實例化爲對象形式。函數

 

二、javascript 中的類
然而 javascript 其實並無類的概念,可是 咱們早已經習慣用類來思考,因此 javascript 也提供了一些近似類的語法,咱們用它模擬出了類,然而這種 「類」仍是與傳統面向類語言中的類有不一樣。
在繼承和實例化的過程當中,javascript的對象機制並不會自動執行復制行爲。簡單來講,javascript中只有對象,並不存在能夠被實例化的「類」。一個對象並不會被複制到其它對象,他們只會被關聯起來,也就是複製的是實際上是引用。(對於複製引用,具體的能夠看【 js 基礎 】 深淺拷貝 一文,第一部分)工具


2、javascript 原型鏈、「類」和 繼承
一、 [[prototype]]:javascript 中的對象都有一個特殊的 [[prototype]] 內置屬性,其實就是對於其餘對象的引用。他的做用是什麼呢?
當你試圖訪問對象的屬性的時候,就會觸發對象的內置操做 [[Get]],[[Get]] 操做就是從對象中找到你要的屬性。然而他是怎麼找的呢?
例子:post

1 var testObject = {
2     a:2
3 };
4 console.log(testObject.a) // 2

在上面的代碼中,當你 console 的時候,會觸發 [[Get]] 操做,查找 testObject 中的 a 屬性。對於默認的 [[Get]]操做,第一步是檢查對象自己是否有這個屬性,若是有的話就直接使用它。第二步,若是 a 不在 testObject 中,也就是 沒法在對象自己中找到須要的屬性,就會繼續訪問對象的 [[prototype]] 鏈。學習

例子:

複製代碼
1 var anotherObject  = {
2     a : 2
3 };
4 
5 var testObject  = Object.create(anotherObject);
6 
7 console.log(testObject.a); // 2
複製代碼

Object.create() 方法會建立一個對象並把這個對象的 [[prototype]] 關聯 到指定的對象(anotherObject)。
例子中,testObject 對象的 [[prototype]] 關聯到了 anotherObject。 testObject 自己並無 a 屬性,然而仍是能夠 console 出testObject.a 爲 2,這是 [[Get]] 從 testObject 的 [[prototype]] 鏈中找到的,即 anotherObject 中的屬性a。可是假若 anotherObject中也沒有屬性 a,而且 [[prototype]]不爲空,就會繼續查找下去。這個過程會持續到找到匹配的屬性名,或者查找完整條 [[prototype]] 鏈未找到,[[Get]] 操做的返回值是 undefined 。

那麼哪裏是原型鏈的盡頭呢?
全部的普通的 [[prototype]] 鏈最終都會指向 內置的 Object.prototype 。

 

二、「類」:在上一部分的內容中,咱們已經說到,javascript 中實際上是沒有傳統意義上的「類」的,但咱們一直在試圖模仿類,主要是利用了 函數的一種特殊特性:全部的函數默認都會有一個名爲 prototype 的公有而且不可枚舉的屬性,它會指向另外一個對象。
例子:

複製代碼
1 function foo(){
2     //…
3 }
4 
5 foo.prototype; // {}
6 
7 var a = new foo();
8 Object.getPrototypeOf(a) === foo.prototype;//true
複製代碼

這個對象是在調用 new foo () 時建立的,最後會被關聯到 foo.prototype 上。就像例子中的調用 new foo () 時會建立 a ,而後 將 a 內部的 [[prototype]] 連接到 foo.prototype 所指向的對象。

在傳統的面向類的語言中,類能夠被複制屢次,每次實例化的過程都是一次複製。但在 javascript 中沒有相似的複製機制。你不能建立一個類的多個實例,只能建立多個對象,他們的 [[prototype]] 關聯的是同一個對象。由於在默認狀況下,並不會進行復制,因此這些對象之間並不會徹底失去聯繫,他們是互相關聯的。

就像上面的例子 new foo () 會生成一個新的對象,稱爲 a,這個新的對象的內部的 [[prototype]] 關聯的是 foo.prototype 對象。最後咱們獲得兩個對象,他們之間互相關聯。咱們並無真正意義上初始化一個類,實際上咱們並無從 「類」 中複製任何行爲到一個對象中,只是讓兩個對象互相關聯着。

再強調一下: 在 javascript 中,並不會將一個對象(「類」)複製到另外一個對象(「實例」),只是將它們關聯起來。看一個圖:

箭頭表示 關聯。
這個圖就表達了 [[prototype]] 機制,即 原型繼承。
可是說是 繼承實際上是不許確的,由於傳統面向類的語言中 繼承 意味着複製操做,而 javascript (默認)並不會複製對象屬性,而是在兩個對象之間建立一個關聯,這樣一個對象能夠 委託 訪問另外一個對象的屬性和函數。委託 能夠更加準確的描述 javascript 中對象的關聯機制。


三、 (原型)繼承
來看個例子:

複製代碼
 1 function foo(name){
 2     this.name = name;
 3 }
 4 
 5 foo.prototype.myName = function(){
 6     return this.name;
 7 }
 8 
 9 function bar(name,label){
10     foo.call(this,name);
11     this.label = label;
12 }
13 
14 // 建立了一個新的 bar.prototype 對象並把它關聯到了 foo.prototype。
15 bar.prototype = Object.create(foo.prototype);
16 
17 bar.prototype.myLabel = function(){
18     return this.label;
19 }
20 
21 var a = new Bar(「a」,」obj a」);
22 console.log(a.name) // 「a"
23 console.log(a.label) // "obj a"
複製代碼

聲明 function bar(){} 時,bar 會有一個默認的 .prototype 關聯到默認的對象,可是這個對象不是咱們想要的 foo.prototype 。所以 咱們經過 Object.create() 建立了一個新的對象並把它關聯到咱們但願的對象上,即 foo.prototype,直接把原始的關聯對象拋棄掉。

若是你說爲何不用下面這種方式關聯?

bar.prototype = foo.prototype

由於 這種方式並不會建立一個關聯到 foo.prototype 的新對象,它只是讓 bar.prototype 直接引用 foo.prototype 。所以當你執行 bar.prototype.myLabel 的賦值語句時會直接修改 foo.prototype 對象自己。

或者說爲何不用new?

bar.prototype = new foo();

這樣的確會建立一個關聯到 foo.prototype 的新對象。 可是它同時 也執行了對 foo 函數的調用,若是 foo 函數中有給this添加屬性、修改狀態、寫日誌等,就會影響到 bar() 的 「後代」 。

這裏補充兩點關於 new ,方便理解:

function foo(){
    console.log(「test」);
}

var a = new foo(); // test

當你執行 var a = new foo(); 也就是使用 new 來調用函數,會執行下面四步操做:
一、建立一個全新的對象
二、這個新對象會被執行 [[prtotype]] 鏈接
三、這個新對象會綁定到函數調用的 this 上
四、若是函數沒有返回值,那麼 new 表達式中的函數調用會自動返回這個新對象。

另外一點,當你執行 var a = new foo(); 時 ,console 打出 test。foo 只是個普通的函數,當使用 new 調用時,它就會創造一個新對象並賦值給 a,固然也會調用自身。

 

綜上,要建立一個合適的關聯對象,最好的方式就是用 Object.create(),這樣作也有缺點:就是建立了新對象,而後把舊對象拋棄掉,不能直接修改默認的已有對象了。
Object.create() 會建立一個 擁有空 [[prototype]] 鏈接的對象。它是 es5 新增的方法,讓咱們來看看在老的環境中如何實現它:

複製代碼
if(!Object.create){
    Object.create = function(o){
        function F(){}
        F.prototype = o;
        return new F();
    }
}
複製代碼

咱們使用了一個空函數 F,經過改寫它的 .prototype 屬性使其指向想要關聯的對象,而後再使用 new F() 來構造一個新對象來進行關聯。


3、類式繼承設計模式 和 委託設計模式
這兩種模式都是用來實現繼承,本質上也就是 關聯。

一、類式繼承設計模式:
這個應該是你們最熟悉的,主要就是運用構造函數和原型鏈實現繼承,也就是所謂的面向對象風格。

複製代碼
 1 function Foo(who){
 2     this.me = who;
 3 }
 4 
 5 Foo.prototype.identify = function(){
 6     return "i am 「 + this.me;
 7 }
 8 
 9 function Bar(who){
10     Foo.call(this,who);
11 }
12 
13 Bar.prototype = object.create(Foo.prototype);
14 
15 Bar.prototype.speak = function(){
16     alert(「Hello,」+ this.identify()+」.」);
17 }
18 
19 var b1 = new Bar(「b1」);
20 var b2 = new Bar(「b2」);
21 
22 b1.speak();
23 b2.speak();
複製代碼

子類 Bar 繼承了 父類 Foo,而後生成了 b1 和 b2 兩個實例。 b1 繼承了 Bar. prototype , Bar.prototype 繼承了 Foo.prototype。

二、 委託設計模式
對象關聯風格:

複製代碼
 1 Foo = {
 2     init:function(who){
 3         this.me = who;  
 4     },
 5     identify:function(){
 6         return 「i am」 +this.me;
 7     }
 8 };
 9 
10 Bar = Object.create(Foo);
11 Bar.speak = function(){
12     alert(「hello,」 + this.identify())
13 };
14 
15 var b1 = Object.create(Bar);
16 b1.init(「b1」);
17 var b2 = Object.create(Bar);
18 b2.init(「b2」);
19 
20 b1.speak();
21 b2.speak();
複製代碼

這段代碼一樣 利用 [[prototype]] 把 b1 委託給 Bar 並把 Bar 委託給 Foo,和上一段代碼一摸同樣,一樣實現了三個對象的關聯。


三、以上兩種模式都實現了三個對象的關聯,那麼它們的區別是什麼呢?
首先是思惟方式的不一樣:
      類式繼承設計模式:定義一個通用的父類,能夠將其命名爲 Task,在 Task 中定義全部任務都有的行爲。接着定義子類 A 和 B,他們都繼承子 Task,而且會添加一些特殊的行爲來處理對應的人物。而後你實例化子類,這些實例擁有 父類 Task 的通用方法,也擁有 子類 A 的特殊行爲。

     委託設計模式:首先定義一個名爲Task 的對象,它包含全部任務均可以使用的行爲。接着對於每一個任務 A 和 B,都會定義一個對象來存儲對應的數據和行爲。執行 任務 A 須要兩個兄弟對象(Task 和 A)協做完成,只是在須要某些通用行爲的時候 能夠容許 A 對象委託給 Task。在上面的例子中,也就是 Bar 經過 Object.create(Foo); 建立,它的 [[prototype]] 委託給了 Foo 對象。這就是一種對象關聯的風格。委託行爲意味着某些對象(Bar)在找不到屬性或者方法引用時會把這個請求委託給另外一個對象(Foo)。

相關文章
相關標籤/搜索