1. 程式人生 > >都0202年了,你還不知道javascript有幾種繼承方式?

都0202年了,你還不知道javascript有幾種繼承方式?

前言     當面試官問你:你瞭解js哪些繼承方式?es6的class繼承是如何實現的?你心中有很清晰的答案嗎?如果沒有的話,可以通過閱讀本文,幫助你更深刻地理解js的所有繼承方式。       js繼承總共分成5種,包括建構函式式繼承、原型鏈式繼承、組合式繼承、寄生式繼承和寄生組合式繼承。   建構函式式繼承       首先來看第一種,建構函式式繼承,顧名思義,也就是利用函式去實現繼承;       假設我們現在有一個父類函式:
// 父類建構函式
function Parent(color) {
    this.color = color;
    this.print = function() {
        console.log(this.color);
    }
}

    現在要編寫一個子類函式來繼承這個父類,如下:

// 子類建構函式
function Son(color) {
    Parent.call(this, color);
}

    上面程式碼可以看到,子類Son是通過Parent.call的方式去呼叫父類建構函式,然後把this物件傳進去,執行父類建構函式之後,子類Son就擁有了父類定義的color和print方法。

    呼叫一下該方法,輸出如下:

// 測試
var son1 = new Son('red');
son1.print(); // red
​
var son2 = new Son('blue');
son2.print(); // blue
    可以看到son1和son2都正常繼承了父類的print方法和各自傳進去的color屬性;       以上就是建構函式式繼承的實現了,這是最原始的js實現繼承的方式;       但是當我們深入想一下會發現,這種根本就不是傳統意義上的繼承!         因為每一個Son子類呼叫父類生成的物件,都是各自獨立的,也就是說,如果父類希望有一個公共的屬性是所有子類例項共享的話,是沒辦法實現的。什麼意思呢,來看下面的程式碼:
function Flower() {
    this.colors = ['黃色', '紅色'];
    this.print = function () {
        console.log(this.colors)
    }
}
​
function Rose() {
    Flower.call(this);
}
​
var r1 = new Rose();
var r2 = new Rose();
​
console.log(r1.print()); // [ '黃色', '紅色' ]
console.log(r2.print()); // [ '黃色', '紅色' ]

    我們現在有一個基類Flower,它有一個屬性colors,現在我們把某一個例項的colors值改一下:

r1.colors.push('紫色');
​
console.log(r1.print()); // [ '黃色', '紅色', '紫色' ]
console.log(r2.print()); // [ '黃色', '紅色' ]
    結果如上,顯然,改變的只有r1的值,因為通過建構函式創造出來的例項物件中,所有的屬性和方法都是例項內部獨立的,並不會跟其他例項共享。       總結一下建構函式的優缺點:
  • 優點:所有的基本屬性獨立,不會被其他例項所影響;
  • 缺點:所有希望共享的方法和屬性也獨立了,沒有辦法通過修改父類某一處來達到所有子例項同時更新的效果;同時,每次建立子類都會呼叫父類建構函式一次,所以每個子例項都拷貝了一份父類函式的內容,如果父類很大的話會影響效能;
原型鏈繼承       下面我們來看第二種繼承方式,原型鏈式繼承;       同樣先來看下例子:
function Parent() {
    this.color = 'red';
    this.print = function() {
        console.log(this.color);
    }
}
function Son() {
}

    我們有一個父類和一個空的子類;

Son.prototype = new Parent();
Son.prototype.constructor = Son;

    接著我們把子函式的原型屬性賦值給了父函式的例項;

var son1 = new Son();
son1.print(); // red
    最後新建子類例項,呼叫父類的方法,成功拿到父類的color和print屬性方法;       我們重點來分析一下下面兩行程式碼:
Son.prototype = new Parent();
Son.prototype.constructor = Son;
    這段程式碼中,子函式的原型賦給了父函式的例項,我們知道prototype是函式中的一個屬性,js的一個特性就是:如果一個物件某個屬性找不到,會沿著它的原型往上去尋找,直到原型鏈的最後才會停止尋找。       關於原型更多基礎的知識,可以參考一下其他文章,或許以後我也會出一期專門講解原型和原型鏈的文章。       回到程式碼,我們看到最後例項son成功呼叫了Print方法,輸出了color屬性,這是因為son從函式Son的prototype屬性上面去找到的,也就是從new Parent這個物件裡面找到的;           這種方式也不是真正的繼承,因為所有的子例項的屬性和方法,都在父類同一個例項上了,所以一旦某一個子例項修改了其中的方法,其他所有的子例項都會被影響,來看下程式碼: 
