JS面向對象篇4、原型鏈與繼承(多種繼承實現方式及其利弊分析)

本片文章內容:
一、什麼是原型鏈;
二、利用原型鏈實現繼承;
三、借用構造函數實現繼承;
四、組合繼承(最經常使用的繼承模式);
五、原型式繼承:Object.create();
六、寄生式繼承、寄生組合繼承(最理想的繼承範式);javascript

前言.....

因爲本文篇幅較長,避免你們看了後亂了章法,仍是進一步詳細明確下主要內容。首先介紹了什麼是原型鏈,理解了原型鏈的概念才能夠進一步學習繼承的知識,已經瞭解的可跳過。由於javascript的繼承主要是依靠原型鏈來實現的,固然實現繼承的方式有不少種,上面也有羅列,其中組合繼承是最經常使用的方式,由於它是由原型鏈和借用構造函數兩種方式結合而來,因此能夠先簡單瞭解借用構造函數以及原型鏈實現繼承的方法。剩下的原型式繼承和寄生式繼承,都是爲了最後的寄生組合式繼承所作的鋪墊,由於寄生組合式繼承相比最經常使用的組合繼承更加高效,因此這種方式也是最理想的繼承範式。java

原型鏈

因爲前面的文章中已經詳細寫過關於原型的文章,那麼先來回顧一下原型、構造函數和實例對象的關係,請謹記下面的規則,這是理解原型鏈的基礎:
每個構造函數都有一個屬性叫prototype,這個屬性是一個指針,它指向了一個對象,這個對象就是原型;
每個原型對象默認會獲取一個constructor屬性,這個屬性又指回了構造函數;
每個實例對象都有一個內部屬性[[prototype]],這個屬性指向建立它的構造函數的prototype屬性值,也就是原型對象;
當試圖得到一個對象的某個屬性時,若這個對象自己沒有這個屬性,那麼就要去它的[[prototype]](即它的構造函數的prototype)中尋找。瀏覽器

因爲不少瀏覽器中經過__proto__可以訪問到對象內部的[[prototype]]屬性,因此接下來討論中咱們都會使用__proto__代替[[prototype]]。app

知道了以上規則,那麼原型鏈其實就是從一個對象出發,每個對象都有一個__proto__屬性,這個屬性指向這個對象的構造函數的prototype屬性值,即原型對象,而原型對象自己也是一個對象,所以它自己也有一個__proto__屬性,這個屬性再次指向這個原型對象的構造函數的prototype屬性值,以此類推,這樣由對象的__proto__屬性關聯造成的這條鏈就是原型鏈。
這裏要謹記一個問題,由於全部對象都是經過new Object()建立的,那麼全部函數的默認原型都是Object的實例。函數

function Person(name) {
    this.name = name;
}
var p = new Person();
console.log( p.__proto__ == Person.prototype);//true,實例對象p的__proto__屬性指向函數Person的prototype屬性值;
console.log(p.__proto__.constructor);//結果:ƒ Person(name) {this.name = name;},p的構造函數爲Person函數
console.log(Person.prototype.__proto__.constructor);//ƒ Object() { [native code] },Person.prototype這個對象的構造函數(即Person.prototype這個對象的__proto__中的constructor屬性)爲Object
console.log(Object.prototype == Person.prototype.__proto__);//所以Person.prototype對象的__proto__屬性指向它的構造函數Object的prototype
console.log(Object.prototype.__proto__);//null,再向上一層去找Object的原型(Object.prototype)也是一個對象,它的原型爲null;

注意因爲上面代碼中打印Person.prototype
以上代碼造成了的原型鏈:p——>p.__proto__(Person.prototype)——>Person.prototype.__proto__(Object.prototype)——>null,以下圖所示:
在這裏插入圖片描述
到這裏若是沒有理解也沒有關係,繼續看下面的原型鏈實現繼承能夠更加清晰的瞭解原型鏈。學習

原型鏈實現繼承

javascript中的繼承主要是依靠原型鏈來實現的,其基本思想就是利用原型,讓一個引用類型繼承另外一個引用類型的屬性和方法。
具體作法就是重寫建立引用類型a的構造函數A的原型對象爲另外一個引用類型b。this

