徹底搞懂JavaScript中的繼承

你的聲先生發表於2019-08-06

上一片文章,給大家分享了物件的建立和函式的建立及優缺點。還不知道的小夥伴可以去物件導向,搞定物件這節先了解一下,回過頭再來看這篇文章。

概念

什麼是原型鏈以及書中介紹了好多種繼承方法,優缺點是什麼!

什麼是原型鏈

當談到繼承時,JavaScript 只有一種結構:物件。每個例項物件( object )都有一個私有屬性(稱之為 proto )指向它的建構函式的原型物件(prototype )。該原型物件也有一個自己的原型物件( proto ) ,層層向上直到一個物件的原型物件為null。根據定義,null沒有原型,並作為這個原型鏈中的最後一個環節。---來著MDN

這裡是官方給出的解釋,我們用個例子,來具體的去看看這個原型鏈。在舉例之前,我們先來了解一下,原型和例項的關係。

每個建構函式(constructor)都有一個原型物件(prototype),原型物件都包含一個指向建構函式的指標,而例項都包含一個指向原型物件的內部指標。

上面的這段還如果你還沒理解,我還是建議你去看物件導向,搞定物件,這裡已經給你詳細的解釋了。這裡就不多說了。
那我們想一下,如果讓一個函式的例項物件,指向函式的原型物件,會發生什麼?

   function Father(){
	this.property = true;
}
Father.prototype.getFatherValue = function(){
	return this.property;
}
function Son(){
	this.sonProperty = false;
}
//繼承 Father
Son.prototype = new Father();//Son.prototype被重寫,導致Son.prototype.constructor也一同被重寫
Son.prototype.getSonVaule = function(){
	return this.sonProperty;
}
var instance = new Son();
alert(instance.getFatherValue());//true
複製程式碼

instance例項通過原型鏈找到了Father原型中的getFatherValue方法. 為了找到getFatherValue屬性,都經歷了什麼呢?

  1. 先在instance中尋找,沒找到
  2. 接著去instance.proto(Son.prototype)中尋找,也是Father的例項中尋找。又沒找到。
  3. 去Father.prototype中尋找,找到了,返回結果。假如還沒找到。
  4. 去object.prototype中尋找,假如還沒找到。
  5. 返回undefined.

instance --> new Father() --> Father.prototype --> object.prototype --> undefined 這就是你日思夜想不得解的原型鏈啊!是不是很好理解了。
例項與原型連線起來的鏈條,叫做原型鏈(我自己定義的)。

原型鏈繼承問題

上面的例子,就是原型鏈繼承的標準例子。但是原型鏈繼承,是有問題的。高階程式設計上說,他有兩個問題:

  • 當原型鏈中包含引用型別值的原型時,該引用型別值會被所有例項共享;
  • 在建立子型別(例如建立Son的例項)時,不能向超型別(例如Father)的建構函式中傳遞引數.

是有這兩個問題,但我說也不一定,當你原型鏈不需要飲用型別,建立的子型別不需要傳參!那就不存在問題啊!哈哈哈
別鬧,畢竟這樣的需求很少,甚至根本不可能有,還是老實兒解決上面的問題吧。

借用建構函式

原理:在子型別建構函式中呼叫超型別建構函式。

原型鏈繼承的問題

先來看看原型鏈函式的問題:

function Father (){
    this.colors = ['red','green','blue'];
}
function Son (){
}
Son.prototype = new Father();

let instance1 = new Son();
instance1.colors.push('white');
console.log(instance1.colors);

let instance2 = new Son();
console.log(instance2.colors);
複製程式碼

執行之後,就會發現,返回的結果是一樣的,也就是說,所有的例項都會共享colors這個屬性。這並不是不我們想要的結果。

原型鏈繼承的問題的解決辦法

在子型別建構函式中呼叫超型別建構函式。

function Father(name){
    this.colors = ['red','green','blue'];
    
}
function Son(name){
    Father.call(this,name);
}

let instance1 = new Son();
instance1.colors.push('white');
console.log(instance1.colors); // ['red','green','blue','white'];

let instance2 = new Son();
console.log(instance2.colors); // ['red','green','blue'];
複製程式碼

請記住,函式只是在特定環境中執行的程式碼的物件,因此可以通過call()或apply()方法也可以在新建立的物件上執行建構函式。
這段話很好理解:誰幹(調)的,誰負責。
結合上面的程式碼,instance1呼叫的colors屬性,那就你instance1物件負責,我instance2沒做任何事,我不負責。

再來看傳參問題

function Father(name){
    this.colors = ['red','green','blue'];
    this.name  = name; //新增code
}
function Son(name){
    Father.call(this,name); //將name,傳遞給Father。
}

