JavaScript 各類繼承方式優缺點對比


原型對象

不管何時,只要建立一個新函數,就會根據一組特定的規則爲該函數建立一個 prototype 屬性,這個屬性指向函數的原型對象。默認狀況下,全部原型對象都會自動得到一個 constructor(構造函數)屬性,這個屬性指向 prototype 屬性所在的函數。數組

function Person(){
}   
複製代碼

當咱們用構造函數建立一個實例時,也會爲這個實例建立一個 __proto__ 屬性,這個__proto__ 屬性是一個指針指向構造函數的原型對象函數

let person = new Person();
person.__proto__ === Person.prototype    // true
let person1 = new Person();
person1.__proto__ === Person.prototype    // true
複製代碼

因爲同一個構造函數建立的全部實例對象的__proto__ 屬性都指向這個構造函數的原型對象,所以全部的實例對象都會共享構造函數的原型對象上全部的屬性和方法,一旦原型對象上的屬性或方法發生改變,全部的實例對象都會受到影響。優化

function Person(){
}
Person.prototype.name = "Luke";
Person.prototype.age = 18;
let person1 = new Person();
let person2 = new Person();
alert(person1.name)    // "Luke"
alert(person2.name)    // "Luke"
Person.prototype.name = "Jack";
alert(person1.name)    // "Jack"
alert(person2.name)    // "Jack"
複製代碼

重寫原型對象

咱們常常用一個包含全部屬性和方法的對象字面量來重寫整個原型對象,以下面的例子所示ui

function Person(){
}
Person.prototype = {
    name : "Luke",
    age : 18,
    job : "Software Engineer",
    sayName : function(){
        alert(this.name)
    }
}
複製代碼

在上面的代碼中,咱們將 Person.prototype 設置爲一個新對象,而這個對象中沒有constructor屬性,這致使 constructor 屬性再也不指向 Person,而是指向 Objectthis

let friend = new Person();
alert(friend.constructor  === Person);    //false 
alert(friend.constructor  === Object);    //true
複製代碼

若是 constructor 的值很重要,咱們能夠像下面這樣特地將它設置回設置回適當的值spa

function Person(){
}
Person.prototype = {
    constructor : Person,
    name : "Luke",
    age : 18,
    job : "Software Engineer",
    sayName : function(){
        alert(this.name)
    }
}
複製代碼

原型鏈及原型鏈繼承

每一個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針(constructor),而實例都包含一個指向原型對象的內部指針(__proto__)。那麼,假如咱們讓原型對象等於另外一個類型的實例,結果會怎麼樣呢?顯然,此時的原型對象將包含一個指向另外一個原型的指針,相應地,另外一個原型中也包含着一個指向另外一個構造函數的指針。假如另外一個原型又是另外一個構造函數的實例,那麼上述關係依然成立,如此層層遞進,就構成了實例與原型的鏈條。這就是所謂的原型鏈的基本概念。prototype

function Super(){
    this.property = true;
}

Super.prototype.getSuperValue = function(){
    return this.property;
}

function Sub(){
    this.subproperty = false;
}

Sub.prototype = new Super();    //繼承了 Super 

Sub.prototype.getSubValue = function (){
    return this.subproperty;
}

let instance = new Sub();
console.log(instance.getSuperValue());    //true

console.log(instance.__proto__ === Sub.prototype);    //true
console.log(Sub.prototype.__proto__ === Super.prototype);    //true

複製代碼

上面的代碼中Sub.prototype = new Super();經過建立Super的實例,並將該實例賦值給Sub.prototype來實現繼承。此時存在於Super的實例和原型對象中的全部屬性和方法,也都存在於Sub.prototype中。instanse的__proto__屬性指向Sub的原型對象Sub.prototype,Sub原型對象的__proto__屬性又指向Super的原型對象Super.prototype指針

原型鏈搜索機制

當訪問一個實例的屬性時,首先會在該實例中搜索該屬性。若是沒有找到該屬性,則會繼續搜索實例的原型。在經過原型鏈繼承的狀況下,搜索過程就得以沿着原型鏈繼續向上查找,直到找到該屬性爲止,或者搜索到最高級的原型鏈Object.prototype中,任然沒有找到則返回undefined。就拿上面的例子來講,調用instance.getSuperValue()會經歷三個搜索步驟:1)搜索實例;2)搜索Sub.prototype;3)搜索Super.prototype,最後一步纔會找到該方法。在找不到屬性或方法的狀況下,搜索過程老是要一環一環地前行到原型鏈的末端纔會停下。

原型鏈問題

原型鏈繼承最大的問題是來自包含引用類型值的原型。引用類型值的原型屬性會被全部實例共享。而這正是爲何要在構造函數中,而不是原型對象中定義屬性的緣由。在經過原型來實現繼承時,原型實際上會另外一個類型的實例。因而,原先的實例屬性也就瓜熟蒂落地變成了如今的原型屬性了。

function Super(){
    this.colors = ["red", "blue", "green"];
}
function Sub(){

}
Sub.prototype = new Super();    // 繼承了Super

