天天看點

JavaScript深入了解之繼承

JavaScript深入了解之繼承

寫在前面

繼承是面向對象語言中最重要的一個概念。許多面向對象語言都支援兩種繼承方式:接口繼承和實作繼承。接口繼承隻繼承方法和簽名,而實作繼承則繼承實際的方法。由于在 JavaScript 中函數沒有簽名,是以無法實作接口繼承,隻支援實作繼承。

原型鍊

在 ECMAScript 中描述了原型鍊的概念,并将原型鍊作為實作繼承的主要方法。其基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。這一部分已經在前面總結過了,就不再多說了。

缺點:

  1. 包含引用類型的原型屬性會被所有執行個體屬性共享,容易造成屬性的修改混亂。
  2. 在建立子類型的執行個體時,不能向超類型的構造函數中傳遞參數。

基于以上問題,在實踐中很少會單獨使用原型鍊。

借用構造函數

借用構造函數的思想主要是在子類型的構造函數中調用超類型構造函數。如下面的例子所示

function SuperType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){
    //繼承了 SuperType
    SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);  //"red,blue,green,black"


var instance2 = new SubType();
console.log(instance2.colors);  //"red,blue,green"      

優點:可以在子類型構造函數中向超類型構造函數添加參數

缺點:和構造函數模式一樣的問題,所有的方法都在構造函數中定義,是以就無法做到函數的複用。而且超類型的原型中定義的方法,對于子類型而言也是不可見的。

基于以上問題,借用構造函數的技術也是很少單獨使用的。

組合繼承

組合繼承指的是将原型鍊和借用構造函數的技術組合到一塊,進而發揮二者之長的一種繼承模式。這種方法的主要思路是使用原型鍊實作對原型屬性和方法的繼承,而通過借用構造函數來實作對執行個體屬性的繼承。這樣既通過在原型上定義方法實作了函數複用,又能夠保證每個執行個體都有它自己的屬性。如下面的例子所示:

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


SuperType.prototype.sayName = function(){
console.log(this.name);
}


function SubType(name, age){


//繼承屬性
    SuperType.call(this,name);


this.age = age;
}


//繼承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
}


var instance1 = new SubType("james",9);
instance1.colors.push("black");
console.log(instance1.colors);  //"red,blue,green,black"
instance1.sayName(); // "james"
instance1.sayAge(); // 9


var instance2 = new SubType("kobe",10);
console.log(instance2.colors);  //"red,blue,green"
instance2.sayName(); // "kobe"
instance2.sayAge(); // 10      

優點:組合繼承避免了原型鍊和借用構造函數的缺陷,融合了它們的優點,成為 JavaScript 中最常用的繼承模式。而且,instanceof 和 isPropertyOf() 也能夠用于識别基于組合繼承建立的對象。

缺點:調用了兩次超類的構造函數,導緻基類的原型對象中增添了不必要的超類的執行個體對象中的所有屬性。

原型式繼承

原型式繼承的主要思路是可以基于已有的對象建立新的對象,同時還不必是以建立自定義類型。如下面的例子所示。

function object(o){
function F(){};
    F.prototype = o;
return new F();
}      

簡單來說這個函數的作用就是,傳入一個對象,傳回一個原型對象為該對象的新對象。

ECMAScript 5中新增了 Object.create() 方法規範了原型式繼承。這個方法接收兩個參數,一個是将被用作新對象原型的對象,一個是為新對象定義額外屬性的對象(可選)。

注意第二個參數的格式與 Object.defineProperties() 方法的第二個參數格式相同。以這種方式指定的任何屬性都會覆寫原型對象上的同名屬性。在第二個參數為空的情況下,該方法與 object() 方法的行為相同。

優點:可以實作基于一個對象的簡單繼承,不必建立構造函數

缺點:與原型鍊中提到的缺點相同,一個是傳參的問題,一個是屬性共享的問題。

寄生式繼承

寄生式繼承的思路是,建立一個僅用于封裝繼承過程的函數,該函數在内部以某種方式增強對象,最後傳回這個對象。如下面的例子所示。

function createAnother(original){


var clone = object(original); //通過調用函數建立一個新對象


clone.sayHi = function(){  // 某種方式增強這個對象
        console.log("hi");
    }


return clone;  // 傳回這個對象
}


var person = {
    name: "james"
}


var anotherPerson = createAnother(person);


anotherPerson.sayHi(); // "hi"      

優點:在主要考慮對象而不是自定義類型和構造函數的情況下,實作簡單的繼承。

缺點:使用該繼承方式,在為對象添加函數的時候,沒有辦法做到函數的複用。

寄生式組合繼承

前面我們提到過了組合繼承的缺點,由于調用了兩次超類的構造函數,導緻基類的原型對象中增添了不必要的超類的執行個體對象中的所有屬性。

寄生式組合繼承就是用來解決這個問題,它與組合繼承不同的地方主要是,在繼承原型時,我們繼承的不是超類的執行個體對象,而是原型對象是超類原型對象的一個執行個體對象,這樣就解決了基類的原型對象中增添了不必要的超類的執行個體對象中的所有屬性d的問題。

我們可以封裝繼承原型時函數如下:

function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); 
// 建立原型對象是超類原型對象的一個執行個體對象
    prototype.constructor = subType; 
    // 彌補因為重寫原型而失去的預設的 constructor 屬性。
    subType.prototype = prototype; 
// 實作原型繼承
}      

優點:效率高,避免了在 SubType.prototype 上建立不必要的屬性。與此同時還能保持原型鍊不變,開發人員普遍認為寄生組合式繼承是引用類型最理想的繼承範式。

寫在最後

對于繼承相關的知識就總結到這裡,感覺總結一遍對每種繼承方式的了解就更深刻了一些,加油!

JavaScript深入了解之繼承

繼續閱讀