let instance1 = new Son('hanson'); //建立例項物件時,傳入引數
instance1.colors.push('white');
console.log(instance1.colors); // ['red','green','blue','white'];
console.log(instance1.name); // 'hanson'

let instance2 = new Son();
console.log(instance2.colors); // ['red','green','blue'];
複製程式碼

很好理解嘛,通過建構函式Son,我們給Father傳了參。完美解決傳參問題。

借用建構函式的問題

  1. 所有的方法都得定義在建構函式上,無法實現複用。
  2. 超型別中的方法,對於子型別是不可見的。

如何解決這兩個問題呢?引出我們下一個繼承方法---組合繼承。

組合繼承

組合繼承也叫做偽經典繼承,指的是將原型鏈和借用建構函式的技術組合在一起,從而發揮二者之長的繼承模式

話不多說,上程式碼!


function Father(name){
    this.colors = ['red','green','blue'];
    this.name  = name; 
}
Father.prototype.sayName = function(){
    console.log(this.name)
}
function Son(name,age){
    Father.call(this,name); //將name,傳遞給Father。
    this.age = age;
}
Son.prototype = new Father();
Son.prototype.sayAge = function(){
    console.log(this.age);
}
let instance1 = new Son('hanson',18);//建立例項物件時,傳入引數
Son.prototype.constructor = Son;
instance1.colors.push('white');
console.log(instance1.colors); // ['red','green','blue','white'];
console.log(instance1.sayAge); // 18

let instance2 = new Son('grey',20);
console.log(instance2.colors); // ['red','green','blue'];
console.log(instance2.sayName()); // 'grey'
console.log(instance2.sayAge()); //18
複製程式碼

總結:分別擁有自己的屬性,還享有公共的方法,真好。

不靠譜的原型式繼承和寄生式繼承

這兩個繼承方式,都是一個叫克羅克德的人提出的,我們也不知道,我們也不敢問,估計式為了後面的組合式寄生繼承做鋪墊?

//原型式繼承 
var person = {
    name:'hanson',
    friends:['sha','feng','qiang']
}
var another = Object(person); //複製一份
another.name = 'bo';
another.friends.push('lei');
console.log(another.friends); //['sha','feng','qiang','lei']
console.log(another.name); //'bo'
console.log(person.friends); //['sha','feng','qiang','lei']
複製程式碼

相比上面的方法,這個要簡便的多,沒用到建構函式,原型鏈。但問題也十分明顯,汙染引用屬性

//寄生式繼承---也是克羅克德提出來的
function creat(obj) {
    var clone = Object(obj);
    clone.sayName = function(){
        console.log('hanson')
    }
    return clone;
}
var person = {
    age:18,
    friends:['qiang','sha','feng'] 
}
var another = creat(person);
console.log(another.sayName());
console.log(person.sayName());
複製程式碼

其實道理沒變,clone了一份物件,但是,同樣,peron物件也被玷汙了!不信,你列印一下,person也有了sayName()方法。犧牲太大,反正我不用~

最後一個,寄生組合式繼承

在講解之前呢,我們先來看看,組合繼承的缺點。

function Father(name){
    this.name = name;
    this,friends = ['qiang','sha','feng'] ;
}
Father.prototype.constructor = function(){
    console.log(this.name);
}
function Son(name,age){
    Father.call(this); //第二次呼叫
}
Son.prototype = new Father(); //第一次呼叫
Son.prototype.constructor = Son;
Son.prototype.sayName = function(){
    console.log(this.name);
}
複製程式碼

我們第一次呼叫超型別建構函式(Father),無非是想指定子型別的原型,讓他們直接建立聯絡而已。 給他個副本,又如何!

function inheritPrototype(Son,Father){
    var prototype = Object(Father.prototype);
    prototype.constructor = Son;
    Son.prototype = prototype;
}
複製程式碼

這個函式,接收兩個引數,一個子型別,一個超型別。在函式內部,

  1. 建立超型別的一個副本。
  2. 為副本新增constructor屬性,你補重寫prototype屬性造成的constructor屬性丟失問題。
  3. 將副本賦值給子型別的原型。
function Father(name){
    this.name = name;
    this,friends = ['qiang','sha','feng'] ;
}
Father.prototype.constructor = function(){
    console.log(this.name);
}
function Son(name,age){
    Father.call(this); //第二次呼叫
}
inheritPrototype(Son,Father);
Son.prototype.sayName = function(){
    console.log(this.name);
}
複製程式碼

這個例子,只呼叫了一次超型別建構函式,避免了在子型別上建立不必要的屬性和方法。是最理想的繼承方式。 但是,我可能不會用。。。你會用嗎?

相關文章