##緣起## 工做中須要用到Javascript,關注了Javascript中繼承複用的問題,翻閱了網上的各類關於Javascript繼承的帖子,感受大都思考略淺,並無作過多說明,簡單粗暴的告訴你實現Javascript繼承有1.2.3.4.5幾種方式,名字都叫啥,而後貼幾行代碼就算是玩了。
無論大家懂沒懂,反正我着實沒懂。
隨後研讀了《Javascript高級程序設計》的部分章節,對Javascript的繼承機制略有體會。思考以後,遂而分享而且闡述瞭如何實現抽象類、接口、多態甚至是類型轉換的思路。
##JS繼承,那就先說「繼承」## 凡是玩過一、2種面向對象的語言的人大都不難概括出繼承全部的幾個特性:javascript
##構造一個類## 說到構造一個Javascript的類,網上的說法五花八門。java
var Person = function(name, age) { this.name = name; this.age = age; }; var p = new Person("小王", 10);
注1:此爲代碼1,後面可能做爲引用。
注2: var Person = function(){};
等同於 function Persson(){}
,前一種定義函數的方式沒有名字,故而在var的後面跟上其名字,然後面function定義直接就跟了名字Person了。不過事實上我更喜歡後一種,由於能夠少寫一個var和分號。可是若是在局部做用域要定義一個臨時類,我仍是喜歡前一種,這是一種變量的方式。在局部做用域我更喜歡定義變量而不是函數或者類等結構性的東西,C語言後遺症,呵呵。
注3:其實這種構建類的方式能夠說成是經過構造器(constructor)來構造一個類。
3. 也有人說,應該用Prototype來構造一個類,簡要代碼以下:架構
var Person = function (){}; Person.prototype.name = "小王"; Person.prototype.age = 10; var p = new Person();
注4:這種構造方式,咱們能夠暫且稱之爲「用prototype」的方式來構造。
注5:此爲代碼2,後面可能做引用。
**類的構建方式雖然五花八門,可是大抵都是以上兩種或者其組合的變種。但是咱們何時用構造函數來構建?何時用prototype?何時二者結合使用呢?**要明白這個,咱們先來看看new關鍵字。
###new,你到底幹了什麼事兒?## new關鍵字在絕大多數面向對象的語言中都扮演者舉足輕重的位置,javascript中也不例外。StackOverflow上有一篇帖子關於new關鍵字的玄機,我以爲說的很好:Javascript中的new關鍵字背後到底作了什麼
翻譯以下,爲了懶得移步的童鞋,PC端的童鞋能夠直接點過去。app
- 建立一個新的簡單的Object類型的的對象;
- 把Object的內部的**[[prototype]]屬性設置爲構造函數prototype屬性。這個[[prototype]]**屬性在Object內部是沒法訪問到的,而構造函數的prototype是能夠訪問到的;
- 執行構造函數,若是構造函數中用到了this關鍵字,那就把這個this替換爲剛剛建立的那個object對象。
注6:其實某個對象的[[prototype]]**屬性在不少宿主環境中已經能夠訪問到,例如Chrome和IE10均可以,用__proto__就能夠訪問到,若是下面出現了__proto__字樣,那就表明一個對象的內部prototype。
上面說了一大通,又是構造器,又是prototype,不知所云。下面依次解釋。
###prototype### prototype屬性在構造函數中能夠訪問到,在對象中須要經過__prototype__訪問到。它究竟是什麼?prototype中定義了一個類所共享的屬性和方法。這就意味着:一旦prototype中的某個屬性的值變了,那麼全部這個類的實例的該屬性的值都變了。請看代碼:函數
function Person() { } Person.prototype.name = "小明"; var p1 = new Person(); console.log(Person.prototype); console.log(p1.__proto__); var p2 = new Person(); console.log(p1.name + "\t" + p2.name); Person.prototype.name = "小王"; console.log(p1.name + "\t" + p2.name);
注7:此爲代碼3。 輸出結果以下:
經過這個代碼3的實驗,咱們能夠得出如下結論:測試
Person {name: "小明"}
###this和構造函數### 看完了上面的new關鍵字作的第3步,咱們不可貴出,其實利用constructor的方式來構造類本質:先new一個臨時實例對象,將this關鍵字替換爲臨時實例對象關鍵字,而後使用[對象].[屬性]=xxx
的方式來構建一個對象,再將其返回。
但是這樣帶來一個問題就是:方法不被共享。
請看代碼4實驗:this
function Person() { this.name = "小明"; this.showName = function() { console.log(this.name) }; } var p1 = new Person(); var p2 = new Person(); p1.showName(); p2.showName(); p1.showName = function() { console.log("我不是小明,我是小王"); } p1.showName(); p2.showName();
注8:以上爲代碼4。
其運行結果爲:
咱們知道,類的同一個方法,應該儘可能保持共享,由於他們屬於同一個類,那麼這一個方法應該相同,因此應該保持共享,否則會浪費內存。
咱們的Person類中含有方法showName,雖然p1和p2實例屬於兩個實例對象,可是其showName卻指向了不一樣的內存塊!
這可怎麼辦?
對,請出咱們的prototype,它能夠實現屬性和方法的共享。請看代碼5實驗:prototype
function Person() { this.name = "小明"; } Person.prototype.showName = function() { console.log(this.name); } var p1 = new Person(); var p2 = new Person(); p1.showName(); p2.showName(); Person.prototype.showName = function() { console.log("個人名字是" + this.name); } p1.showName(); p2.showName();
注9:以上爲代碼5 。 運行結果以下:
這樣咱們很是完美地完成了一個類的構建,他知足:翻譯
function Person1() { } // prototype 沒有徹底被改寫 Person1.prototype.showName = function() { console.log(this.name); }; var p1 = new Person1(); console.log(p1 instanceof Person1); console.log(p1.constructor); function Person2() { } // prototype 徹底被改寫 Person2.prototype = { showName : function() { console.log(this.name); } }; var p2 = new Person2(); console.log(p2 instanceof Person2); console.log(p2.constructor);
注10:以上爲代碼6 。 運行結果以下:
經過以上代碼6的實驗,咱們能夠看出:重寫整個prototype會將對象的constructor指針直接指向了Object,從而致使了constructor不明的問題。
如何解決呢?咱們能夠經過顯示指定其constructor爲Person便可。
請看代碼7:設計
function Person2() { } // prototype 徹底被改寫 Person2.prototype = { constructor : Person2, // 顯示指定其constructor showName : function() { console.log(this.name); } }; var p2 = new Person2(); console.log(p2 instanceof Person2); console.log(p2.constructor);
注11:以上爲代碼7 。 運行結果以下:
###對象、constructor和prototype三者之間的關係 上面說了那麼多,我想你們都有點被constructor、prototype、對象搞得雲裏霧裏的,其實我剛開始也是這樣。下面我總結敘述一下這三者之間的關係,相信看了以後就會逐漸明白的:
###Javascript的Object架構###
解釋以下:
var f1 = new Foo();
建立了一個Foo對象;__proto__
屬性,指向了一個prototype的實例對象Foo.prototype
;Foo.prototype
有個constructor
屬性,指向了Foo構造函數,這個屬性的值標明瞭,這個f1對象的類型,也即f1 instanceof Foo的結果爲true;Foo.prototype
;###Javascript對象的屬性查找方式 咱們訪問一個Javascript對象的屬性(含「方法」)的時候,查找過程究竟是什麼樣的呢?
先找先找對象屬性,對象的屬性中沒有,那就找對象的prototype共享屬性
請看代碼8:
var p = { name : "小明" }; //對象中能查找到name console.log(p.name); //對象中找不到myName,查找其prototype屬性,因爲p是Object類型的對象,故而查找Object的prototype是否有myName console.log(p.myName); Object.prototype.myName="個人名字是小明"; console.log(p.myName);
注12:以上爲代碼8 。
結果以下:
此處不難理解,很少作解釋。
##按照「繼承」理念來實現JS繼承## 在咱們懂了prototype、constructor、對象、new以後,咱們能夠真正按照「繼承」的理念來實現javascript的繼承了。
###原型鏈 試想一下,若是構造函數的prototype對象的__proto__指針(每一個實例對象都有一個__proto__指針)指向的是另外一個prototype對象(咱們稱之爲prototype對象2)的話,而prototype對象2的constructor指向的是構建prototype對象2的構造函數。那麼依次往復,就構成了原型鏈。
上面的話有點繞口,你們多多體會。
我結合上面的Javascript對象的架構繼續給你們說說:
原型鏈只能繼承共享的屬性和方法,對於非共享的屬性和方法,咱們須要經過顯示調用父類構造函數來實現
查找對象的屬性的修正:
因此很簡單,咱們想要實現Javascript的繼承已經呼之欲出了:
###繼承構造函數中定義的屬性和方法 咱們經過call或者apply方法便可實現父類構造函數調用,而後把當前對象this和參數傳遞給父類,這樣就能夠實現繼承構造函數中定義的屬性和方法了。請看代碼9:
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.showName = function() { console.log(this.name); }; function Male(name, age) { // 調用Person構造函數,把this和參數傳遞給Person Person.apply(this, arguments); this.sex = "男"; } var m = new Male("小明", 20); console.log(m); console.log(m instanceof Male); console.log(m instanceof Person); console.log(m instanceof Object);
執行結果以下:
Person.prototype
中繼承過來的。m instanceof Person
結果爲false, 顯然m.__proto__.constructor
指向的是Male構造函數,而非Person。m instanceof Object
的結果卻爲true,那是由於m的原型鏈的上一級爲Object類型,故而instance of Object
的結果爲true。###繼承prototype中定義的屬性和方法,而且與繼承構造函數結合起來### 如何繼承prototype中定義的屬性和方法呢?
直接把父類的prototype給子類的prototype不就好了。
的確,這樣是可以實現方法共享,但是一旦子類的prototype的某個方法被重寫了,那麼父類也會擱着變更,怎麼辦?
**new一個父類!**賦值給子類的prototype。
請看代碼10:
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.showName = function() { console.log(this.name); }; function Male(name, age) { // 調用Person構造函數,把this和參數傳遞給Person Person.apply(this, arguments); this.sex = "男"; } // 繼承prototype Male.prototype = new Person(); var m = new Male("小明", 20); console.log(m); console.log(m instanceof Male); console.log(m instanceof Person); console.log(m instanceof Object);
結果以下:
你們能夠看到m對象不只有name, age , sex三個屬性,並且經過其原型鏈能夠找到showName方法。
若是你們仔細觀察,會發現多出了兩個undefined值的name和age!
爲何?!
究其緣由,由於在執行Male.prototype = new Person()
的時候,這兩個屬性就在內存中分配了值了。並且改寫了Male的整個prototype,致使Male對象的constructor也跟着變化了,這也很差。
這並非咱們想要的!咱們只是單純的想要繼承prototype,而不想要其餘的屬性。
怎麼辦?
借用一個空的構造函數,借殼繼承prototype,而且顯示設置constructor
代碼以下:
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.showName = function() { console.log(this.name); }; function Male(name, age) { // 調用Person構造函數,繼承構造函數的屬性,把this和參數傳遞給Person Person.apply(this, arguments); this.sex = "男"; } // 借用一個空的構造函數 function F() { } F.prototype = Person.prototype; // 繼承prototype Male.prototype = new F(); // 顯示指定constructor Male.prototype.constructor = Male; var m = new Male("小明", 20); console.log(m); m.showName(); console.log(m.constructor == Male); console.log(m instanceof Person); console.log(m instanceof Male); console.log(m instanceof F);
執行結果:
咱們可喜的將m的constructor正本清源!並且instanceof類型判斷都沒有錯誤(instanceof本質上是經過原型鏈找的,只要有一個原型知足了那結果就爲true)。 ##繼承prototype的封裝&測試 上述繼承prototype的代碼非常醜陋,讓咱們封裝起來吧。而且測試了一下代碼:
// 繼承prototype & 設定subType的constructor爲子類,不跟着prototype變化而變化 function inheritPrototype(subType, superType) { // 如下三行能夠寫成一個新的函數來完成 function F() { } // 把F的prototype指向父類的prototype,修改整個prototype而不是部分prototype F.prototype = superType.prototype; // new F()完成兩件事情,1. 執行F構造函數,爲空;2. 執行F的prototype的內存分配,這裏就是父類,也就是Person的getAge方法 // 因此這裏是繼承了父類的getAge()方法,賦值給了proto var proto = new F(); // proto的構造函數顯示指定爲子類(因爲上面重寫了F的prototype,故而構造函數也變化了) proto.constructor = subType; // 實現真正意義上的prototype的繼承,而且constructor爲子類 subType.prototype = proto; } function Person(name, age) { this.name = name; this.age = age; this.getName = function() { return this.name; }; } Person.prototype.getAge = function() { return this.age; }; function Male(name, age) { Person.apply(this, [name, age]); // 借用構造函數繼承屬性 this.sex = "男"; this.getSex = function() { return this.sex; }; } inheritPrototype(Male, Person); // 方法覆蓋 Male.prototype.getAge = function() { return this.age + 1; }; var p = new Person("好女人", 30); var m = new Male("好男人", 30); console.log(p); console.log(m); console.log(p.getAge()); console.log(m.getAge());
運行結果爲:
至此,咱們已經完成了真正意義上的javascript繼承!
讓咱們再來回頭驗證一下,TDD嘛~呵呵
##總結## 咱們是經過:
至此,較爲漂亮的完成了Javascript的繼承!
經過此思路,想要實現抽象類,接口等面向對象的概念應該也不是難事吧。呵呵。
抽象類:父類構造函數中只有方法定義,則該父類即爲抽象父類。
接口:父類構造函數中方法定義爲空。
多態:父類中調用一個未實現的函數,在子類中實現便可。
類型轉換:把中間層F斷掉,從新指定實例對象的__proto__指向的prototype對象,那麼F中繼承的方法將不復存在,故而調用方法就是直接調用被指向的prototype對象的方法了。關於類型轉換的代碼以下:
// 繼承prototype & 設定subType的constructor爲子類,不跟着prototype變化而變化 function inheritPrototype(subType, superType) { // 如下三行能夠寫成一個新的函數來完成 function F() { } // 把F的prototype指向父類的prototype,修改整個prototype而不是部分prototype F.prototype = superType.prototype; // new F()完成兩件事情,1. 執行F構造函數,爲空;2. 執行F的prototype的內存分配,這裏就是父類,也就是Person的getAge方法 // 因此這裏是繼承了父類的getAge()方法,賦值給了proto var proto = new F(); // proto的構造函數顯示指定爲子類(因爲上面重寫了F的prototype,故而構造函數也變化了) proto.constructor = subType; // 實現真正意義上的prototype的繼承,而且constructor爲子類 subType.prototype = proto; } function Person(name, age) { this.name = name; this.age = age; this.getName = function() { return this.name; }; } Person.prototype.getAge = function() { return this.age; }; function Male(name, age) { Person.apply(this, [name, age]); // 借用構造函數繼承屬性 this.sex = "男"; this.getSex = function() { return this.sex; }; } inheritPrototype(Male, Person); // 方法覆蓋 Male.prototype.getAge = function() { return this.age + 1; }; var p = new Person("好女人", 30); var m = new Male("好男人", 30); console.log(p); console.log(m); // 將m轉換爲Person類型從而調用Person類的方法 m.\__proto\__ = Person.prototype; console.log(p.constructor == Person); console.log(m.constructor == Male); console.log(m instanceof Male); console.log(m instanceof Person); console.log(p.getAge()); console.log(m.getAge()); // 將m轉換爲Male類型從而調用Male類的方法 m.\__proto\__ = Male.prototype; console.log(p.constructor == Person); console.log(m.constructor == Male); console.log(m instanceof Male); console.log(m instanceof Person); console.log(p.getAge()); console.log(m.getAge());
運行結果:
你們能夠看到類型轉換以後,調getAge()方法的不一樣了吧。
【文獻引用】 1.《Professional Javascript for Web Developers》 3rd. Edition 第六章