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

goyth發表於2018-08-09

原型物件

無論什麼時候,只要建立一個新函式,就會根據一組特定的規則為該函式建立一個 prototype 屬性,這個屬性指向函式的原型物件。預設情況下,所有原型物件都會自動獲得一個 constructor(建構函式)屬性,這個屬性指向 prototype 屬性所在的函式。

function Person(){
}   
複製程式碼

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

當我們用建構函式建立一個例項時,也會為這個例項建立一個 __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"
複製程式碼

重寫原型物件

我們經常用一個包含所有屬性和方法的物件字面量來重寫整個原型物件,如下面的例子所示

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

在上面的程式碼中,我們將 Person.prototype 設定為一個新物件,而這個物件中沒有constructor屬性,這導致 constructor 屬性不再指向 Person,而是指向 Object

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

如果 constructor 的值很重要,我們可以像下面這樣特意將它設定回設定回適當的值

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

原型鏈及原型鏈繼承

每個建構函式都有一個原型物件,原型物件都包含一個指向建構函式的指標(constructor),而例項都包含一個指向原型物件的內部指標(__proto__)。那麼,假如我們讓原型物件等於另一個型別的例項,結果會怎麼樣呢?顯然,此時的原型物件將包含一個指向另一個原型的指標,相應地,另一個原型中也包含著一個指向另一個建構函式的指標。假如另一個原型又是另一個建構函式的例項,那麼上述關係依然成立,如此層層遞進,就構成了例項與原型的鏈條。這就是所謂的原型鏈的基本概念。

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
複製程式碼

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

相關文章