function Father () {
    this.fatherName = 'dad'
}
Father.prototype.say = function () {
    console.log(this.fatherName)
}
function Child () {
    this.childName = 'son'
}
Child.prototype = new Father()
Child.prototype.sing = function () {
    console.log('hello dad')
}
var c = new Child()
c.say() // dad

利用原型,咱們將子類型Child構造函數的原型重寫爲父類型的實例,這樣,當在子類型的實例c上訪問一個屬性時,會先在實例c本上尋找,若是沒有找到,則回去c的原型對象,也就是Child.prototype上去找,而這時Child.prototype指向的是Father的實例,由此實現了子類型訪問父類型實例的屬性和方法(包括父實例對象和原型對象上的全部屬性和方法),也就實現了繼承。
在這裏插入圖片描述
全部函數的默認原型都是Object的實例,因此Father.prototype就是Object實例,那麼Father.prototype.__proto__就是Object.prototype,Object.prototype也是一個對象,在向上找Object.prototype這個對象的原型則是null。.net

注意事項

這種利用原型鏈實現繼承的方式有一個特別須要注意的問題:若是子自類型的原型上須要添加本身的方法或者覆蓋父類型的方法,必須將添加方法的代碼寫在替換原型的語句以後,不然會找不到。另外也不能在替換了自類型構造函數的原型爲另外一個實例後,再用字面量的方式給自類型添加方法,這樣作則會切斷自類型與父類型的聯繫,再也不能實現繼承。prototype

function Father () {
    this.fatherName = 'dad'
}
Father.prototype.say = function () {
    console.log(this.fatherName)
}
function Child () {
    this.name = 'son'
}
//一、先替換原型
Child.prototype = new Father()
//二、再添加方法
Child.prototype.sing = function () {
    console.log('hello dad')
}
//三、若添加方法像下面這樣使用字面量方式就不能實現繼承了
//Child.prototype = {
//  sing : function () {
//      console.log('hello dad')
//  }
//}
var c = new Child()
c.say() // dad
存在的問題

問題一:原型上的引用類型屬性會被全部子類型實例共享,它們之間的修改相互影響;
問題二:沒辦法像父類型的構造函數傳參數,以使得全部實例動態設置本身的屬性值;
基於以上不多單獨使用原型鏈來實現繼承。指針

借用構造函數

這種方式的基本思想:在自類型構造函數內部,利用call或apply方法調用父類型構造函數,傳入當前做用域,即this,就能夠添加每一個子類型實例本身的屬性了。
由於每一個自實例都擁有本身的屬性,因此避免了共享屬性帶來的相互影響的問題。另外,call方法除了能夠傳一個this,還能夠傳其餘的參數給父類型構造函數,從而動態設置子類型的屬性值。

function Father (name, age) {
    this.name = name
    this.age = age
    this.interest = ['sing', 'book']
}
function Child (name, age, sex) {
    //繼承了Father,同時傳遞了參數給父類型構造函數
    Father.call(this, name, age)
}
var sister = new Father('Amy', 18)
var brother = new Father('Tom', 14)
sister.interest.push('paint')
console.log(sister.interest) // ['sing', 'book', 'paint']
console.log(brother.interest) // ['sing', 'book']

若是單獨使用借用構造函數的方法實現繼承,那麼方法就只能定義在構造函數中了,那麼就會變成建立n個實例,產生n個一樣的函數,就沒有函數複用可言了。因此咱們也不多單獨使用這種方式。

組合繼承

結合原型鏈與借用構造函數實現繼承的兩種方法來實現繼承叫組合繼承。
基本思想就是:原型鏈實現原型上的可共享屬性和方法的繼承,借用構造函數實現父類型實例屬性的繼承。

function Father (name, age) {
    this.name = name
    this.age = age
    this.interest = ['sing', 'book']
}
Father.prototype.say = function () {
    console.log('hello my children')
}
function Child (name, age, sex) {
    //繼承父類型屬性
    Father.call(this, name, age)
    this.sex = sex
}
//繼承方法
Child.prototype = new Father()
Child.prototype.constructor = Child
Child.prototype.sing = function () {
    console.log('hello dad')
}
var sister = new Father('Amy', 18, 'girl')
var brother = new Father('Tom', 14, 'boy')
sister.interest.push('paint')
console.log(sister.interest) // ['sing', 'book', 'paint']
console.log(brother.interest) // ['sing', 'book']
console.log(sister.name) // Amy
console.log(brother.name) // Tom