function Flower() {
    this.colors = ['黃色', '紅色'];
    this.print = function () {
        console.log(this.colors)
    }
}
​
function Rose() {}
Rose.prototype = new Flower();
Rose.prototype.constructor = Rose;
​
var r1 = new Rose();
var r2 = new Rose();
​
console.log(r1.print()); // [ '黃色', '紅色' ]
console.log(r1.print()); // [ '黃色', '紅色' ]
​
r1.colors.push('紫色');
​
console.log(r1.print()); // [ '黃色', '紅色', '紫色' ]
console.log(r2.print()); // [ '黃色', '紅色', '紫色' ]
    還是剛才的例子,這次Rose子類選擇了原型鏈繼承,所以,子例項r1修改了colors之後,r2例項的colors也被改動了,這就是原型鏈繼承不好的地方。       來總結下原型鏈繼承的優缺點:
  • 優點:很好的實現了方法的共享;
  • 缺點:正是因為什麼都共享了,所以導致一切的屬性都是共享的,只要某一個例項進行修改,那麼所有的屬性都會變化 
組合式繼承       這裡來介紹第三種繼承方式,組合式繼承;       這種繼承方式很好理解,既然建構函式式繼承和原型鏈繼承都有各自的優缺點,那麼我們把它們各自的優點整合起來,不就完美了嗎?  

 

 

                  組合式繼承做的就是這個事情~來看一段程式碼例子:
​function Parent(color) {
    this.color = color;
}
Parent.prototype.print = function() {
    console.log(this.color);
}
function Son(color) {
    Parent.call(this, color);
}
Son.prototype = new Parent();
Son.prototype.constructor = Son;
​
var son1 = new Son('red');
son1.print(); // red
​
var son2 = new Son('blue');
son2.print(); // blue
    上面程式碼中,在Son子類中,使用了Parent.call來呼叫父類建構函式,同時又將Son.prototype賦給了父類例項;為什麼要這樣做呢?為什麼這樣就能解決上面兩種繼承的問題呢?         我們接著分析一下,使用Parent.call呼叫了父類建構函式之後,那麼,以後所有通過new Son創建出來的例項,就單獨拷貝了一份父類建構函式裡面定義的屬性和方法,這是前面建構函式繼承所提到的一樣的原理;       然後,再把子類原型prototype賦值給父類的例項,這樣,所有子類的例項物件就可以共享父類原型上定義的所有屬性和方法。這也不難理解,因為子例項會沿著原型鏈去找到父類函式的原型。       因此,只要我們定義父類函式的時候,將私有屬性和方法放在建構函式裡面,將共享屬性和方法放在原型上,就能讓子類使用了。       以上就是組合式繼承,它很好的融合了建構函式繼承和原型鏈繼承,發揮兩者的優勢之處,因此,它算是真正意義上的繼承方式。   寄生式繼承       既然上面的組合式繼承都已經這麼完美了,為什麼還需要其他的繼承方式呢?         我們細想一下,Son.prototype = new Parent();這行程式碼,它有什麼問題沒有?       顯然,每次我們例項化子類的時候,都需要呼叫一次父類建構函式,那麼,如果父類建構函式是一個很大很長的函式,那麼每次例項化子類就會執行很長時間。       實際上我們並不需要重新執行父類函式,我們只是想要繼承父類的原型。       寄生式繼承就是在做這個事情,它是基於原型鏈式繼承的改良版:  
var obj = {
    color: 'red',
    print: function() {
        console.log(this.color);
    }
};
​
var son1 = Object.create(obj);
son1.print(); // red
​
var son2 = Object.create(obj);
son2.print(); // red

    寄生式繼承本質上還是原型鏈繼承,Object.create(obj);方法意思是以obj為原型構造物件,所以寄生式繼承不需要建構函式,但是同樣有著原型鏈繼承的優缺點,也就是它把所有的屬性和方法都共享了。

寄生組合式繼承       接下來到我們最後一個繼承方式,也就是目前業界最為完美的繼承解決方案:寄生組合式繼承。       沒錯,它就是es6的class語法實現原理。       但是如果你理解了組合式繼承,那麼理解這個方式也很簡單,只要記住,它出現的主要目的,是為了解決組合式繼承中每次都需要new Parent導致的執行多一次父類建構函式的缺點。       下面來看程式碼:
function Parent(color) {
    this.color = color;
}
Parent.prototype.print = function() {
    console.log(this.color);
}
function Son(color) {
    Parent.call(this, color);
}
Son.prototype = Object.create(Parent.prototype);
Son.prototype.constructor = Son;
​
var son1 = new Son('red');
son1.print(); // red
​
var son2 = new Son('blue');
son2.print(); // blue
    這段程式碼不同之處只有一個,就是把原來的Son.prototype = new Parent();修改為了Son.prototype = Object.create(Parent.prototype);       我們前面講過,Object.create方法是以傳入的物件為原型,建立一個新物件;建立了這個新物件之後,又賦值給了Son.prototype,因此Son的原型最終指向的其實就是父類的原型物件,和new Parent是一樣的效果;         到這裡,我們5中js的繼承方式也就講完了;

 

 

 

                      如果你對上面的內容感到疑問或者不理解的,可以留言和我交流,或者關注公眾號直接聯絡我~       最後感謝小夥伴的閱讀,如果覺得文章寫的還可以的話,歡迎點個贊、點個關注,我會持續輸出優質的技術分享文章。​