let instance1 = new Sub();

instance1.colors.push("black");
alert(instance1.colors);    //"red, blue, green, black"

let instance2 = new Sub();
alert(instance2.colors);    //"red, blue, green, black"
複製代碼

上面的代碼中,Super 構造函數定義了一個colors 屬性,該屬性是一個數組。Super 的每一個實例都會有各自包含本身數組的colors 屬性。當Sub 經過原型鏈繼承了Super以後,Sub.prototype 就變成了Super 的一個實例,所以它也擁有了一個它本身的colors 屬性。結果是全部的Sub 實例都會共享這一個colors 屬性。 原型鏈的第二個問題是沒有辦法在不影響全部對象實例的狀況下,給超類的構造函數傳遞參數。

構造函數繼承(經典繼承)

即在子類構造函數的中調用父類構造函數,此時當構建一個子類實例時,此實例也會擁有父類實例的屬性和方法。

function Super(){
    this.colors = ["red", "blue", "green"];
}
function Sub(){
    Super.call(this, name);    //繼承了Super
}

let instance1 = new Sub();

instance1.colors.push("black");
alert(instance1.colors);    //"red, blue, green, black"

let instance2 = new Sub();
alert(instance2.colors);    //"red, blue, green"
複製代碼

上面的代碼,當構建Sub的實例時,也會調用Super 的構造函數,這樣就會在新Sub對象上執行Super()函數中定義的全部對象初始化代碼。結果,Sub 的每一個實例就都會具備本身的colors 屬性的副本了。

構造函數繼承問題

若是僅僅是借用構造函數,那麼也將沒法避免構造函數模式存在的問題——方法都在構造函數中定義,所以函數服用就無從談起。並且,在超類原型中定義的方法,對子類而已也是不可見的。

組合繼承

是指將原型鏈和構造函數的相結合,發揮兩者之長的一種繼承模式。其思路是使用原型鏈實現對原型屬性和方法的繼承,而經過借用構造函數來實現對實例屬性的繼承。這樣,即經過在原型上定義方法實現了函數複用,又可以保證每一個實例都有它本身的屬性。