組合繼承方式是javascript中最經常使用的繼承模式。

原型式繼承:Object.create()

基本思想:藉助原型基於已有的對象建立新對象。

function object (o) {
    function F() {}
    F.prototype = o
    return new F()
}

以上代碼能夠看出,這個object函數的做用就是,返回一個繼承給定對象的新對象。如:

var father = {
    name : 'zhangsan',
    age : 40,
    interest : ['sing', 'book'],
    sayName : function () {
        console.log(this.name)
    }
}
var child1 = object(father)
var child2 = object(father)
console.log(child1.name)
console.log(child2.name)
child1.interest.push('paint')
console.log(child1.interest) // ['sing', 'book', 'paint']
console.log(child2.interest) // ['sing', 'book', 'paint']

經過這種方式能夠建立兩個繼承father對象的實例對象child一、child2。
ECMAScript經過新增的Object.create()方法規範化了原型式繼承。
這個方法接收兩個參數,使用方法以下:

var father = {
    name : 'youyang',
}
var child = Object.create(father, {
    name : {
        value : 'xiaoqu'
    }
})
console.log(child.name) // xiaoqu

Object.create()方法的第二個參數與Object.defineProperties()方法的第二個參數格式相同,關於Object.defineProperties()方法的使用以及屬性描述符相關問題能夠閱讀這篇文章:理解對象及屬性特性(屬性描述符)

注意事項

原型式繼承實際上就是返回了一個指定對象的副本而已。
仔細想來Object.create()方法也只是一個建立對象的方法,只不過這個對象的原型爲某個指定對象

存在的問題

父類型的引用類型屬性在自類型實例中會共享,修改後會相互影響。

原型式繼承在只想讓一個對象與另外一個對象相似的狀況下,沒有必要建立構造函數時徹底能夠勝任。

寄生式繼承

寄生式繼承是在原型式繼承的基礎上擴展來的,以前也強調過原型式繼承只是返回了指定對象的一個副本,因此原型式繼承並無給子類型添加屬於本身的屬性和方法。
寄生式繼承的思想是:依舊在函數內部,利用原型基於已有對象建立新的對象,不一樣的是要爲這個新對象添加一些屬於本身的屬性和方法。

function createAnother (o) {
    var another = object(o) // object是原型式繼承中的那個object函數
    another.sayHello = function () {
        console.log('hello world')
    }
    return another
}

上面封裝的函數createAnother就是寄生式繼承的實現。

var father = {
    name : 'youyang',
}
var child = createAnother(father)
child.sayHello() // hello world
存在的問題

使用這種方式來爲子類型添加函數時,依然也是不能作到函數複用,沒建立一個對象,就會相應建立一個方法(這裏指sayHello),會下降效率。

寄生組合式繼承

前面說到組合繼承是最經常使用的繼承模式,可是這種方式其實仍是有不足的,它的問題就是要執行兩次父類型的構造函數。可回顧一下以前組合繼承的代碼,分別在子類型構造函數內部經過call方法調用了一次,以及在重寫自類型構造函數原型的時候又調用了一次。
所謂寄生組合式繼承就是結合組合繼承和寄生式繼承兩種方式來實現繼承,具體思想是:將子類型構造函數原型重寫爲父類型實例改成重寫爲父類型構造函數原型對象的副本。

function createAnotherPrototype(Father, Child) {
    var another = object(Father.prototype)
    another.constructor = Child
    Child.prototype = another
}

以上就是寄生組合式繼承的核心代碼,因而只要將組合繼承例子中的Child.prototype = new Father()這行代碼改成調用上面封裝好的函數便可:createAnotherPrototype(Father, Child);

開發人員廣泛認爲寄生組合式繼承是引用類型最理想的繼承範式!

相關文章
相關標籤/搜索