細說 js 的7種繼承方式

站住,別跑發表於2021-01-14

在這之前,先搞清楚下面這個問題:

function Father(){}    
Father.prototype.name = 'father';
Father.prototype.children = [];
        
const child1 = new Father();
            
console.log('get1 ==',child1); // Father {}
console.log('get ==',child1.name); // father
console.log('get ==',child1.children); // []
    
child1.name = 'child1';
console.log('set ==',child1.name); // child1
    
child1.children.push('child2');
// child1.children = ['123'];
console.log('set ==',child1.children); // ["child2"]
    
console.log('get2 ==',child1) // Father {name: "child1"}

疑問:

(1)為什麼訪問 child1.name 的時候,值是原型上的 name 的值,而設定值之後,例項的原型上的name屬性未被修改,反而自己生成了一個name屬性?

(2) child1.children.push('child2')  與   child1.children = ['123'];  最終的結果為什麼會不同?為什麼 push 方法會導致原型上的 children 屬性也會改變?

參考:

(1)《你不知道的js》第五章-原型——設定與遮蔽屬性 

(2)“=”操作符為物件新增屬性

簡單來說就是:

(1)查詢物件屬性的時候,會從本體物件開始查詢,如果有就返回本體上的屬性,因為原型鏈上的被遮蔽了。如果沒有就查原型鏈,直到原型鏈最高層null,找不到就返回 undefined。

(2)設定值的時候,如果該屬性沒有通過 Object.defineproperty 設定 setter 或者 writable 為 true,並且本體物件中沒有該屬性,並且是 ‘=’ 號賦值,那麼會直接在本體物件中新增該屬性。

(3)所以上面的 name 值,查詢的時候是 原型上的 name值。而設定的時候,符合 (2)的條件,所以直接在 child1 中新增 name屬性。如果 改成   child1.name++ ,結果也是本體物件中新增新屬性。因為 這句程式碼等價於 child1.name = child1.name + 1; 。是隱形的等號賦值哦。

(4) child1.children.push('child2')  由於不是等號賦值,那麼在 執行 child1.children 的時候,查詢到 children 之後,沒有 “=” 號賦值,而是 push ,所以操作的是 原型物件中的 children 屬性(引用屬性)。 而 child1.children = ['123']  也符合(2)的條件。所以是本體物件新增該屬性。

(5)可以簡單的理解為如果本體物件上沒有該屬性, ‘=’ 號賦值之後,分配了新的記憶體地址,因此只有在本體上新增屬性,才能儲存賦的值。如果有該屬性,就是簡單的值替換。

 

上面的問題明白了之後,再來了解一下 js 的幾種繼承方式。文字有點多,一定要耐心看。幾種繼承方式是有關係的

1. 原型鏈繼承

function Father(){
    this.name = 'father';
    this.children = [];
    this.age = 30;
}
Father.prototype.say = function(){
    if(this.children.length){
        console.log('我的孩子:'+this.children.join());
    }else{
        console.log('我是單身狗')
    }
}
function CreateChild(name){
    this.name = name || '未出生';
    this.age = 0;
    // this.children = [];
}

CreateChild.prototype = new Father(); // 這句很關鍵,讓子類和父類連結起來,子類繼承了父類所有的屬性和方法,包括父類原型上的

let child1 = new CreateChild('張三');
let child2 = new CreateChild('張四');

child1.children.push('張小一')
    
console.log(child1)
console.log(child2.children) 

結果:

 

特性:

(1)是通過覆蓋建構函式的 原型 prototype 來實現的。

(2)不能給父類傳參。

(3)子類繼承父類所有的屬性和方法,包括 原型 prototype 上的。

(4)這裡的繼承屬性和方法指的是:構造的子例項,本身上是沒有屬性的(除非自己初始有),只有原型上繼承的父類的屬性和方法,呼叫屬性或方法是根據原型鏈查詢的。

(5)如果父類有引用型別,子類沒有,那麼其中一個子類繼承來的引用型別修改後會影響所有的子類

說明:

(1)為什麼訪問 child2.children 的時候,也會出現 child1 的的 children 內容? 因為 訪問的時候,child2 本身是沒有children 這個屬性的,只有在原型上去找,剛好找到父級存在這個屬性,而這個屬性又是引用屬性,一處修改,所有引用的地方都更改了。所以最後結果也是 [‘張小一’];