function Super(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

Super.prototype.sayName = function (){
    alert(this.name);
};

function Sub(name, age){
    Super.call(this, name);    //繼承了Super 屬性 (第二次調用Sup構造函數)
    this.age = age;
}

Sub.prototype = new Super();    // 繼承了Super 原型鏈上的方法 (第一次調用Sup構造函數)
Sub.prototype.constructor = Sub;
Sub.prototype.sayAge = function (){
    alert(this.age);
};

var instance1 = new Sub("Luke", 18);
instance1.colors.push("black");
alert(instance1.colors);    //"red, blue, green, black"
instance1.sayName();    //"Luke"
instance1.sayAge()    //18

var instance2 = new Sub("Jack", 20);
alert(instance2.colors);    //"red, blue, green"
instance2.sayName();    //"Jack"
instance2.sayAge()    //20

複製代碼

在上面的例子中,Sup構造函數定義了兩個屬性:name和colors。Sup的原型定義了一個方法sayName()。Sub構造函數在調用Sup構造函數時傳入了name參數,緊接着又定義了它本身的屬性age。而後,將Sup的實例賦值給Sub的原型,而後又在該新原型上定義了sayAge()方法。這樣就可讓兩個不一樣的Sub 實例即分別擁有本身的屬性————包括colors 屬性,又可使用相同的方法了。 組合繼承避免了原型鏈和構造函數的缺陷,融合了它們的優勢,是JavaScript中最經常使用的繼承模式。可是美中不足的是,上面的代碼中調用了兩次父類構造函數。Sub.prototype = new Super(); 第一次調用父類構造函數時,將Sup父類構造函數的實例賦值給了Sub子類的原型對象Sub.prototype。此時也會將父類構造函數實例上的屬性賦值給子類的原型對象Sub.prototype。而第二次是在子類的構造函數中調用父類的構造函數 Super.call(this),此時會將父類構造函數實例上的屬性賦值給子類的構造函數的實例。根據原型鏈搜索原則,實例上的屬性會屏蔽原型鏈上的屬性。所以咱們沒有必要將父類構造函數實例的屬性賦值給子類的原型對象,這是浪費資源而又沒有意義的行爲。

優化後的組合繼承

function Super(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

Super.prototype.sayName = function (){
    alert(this.name);
};

function Sub(name, age){
    Super.call(this, name);    //繼承了Super 屬性
    this.age = age;
}

function F(){
}
F.prototype = Super.prototype; 
Sub.prototype = new F();    // 繼承了Super 原型鏈上的方法

Sub.prototype.constructor = Sub;
Sub.prototype.sayAge = function (){
    alert(this.age);
};

var instance1 = new Sub("Luke", 18);
instance1.colors.push("black");
alert(instance1.colors);    //"red, blue, green, black"
instance1.sayName();    //"Luke"
instance1.sayAge()    //18

var instance2 = new Sub("Jack", 20);
alert(instance2.colors);    //"red, blue, green"
instance2.sayName();    //"Jack"
instance2.sayAge()    //20

複製代碼

上面的例子經過將父類的原型對象直接賦值給一箇中間構造函數的原型對象,而後將這個中間構造函數的實例賦值給子類的原型對象Sub.prototype,從而完成原型鏈繼承。它的高效性體如今只調用了一個父類構造函數Super,而且原型鏈保持不變。還有一種簡便的寫法是採用ES5的Object.create()方法來替代中間構造函數,其實原理都是同樣的

function Super(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

Super.prototype.sayName = function (){
    alert(this.name);
};

function Sub(name, age){
    Super.call(this, name);    //繼承了Super 屬性
    this.age = age;
}
/* function F(){ } F.prototype = Super.prototype; Sub.prototype = new F(); // 繼承了Super 原型鏈上的方法 Sub.prototype.constructor = Sub; */
//這行代碼的原理與上面註釋的代碼是同樣的
Sub.prototype = Object.create(Super.prototype, {constructor: {value: Sub}})

Sub.prototype.sayAge = function (){
    alert(this.age);
};

var instance1 = new Sub("Luke", 18);
instance1.colors.push("black");
alert(instance1.colors);    //"red, blue, green, black"
instance1.sayName();    //"Luke"
instance1.sayAge()    //18

var instance2 = new Sub("Jack", 20);
alert(instance2.colors);    //"red, blue, green"
instance2.sayName();    //"Jack"
instance2.sayAge()    //20
複製代碼

更簡單的繼承方式

還有一種更簡單的繼承方法,就是直接將子類的原型對象(prototype)上的__proto__指向父類的的原型對象(prototype),這種方式沒有改變子類的原型對象,所以子類原型對象上的constructor屬性仍是指向子類的構造函數,並且當子類的實例在子類的原型對象上沒有搜索到對應的屬性或方法時,它會經過子類原型對象上的__proto__屬性,繼續在父類的原型對象上搜索對應的屬性或方法

function Super(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

Super.prototype.sayName = function (){
    alert(this.name);
};

function Sub(name, age){
    Super.call(this, name);    //繼承了Super 屬性
    this.age = age;
}

Sub.prototype.__proto__ = Super.prototype
Sub.prototype.sayAge = function (){
    alert(this.age);
};
var instance1 = new Sub("Luke", 18);
instance1.colors.push("black");
alert(instance1.colors);    //"red, blue, green, black"
instance1.sayName();    //"Luke"
instance1.sayAge()    //18

var instance2 = new Sub("Jack", 20);
alert(instance2.colors);    //"red, blue, green"
instance2.sayName();    //"Jack"
instance2.sayAge()    //20
複製代碼

Object.setPrototypeOf()

Object.setPrototypeOf()是ECMAScript 6最新草案中的方法,相對於 Object.prototype.proto ,它被認爲是修改對象原型更合適的方法

function Super(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

Super.prototype.sayName = function (){
    alert(this.name);
};

function Sub(name, age){
    Super.call(this, name);    //繼承了Super 屬性
    this.age = age;
}

//Sub.prototype.__proto__ = Super.prototype
Object.setPrototypeOf(Sub.prototype, Super.prototype)

Sub.prototype.sayAge = function (){
    alert(this.age);
};
var instance1 = new Sub("Luke", 18);
instance1.colors.push("black");
alert(instance1.colors);    //"red, blue, green, black"
instance1.sayName();    //"Luke"
instance1.sayAge()    //18

var instance2 = new Sub("Jack", 20);
alert(instance2.colors);    //"red, blue, green"
instance2.sayName();    //"Jack"
instance2.sayAge()    //20
複製代碼

類的靜態方法繼承

上面全部的繼承方法都沒有實現類的靜態方法繼承,而在ES6的class繼承中,子類是能夠繼承父類的靜態方法的。咱們可經過Object.setPrototypeOf()來實現類的靜態方法繼承,很是簡單

Object.setPrototypeOf(Sub, Super)
複製代碼
function Super(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

Super.prototype.sayName = function (){
    alert(this.name);
};

Super.staticFn = function(){
    alert('Super.staticFn')
}

function Sub(name, age){
    Super.call(this, name);    //繼承了Super 屬性
    this.age = age;
}

//Sub.prototype.__proto__ = Super.prototype
Object.setPrototypeOf(Sub.prototype, Super.prototype)
Object.setPrototypeOf(Sub, Super)    // 繼承父類的靜態屬性或方法
Sub.staticFn()    // "Super.staticFn"

Sub.prototype.sayAge = function (){
    alert(this.age);
};
var instance1 = new Sub("Luke", 18);
instance1.colors.push("black");
alert(instance1.colors);    //"red, blue, green, black"
instance1.sayName();    //"Luke"
instance1.sayAge()    //18

var instance2 = new Sub("Jack", 20);
alert(instance2.colors);    //"red, blue, green"
instance2.sayName();    //"Jack"
instance2.sayAge()    //20
複製代碼

這大概就是最終的理想繼承方式吧。

相關文章
相關標籤/搜索