繼承是oo語言中一個最爲人津津樂道的概念。ECMAScript支持實現繼承,並且實現繼承只要是靠原型鏈來實現的
·原型鏈
其基本思想是利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法。
簡單回顧一個構造函數,原型和實例的關係:每一個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的
內部指針。那麼,假設咱們讓原型對象等於另外一個類型的實例,結果會怎樣呢?
// 生物的構造函數 function Bio(){ this.life = true } Bio.prototype.getLife = function () { return this.life; } // 動物的構造函數 function Animal(){ this.eat = true; } Animal.prototype = new Bio(); // 狗的構造函數 function Dog(master){ this.master = master; } Dog.prototype = new Animal(); var dog1 = new Dog("cl"); console.log(dog1.life); // true console.log(dog1.constructor); /* 輸出: ƒ Bio(){ this.life = true } 爲何會這樣,由於 Dog 和 Animal 的constructor 被重寫了的緣故 */
上述代碼中。咱們沒有使用Dog默認提供的原型,而是給它換了一個新原型。這個新原型就是Animal的實例。因而新原型不只具備做爲一個Animal
實例擁有的所有屬性和方法,並且其內部還有一個指針,指向了Animal的原型...
經過實現原型鏈,本質上拓展了本章前面介紹的原型搜索機制。
默認的原型:
事實上,前面例子中展現的原型鏈少了一環。咱們知道,全部的應用類型默認都繼承了Object,而這個繼承也是經過原型鏈實現的。你們記住,全部引用類型
默認原型都是Object的實例,所以默認原型都會包含一個內部指針,指向Object.prototype。這正是全部自定義類型都會繼承toString() ,valueOf()等
默認方法的緣由。
確認原型和實例的關係:
能夠經過兩種方式來肯定原型和實例之間的關係。
// 第一種方法:使用instanceof 操做符,只要用這個操做符檢測實例與原型鏈出現過的構造函數,結果就會返回true。 console.log(dog1 instanceof Dog); console.log(dog1 instanceof Animal); console.log(dog1 instanceof Bio); console.log(dog1 instanceof Object); // 所有爲true // 因爲原型鏈的關係,咱們能夠說dog1 是上述四個任何一個類型的實例 // 第二種方法:使用isPrototypeOf() 方法。一樣,只要原型鏈出現過的原型,均可以說是該原型鏈所派生的實例的原型 console.log(Object.prototype.isPrototypeOf(dog1)); console.log(Animal.prototype.isPrototypeOf(dog1)); console.log(Bio.prototype.isPrototypeOf(dog1)); console.log(Dog.prototype.isPrototypeOf(dog1)); // 所有爲true
謹慎的定義方法
兩點問題:
1: 無論怎麼樣,給原型添加方法的代碼必定放在替換原型的語句以後
2:經過原型鏈實現繼承時,不能使用對象字面量建立原型方法。由於這樣作就會重寫原型鏈
原型鏈的問題:
code..
function Super () { this.color = ["red","blue","green"]; } function Sub () { } Sub.prototype = new Super(); var ins1 = new Sub(); ins1.color.push("black"); console.log(ins1.color); // ["red", "blue", "green", "black"] var ins2 = new Sub(); console.log(ins2.color); // ["red", "blue", "green", "black"] // 那麼問題來了,因爲Sub的全部實例都會共享這一個color屬性,那麼咱們對ins1.color的修改可以經過ins2.color反映出來 // 問題:在建立子類型的實例時,不能向超類型的構造函數傳遞參數。實際上,應該說是沒有辦法在不影響全部對象實例的狀況下,給超類型的 // 構造函數傳遞參數。有鑑於此,實踐中不多會單獨使用原型鏈
·借用構造函數
在解決原型中包括引用類型值所帶來問題的過程當中,開發人員使用一種借用構造函數的技術。這種技術的基本思想至關簡單,即在子類型構造函數的
內部調用超類型構造函數
function Animal(name){ this.name = name; } function Dog(){ // 經過使用call()方法(或apply()方法也能夠),咱們實際上在新建立的Dog實例的環境調用了Animal構造函數。這樣一來 // 也會在心得Dog對象上執行Animal()函數中定義的全部對象初始化代碼 Animal.call(this,"Nic"); this.age = 29; } var ins = new Dog(); console.log(ins.name); // Nic console.log(ins.age); // 29
借用構造函數的問題:
若是僅僅是借用構造函數,那麼將沒法避免構造函數模式存在的問題---方法都在構造函數中定義,那麼函數複用就無從談起。並且超類型的原型定義的
方法,對子類型而言也是不可見的,結果全部類型都只能用構造函數模式。考慮到這些問題,借用構造函數的模式也不多使用
·組合繼承
組合繼承也叫僞經典繼承,指的是將原型鏈和構造函數的技術組合到一塊,從而發揮兩者之長的一種繼承模式。其背後的思路是使用原型鏈實現對原型屬性
和方法的繼承,而使用構造函數來實現對實例屬性的繼承。這樣,既經過在原型上定義方法實現了函數的複用,又可以保證每一個實例都有它本身的屬性。
上面的代碼能夠這樣改寫:
function Animal(name){ this.name = name; } Animal.prototype.home = 'north'; function Dog(){ // 經過使用call()方法(或apply()方法也能夠),咱們實際上在新建立的Dog實例的環境調用了Animal構造函數。這樣一來 // 也會在心得Dog對象上執行Animal()函數中定義的全部對象初始化代碼 Animal.call(this,"Nic"); this.age = 29; } Dog.prototype = new Animal(); Dog.prototype.constructor = Dog; // 修正構造函數 var ins = new Dog(); console.log(ins.name); // Nic console.log(ins.age); // 29 console.log(ins.home); // north
組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優勢,成爲javascript中最經常使用的繼承模式。並且,instanceof和isPrototypeOf()也
可以用於識別基於組合繼承建立的對象。
·原型式繼承
這種方法沒有嚴格意義上的構造函數。他的想法是藉助原型基於已有的對象建立新對象,同時還沒必要所以建立自定義類型:
function object(o){ function F(){}; F.prototype = o; return new F(); } //在object() 函數內部,先建立了一個臨時性的構造函數,而後將傳入的對象做爲這個構造函數的原型,最後返回這個臨時類型的一個新實例。 // 從本質上來說,object()對傳入的對象執行了一次淺複製 var person = { name : "Ni", friends : ['She','Co'], } var anotherPerson = Object(person); anotherPerson.friends.push('tom'); var yetAnotherPerson = Object(person); console.log(yetAnotherPerson.friends); // ["She", "Co", "tom"] // ECMAScript5經過新增Object.create() 方法規範了原型式繼承。這個方法接收兩個參數:一個用做新對象原型的對象和(可選的)一個爲新對象定義 // 額外屬性的對象。在傳入一個參數的狀況下,Object.create() 與 object()方法的行爲相同 var myPerson = Object.create(person,{demo:{value:"G"}}); console.log(myPerson.demo); //G // 在沒有興師動衆的插件構造函數,而只想讓一個對象與另外一個對象保持相似的狀況下,原型式繼承式徹底能夠勝任的。不過別忘了,包含引用類型的值 // 的屬性始終都會共享相應的值,就像適應原型模式同樣 ·寄生式繼承 寄生式繼承是與原型式繼承緊密相關的一種思路,而且一樣也是由上面的做者推而廣之的。寄生式繼承的思路與繼承構造函數和工廠模式相似,即 建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後再像真的是它作了全部工做同樣返回對象 function object(o){ function F(){}; F.prototype = o; return new F(); } function createAnother(orginal){ var close =object(orginal); // 經過調用函數建立一個新對象 close.sayHi = function () { // 以某種方式加強這個對象 console.log('hi'); } return close; // 返回這個對象 } var person = { 'name' : "Ni", friends : ["She","Co"], } var anPer = createAnother(person); anPer.sayHi(); // hi //在考慮到對象不是自定有類型和構造函數的狀況下,寄生式繼承也是一種游泳的模式,前面示範繼承模式時使用的object()函數不是必需的;任何能返回新 // 對象的函數都適用於此模式 // 使用寄生式繼承來爲對象添加函數,會因爲不能作到函數複用而下降效率;這一點與構造函數模式相似
·寄生組合式繼承
前面說過,組合繼承時js最經常使用的繼承模式;不過它也有本身的不足。組合繼承最大的問題是不管什麼狀況下,都會調用兩次超類型構造函數。一次在
建立子類原型的時候,另外一次是在子類構造函數內部。沒錯,,子類型最終會包含超類型對象的所有實例屬性,但咱們不得不在調用子類型構造函數
時重寫這些屬性。再看下組合繼承的例子:
function Super(name){ this.name = name; this.color = ['red','blue']; } Super.prototype.sayName = function () { console.log(this.name); } function Sub(name,age){ Super.call(this,name); // 第二次調用Super(); this.age = age; } Sub.prototype = new Super(); // 第一次調用Super(); Sub.prototype.constructor = Sub; Sub.prototype.sayAge = function () { console.log(this.age); } var demo = new Sub('cl','25'); demo.sayAge(); // 25 // 不知道你有沒有發現。第一次調用Super構造函數時,Sub.prototype會獲得兩個屬性:name 和 color是,它們都時Super的實例屬性,只不過 // 位於Sub的原型中。當調用Super構造函數時,又一次在新對象上建立了name 和 color 。因而,這兩個屬性就屏蔽了原型中的兩個同名屬性 // 好在咱們已經找到解決這個問題方法 --- 寄生組合式繼承 // 所謂繼承組合式繼承,即姐喲個構造函數來繼承屬性,經過原型鏈的混成形式繼承方法。其背後的基本思路是:沒必要爲了指定子類型的原型而 // 調用超類型的構造函數,咱們所須要的無非是超類型原型的一個副本而已。本質上,就是使用寄生式繼承來繼承超類型的原型,而後再將 // 結果制定給子類型的原型。寄生組合式繼承的基本模式以下: function inheritPrototype(subType,superType) { var prototype = Object.create(superType.prototype); // 建立對象 prototype.constructor = subType; // 加強對象 subType.prototype = prototype; // 指定對象 } /* 這個示例中的 inheritPrototype() 函數實現了寄生組合式繼承的最簡單形式。這個函數接收兩個參數:子類型構造函數和超類型構造函數。 在函數內部,第一步是建立超類型原型的一個副本。第二步爲建立的副本添加constructor屬性,彌補因重寫原型而失去的默認的constructor屬性。 最後一步,將建立的對象(即副本)賦值給子類型的原型。這樣咱們就能夠調用inheritPrototype()函數的語句,去替換前面例子中爲子類原型賦值的語句了: */ function SuperType(name){ this.name = name; this.color = ['red','blue','green']; } SuperType.prototype.sayName = function () { console.log(this.name); } function SubType(name, age){ SuperType.call(this,name); this.age = age; } inheritPrototype(SubType,SuperType); SubType.prototype.sayAge = function () { console.log(this.age); } var xx = new SubType('cl',14); console.log(xx); // 這個例子高效率體如今它之調用了一次SuperType構造的函數,而且所以避免了在SubType.prototype上建立沒必要要,多餘的屬性。與此同時, // 還可以正常使用instanceof 和 isPrototypeOf() 。開發人員廣泛認爲寄生組合式繼承是最理想的繼承範式。