(2)如果子類裡面,自己定義有 children 屬性。那麼相當於 生成的例項 child1,child2 有些屬性就是自己的建構函式上的,並不是繼承來的,所以,如果放開  this.children = [] 的註釋。你會看到 生成的例項中,父級的 children 屬性未變化。

(3)如果在  CreateChild.prototype = new Father() 之後再給   CreateChild.prototype = xxx   賦值的話,結果又會不一樣,原型直接被覆蓋了。

(4)這裡沒必要去修復 CreateChild 的 constructor 的指向。因為在  CreateChild.prototype = new Father()    之後,原始 CreateChild 上的原型屬性全都被覆蓋了,去修復也沒什麼作用。

(5)那如果我又 想給父類傳參 怎麼辦?借用建構函式繼承 就能實現這個需求  

 

2. 借用建構函式繼承。

function Father(name){
    this.name = name;
    this.children = ['張大大'];
    this.age = 30;
}
Father.prototype.say = function(){
    if(this.children.length){
        console.log('我的孩子:'+this.children.join());
    }else{
        console.log('我是單身狗')
    }
}
function CreateChild(name){
    // this.children = ['張老大']
    Father.call(this,name);
    // this.children = ['張老大']
}

let child1 = new CreateChild('張三');
let child2 = new CreateChild('張四');

child1.children.push('張大一')
child2.children.push('張小二')

console.log(child1)
console.log(child2)

結果:

 特性:

(1)繼承父類的原始屬性和方法,不包括原型上的屬性和方法。這裡繼承的屬性和方法指的是:構造的例項,本身繼承的是父類的屬性和方法。原型上無任何變化。

(2)可以給父類傳參,但是不能用於例項化(new)父類的時候傳參。

(3)父類的引用屬性是獨立的, Father.call(this,name) 這段程式碼,相當於給 Father 方法的 this 繫結 為 CreateChild 函式中 this 的指向。然後給這個指向繫結屬性和方法,有點 new 的味道。因為 new 的作用是:內部新增一個物件,讓建構函式內部的 this 指向這個新物件,然後執行語句,為這個物件繫結屬性和方法,最後返回這個物件。

(4)每次初始化子類都會執行一次 Father 父類,不能複用(一次執行,多次使用)。且子類未用到原型

說明:

(1)最重要,最核心的就是   Father.call(this,name)   這段程式碼,給子類繫結了屬性和方法。

(2)如果放開 CreateChild 裡面的第一個或者第二個賦值註釋,都會因為程式碼執行的先後順序 ,原始的資料會被覆蓋。

(3)並不會繼承父類原型上的屬性和方法。因為 此時的 Father 只是當作普通函式執行,所以 prototype 原型上的屬性和方法訪問不了,因為 Father 並未使用建構函式的方式執行。

(4)如果我 既想繼承父類原型上的屬性和方法,又想給父類傳參 怎麼辦?那麼 組合繼承 就能滿足這個需求

 

3. 組合繼承  

function Father(name){
    this.name = name;
    this.children = [];
    this.age = 30;
}
Father.prototype.say = function(){
    if(this.children.length){
        console.log('我的孩子:'+this.children.join());
    }else{
        console.log('我是單身狗')
    }
}
Father.prototype.hobbies = ['woman'];
    
function CreateChild(name){
    Father.call(this,name)
}
    
CreateChild.prototype = new Father();
    
let child1 = new CreateChild('張三');
let child2 = new CreateChild('張四');
    
child1.children.push('張大一');
child1.hobbies.push('meet');

child2.children.push('張小二');
child2.hobbies.push('fruit')
        
console.log(child1)
console.log(child2)
console.log(child2.hobbies)

結果:

 特性:

(1)能繼承父類的所有屬性和方法,因為  Father.call(this,name)  這句程式碼。因此,構造例項本身就含有父類的屬性和方法。

(2)能繼承父類 prototype 原型上的屬性和方法,因為  CreateChild.prototype = new Father();  這句程式碼   。因此,構造例項的原型上含有父類原型的屬性和方法

(3)能給父類傳參,但是不能用於例項化(new)父類的時候傳參。

(4)父類原型上如果有引用屬性,某一例項修改後,其它的例項也會受到影響。

(5)每建立一個例項,Father 函式會被執行一次。

說明:

(1)這種繼承方式,是第一,二中方式的 組合,所以叫組合繼承。囊括了這兩種方式的優缺點。

 

4. 原型式繼承

function extendChild(target){
    function Fn(){};
    Fn.prototype = target;
    return new Fn();
}

