天天看點

JS實作繼承的6種方式

1. 原型鍊繼承

将構造函數的原型設定為另一個構造函數的執行個體對象,這樣就可以繼承另一個原型對象的所有屬性和方法,可以繼續往上,最終形成原型鍊。
  • 第一個問題是,當實作繼承後,另一個原型的執行個體屬性,變成了現在這個原型的原型屬性,然後該原型的引用類型屬性會被所有的執行個體共享,這樣繼承原型引用類型屬性的執行個體之間不再具有自己的獨特性了。
  • 第二個問題是,在建立子類型的執行個體時,沒有辦法在不影響所有對象執行個體的情況下給超類型的構造函數中傳遞參數。

2. 借用構造函數繼承

為了解決原型中包含引用類型值的問題,開始使用借用構造函數,也叫僞造對象或經典繼承
function SuperType() {
	this.colors = ["red", "blue", "green"];
}

function SubType() {
	//繼承SuperType
	SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green" 
           
  • 将SuperType函數在SubType構造函數中調用,在每個執行個體中執行,這樣每個執行個體中都會有一份SuperType中的屬性方法的副本,也就實作了繼承SuperType。
  • 這種模式的優勢就是可以在子類型構造函數中向超類型構造函數傳遞參數。
存在的問題就是,所有的類型都隻能使用構造函數模式(因為超類型的原型中定義的方法對于子類型不可見),是以方法都在構造函數中定義,函數複用就無從談起了。

3. 組合繼承

也叫僞經典繼承,将原型鍊和借用構造函數的技術組合到一塊。使用原型鍊實作對原型屬性和方法的繼承,而通過構造函數來實作對執行個體屬性的繼承。
function SuperType(name) {
	this.name = name;
	this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() {
	alert(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() {
	alert(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27 
           
  • 将SubType的原型指定為SuperType的一個執行個體,大緻步驟和原型鍊繼承類似,隻是多了在SubType中借調SuperType的過程。
  • 執行個體屬性定義在構造函數中,而方法則定義在構造函數的新原型中,同時将新原型的constructor指向構造函數。
  • 可以通過

    instanceof

    isPrototypeOf()

    來識别基于組合繼承建立的對象。
  • 避免了原型鍊和借用構造函數的缺陷,融合了它們的優點,成為JS中最常用的繼承模式。
實際上是借用了構造函數,以覆寫的方式,解決了在原型鍊繼承中原型的引用類型屬性共享在所有執行個體中的問題。
因為在子類型中借調構造函數(SuperType.call(this))時,會在自己的所有執行個體中執行一遍SuperType中的代碼,由于每個執行個體this都是不同的,是以SuperType中定義的屬性會在每個執行個體中有一份副本,也就避免了原型鍊繼承中,原型屬性共享的問題(覆寫了原型屬性)。

4. 原型式繼承

不自定義類型的情況下,臨時建立一個構造函數,借助已有的對象作為臨時構造函數的原型,然後在此基礎執行個體化對象,并傳回。
function object(o){
 function F(){}
 F.prototype = o;
 return new F();
} 
           
本質上是object()對傳入其中的對象執行了一次淺複制
var person = {
 name: "Nicholas",
 friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

alert(person.friends); //"Shelby,Court,Van,Rob,Barbie" 
           
  • 原型的引用類型屬性會在各執行個體之間共享。
  • 當隻想單純地讓一個對象與另一個對象保持類似的情況下,原型式繼承是完全可以勝任的。

注意:

ES5 通過新增

Object.create()

方法規範化了原型式繼承。這個方法接收兩個參數:一個用作新對象原型的對象和(可選的)一個為新對象定義額外屬性的對象。在傳入一個參數的情況下,

Object.create()

與這裡的

object()

方法的行為相同。第二個參數與

Object.defineProperties()

方法的第二個參數格式相同:每個屬性都是通過自己的描述符定義的。

5. 寄生式繼承

其實就是在原型式繼承得到對象的基礎上,在内部再以某種方式來增強對象,然後傳回。
function createAnother(original) {
	var clone = object(original);
	clone.sayHi = function() {
		alert("hi");
	};
	return clone;
}
           
  • 思路與寄生構造函數和工廠模式類似。
  • 新的對象中不僅具有original的所有屬性和方法,而且還有自己的sayHi()方法。
  • 寄生式繼承在主要考慮對象而不是自定義類型和構造函數的情況下非常有用。
  • 由于寄生式繼承為對象添加函數不能做到函數複用,是以效率降低。

6. 寄生組合式繼承

組合繼承是JS中最常用的繼承模式,但其實它也有不足,組合繼承無論什麼情況下都會調用兩次超類型的構造函數,并且建立的每個執行個體中都要屏蔽超類型對象的所有執行個體屬性。

寄生組合式繼承就解決了上述問題,被認為是最理想的繼承範式。

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

function inheritPrototype(superType, subType) {
	var prototype = object(superType.prototype);
	prototype.constructor = subType;
	subType.prototype = prototype;
}

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

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

function SubType(name, age) {
	SuperType.call(this, name);
	this.age = age;
}

inheritPrototype(SuperType, SubType);	// 這一句,替代了組合繼承中的SubType.prototype = new SuperType()

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

既然在組合模式中我們通過借調構造函數來為每個執行個體定義執行個體屬性,進而覆寫原型屬性,影響了效率,那麼是否可以把原型改變一下呢,不讓它作為

SuperType

的執行個體,這樣就不會有一些無用的原型屬性了。

不必為了指定子類型的原型而調用超類型的構造函數,我們需要的隻不過是超類型原型的一個副本。

inheritPrototype()

函數中所做的事:

  1. inheritPrototype

    函數中用到了原型式繼承中的

    object()

    方法,将超類型的原型指定為一個臨時的空構造函數的原型,并傳回構造函數的執行個體。
  2. 此時由于構造函數内部為空(不像

    SuperType

    裡面有執行個體屬性),是以傳回的執行個體也不會自帶執行個體屬性,這很重要!因為後面用它作為

    SubType

    的原型時,就不會産生無用的原型屬性了,借調構造函數也就不用進行所謂的“重寫”了。
  3. 然後為這個對象重新指定

    constructor

    SubType

    ,并将其指派給

    SubType

    的原型。這樣,就達到了将超類型構造函數的執行個體作為子類型原型的目的,同時沒有一些從

    SuperType

    繼承過來的無用原型屬性。

詳細解讀請見筆記: JavaScript進階程式設計: 繼承

繼續閱讀