function Father(){
    this.name = 'father';
    this.children = [];
    this.age = 30;
}

const FatherInstance = new Father();

const child1 = extendChild(FatherInstance);
const child2 = extendChild(FatherInstance);

child1.children.push('張大一');
child1.name = 'child1';

child2.children = ['張小二'];
child2.name = 'child2';

console.log(child1)
console.log(child2)

結果:

特性:

(1)通過覆蓋一個函式的原型,實現構造的例項的原型上繼承傳入的物件。構造的例項本身是沒有屬性和方法的。

(2)如果父類有引用屬性,那麼一個構造例項改變後,其它的例項也會改變。

(3)每次新增例項,都需要執行一次 new Fn()。

(4)主要的功能就是:基於已有的物件,去建立新物件,繼承已有物件的屬性和方法。

說明:

(1)如果看了文章最初的第一個問題,就會明白child1和child2的name,還有child2的children 屬性為什麼會新增到本體屬性上。

(2)細心的會發現,這種繼承方式,和 Object.create 的 polify 一樣一樣的,是同樣的原理,看mdn

(3)我如果 想給所有例項新增 共同初始的 方法或者屬性,而又不影響父類 怎麼辦? 寄生式繼承 就能解決這個問題

 

5. 寄生式繼承

function extendChild(target){
    function Fn(){};
    Fn.prototype = target;
    return new Fn();
}
    
function Father(){
    this.name = 'father';
    this.children = [];
    this.age = 30;
}    
const FatherInstance = new Father();    
    
function createChild(target){
    var target = extendChild(target)
    target.name = 'target';
    return target;
}
const child1 = createChild(FatherInstance);
const child2 = createChild(FatherInstance);
    
child1.children.push('張大一');
child1.name = 'child1';
    
child2.children = ['張小二'];
child2.name = 'child2';
    
console.log(child1)
console.log(child2)

結果:

特性:

(1)在原型式繼承上,多加了一個函式。

(2)可以例項化前,給所有例項新增公用的方法或屬性。不會影響父級

說明:

(1)和原型式繼承差不多,其它的沒看出來有什麼優缺點???

(2)組合繼承挺好的,就是父類多呼叫了,而寄生式繼承 只呼叫了一次,能不能把寄生式繼承的優點和組合繼承結合起來?所以 寄生組合式 就這麼來了

 

6. 寄生組合式繼承,這種繼承方式是最優的繼承方式

function Father(name){
    this.name = name || 'father';
    this.children = [];
    this.age = 30;
}
Father.prototype.say = function(){
    console.log(this.name);
}
Father.prototype.hobbies = ['fruit']

function Child(name){
    Father.call(this,name)
    // this.name = name || '未出生';
    // this.age = 0;
}
    
function createChild(target){
    var Fn = function(){};
    Fn.prototype = target;
    return new Fn();
}
    
function extendFn(Child,Father){
    var instance = createChild(Father.prototype);
    Child.prototype = instance;
}
    
extendFn(Child,Father)
        
let child1 = new Child('張三');
let child2 = new Child('張四');
    
child1.children.push('張大一') 
child1.hobbies.push('123')
child1.name = 'child1'
    
child2.children.push('張小二')
child2.name = 'child2'
        
console.log(child1)
console.log(child2) 

 結果:

 特性:

(1)和組合繼承的特性一樣。

(2)解決了 多次呼叫 父類的問題。

說明:

(1)createChild 方法 可以換成 Object.create。還可以省點程式碼,功能是一樣的。

 

7. es6 class類的繼承 extends

class Father{
    constructor(name){
        this.name = name || 'father';
        this.children = [];
    }
    hobbies = ['fruit'];
    say(){
        console.log(this.name)
    }
} 
class Child extends Father{
    constructor(name) {
        super(name);
    }
}
let child1 = new Child('child1');
let child2 = new Child('child2');
    
child1.children.push('child1');
child1.hobbies.push('apple')

child2.hobbies = ['apple2']
    
console.log(child1)
console.log(child2)
console.log(child2.say())

結果:

 特性:

(1)很方便用???

(2)子類會從父類繼承所有的屬性和方法,父類的引用屬性不共享。

 

 

總結:

(1)原型鏈繼承  —(優化)— >  借用建構函式繼承 —(優化)—> 組合繼承

(2)原型式繼承  —(優化)— >  寄生式繼承   —(優化)— >  寄生組合式繼承

 

相關文章