7、面向對象的程序設計3(《JS高級》筆記)

三、繼承

許多OO語言都支持兩種繼承方式:接口繼承和實現繼承。接口繼承只繼承方法簽名,而實現繼承則繼承實際方法。由于函數沒有簽名,在ECMAScript中無法實現接口繼承,只支持實現繼承,而且其實現繼承主要是依靠原型鏈來實現的。

3.1 原型鏈(很少單獨使用)

原型鏈的基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。簡單回顧一下構造函數、原型和實例的關系:每個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。如果我們讓原型對象等于另一個類型的實例,那么就實現了繼承。下面通過例子說明:

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

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

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

//inherit from SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
    return this.subproperty;
};

var instance = new SubType();
alert(instance.getSuperValue());   //true 

說明:繼承的圖解如下:

1

如圖所示,這里讓SubType原型指向一個SuperType實例實現繼承,本質上是重寫了原型對象,代之以一個新類型的實例。要注意instance.constructor現在指向的是SuperType,這是因為原來SubType中的constructor被重寫的緣故(實際上,不是SubType的原型的constructor屬性被重寫了,而是SubType的原型指向了另一個對象——SuperType的原型,而這個原型對象的constructor屬性指向的是SuperType)。在調用方法或屬性時和前面講的一樣,一層層的搜索,直到最后。

3.1.1 別忘記默認的原型

起始所有引用類型默認都繼承了Object,而這個繼承也是通過原型鏈實現的,如下所示:

2

3.1.2 確定原型和實例的關系

可以通過兩種方式來確定原型和實例之間的關系。第一種是使用instanceof操作符,只要用這個操作符來測試實例與原型鏈中出現過的構造函數,結果就會返回true

 alert(instance instanceof Object);      //true
 alert(instance instanceof SuperType);   //true
 alert(instance instanceof SubType);     //true

第二種方式是使用isPrototypeOf()方法,只要是原型鏈中出現過的原型,都可以說是該原型鏈所派生的實例的原型,因此此方法會返回true

 alert(Object.prototype.isPrototypeOf(instance));    //true
 alert(SuperType.prototype.isPrototypeOf(instance)); //true
 alert(SubType.prototype.isPrototypeOf(instance));   //true

3.1.3 謹慎地定義方法

子類型有時候需要重寫超類型中的某個方法,或者添加超類型中不存在的某個方法。但不管這樣,給原型鏈添加方法的代碼一定要放在替換原型的語句之后,也就是原型鏈形成之后:

 function SuperType(){
     this.property = true;
 }
 
 SuperType.prototype.getSuperValue = function(){
     return this.property;
 };
 
 function SubType(){
     this.subproperty = false;
 }
 
 //繼承
 SubType.prototype = new SuperType();
 
 //添加新方法
 SubType.prototype.getSubValue = function (){
     return this.subproperty;
 };
 
 //重寫方法
 SubType.prototype.getSuperValue = function (){
     return false;
 };
 
 var instance = new SubType();
 alert(instance.getSuperValue());   //false

說明:起始很容易理解,因為我們在讓子類型的原型指向超類型的實例的時候,本質上來說是重寫了子類型的原型對象,既然原型對象都重寫了,當然在它之前添加的方法或重寫的方法都是無效的。同樣的,如果已經將子類型的原型指向超類型,那么在后面的代碼中如果又使用字面量添加新方法,則同樣會讓之前的原型鏈遭到破壞:

function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};
function SubType(){
    this.subproperty = false;
}

//inherit from SuperType
SubType.prototype = new SuperType();

//使用字面量添加新方法,會導致上一行代碼無效
SubType.prototype = {
    getSubValue : function (){
        return this.subproperty;
    },
    someOtherMethod : function (){
        return false;
    }
};

var instance = new SubType();
alert(instance.getSuperValue());   //error!

3.1.4 原型鏈的問題

原型鏈最主要的問題來自包含引用類型的原型。通過例子說明:

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

function SubType(){            
}

//inherit from SuperType
SubType.prototype = new SuperType();

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,black"

說明:我們在通過實例instance1向父類屬性colors中添加的元素對于所有子類實例來說都是共享的。原型鏈的第二個問題是:在創建子類型的實例時,不能向超類型的構造函數中傳遞參數,所以實踐中很少單獨使用原型鏈。

3.2 借用構造函數(很少單獨使用)

在解決原型中包含引用類型值所帶來問題的過程中,開發人員開始使用一種叫做借用構造函數的技術。這種技術的基本思想相當簡單,即在子類型構造函數的內部調用超類型構造函數。因為函數只不過是在特定環境中執行代碼的對象,因此通過使用apply()call()方法也可以在(將來)新創建的對象上執行構造函數:

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

function SubType(){  
    //inherit from 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"

說明:這里不再使用原型鏈中哪種將子類型原型指向超類型實例了,而是在子類型構造函數中調用父類型。這樣每個子類實例都有超類型中的引用屬性(colors)副本了

3.2.1 傳遞參數

相對于原型鏈而言,借用構造函數有一個很大的優勢,即可以在子類型構造函數中向超類型構造函數傳遞參數:

function SuperType(name){
    this.name = name;
}

function SubType(){  
    //繼承SuperType,同時還傳遞參數
    SuperType.call(this, "Nicholas");
    
    //實例屬性
    this.age = 29;
}

var instance = new SubType();
alert(instance.name);    //"Nicholas";
alert(instance.age);     //29

說明:SubType構造函數內部調用SuperType構造函數時,實際上是為SubType的實例設置了name屬性。為了確保SuperType構造函數不會重寫子類型的屬性,可以在調用超類型構造函數后,再添加應該在子類型中定義的屬性。

3.2.2 借用構造函數的問題

如果僅僅是借用構造函數,那么也將無法避免構造函數存在的問題——方法都在構造函數中定義,因此函數復用就無從談起了。而且,在超類型的原型中定義的方法,對子類型而言也是不可見的(這不像原型鏈中子類原型指向超類型實例,這里并沒有構造原型鏈,所以不能訪問超類型原型方法)。考慮到這些問題,借用構造函數的技術也是很少單獨使用的

3.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
alert(SubType.prototype.constructor == SuperType.prototype.constructor);//true
alert(SuperType.isPrototypeOf(instance1));//false,這里instance1不是原型鏈派生的實例
alert(instance1 instanceof SuperType);//true

說明:組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優點,成為JS中最常用的繼承模式。而且,instanceofisPrototypeOf()也能夠用于識別基于組合模式繼承創建的對象。

3.4 原型式繼承

這種方法并沒有嚴格意義上的構造函數,就是借助原型可以基于已有對象創建新對象,同時還不必因此創建自定義類型。

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

說明:此函數返回一個臨時類型的一個新實例。本質上講,object()對傳入其中的對象執行了一次淺復制。看下面的例子:

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

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"

說明:這種方法中將person作為新對象的原型,于是所有新實例會共享引用屬性friends,顯然這是不行的。ECMAScript 5通過新增Object.create()方法規范了原型式繼承。這個方法接收兩個參數:一個用作新對象原型的對象和(可選的)一個為新對象定義額外屬性的對象。如果只是傳遞第一個參數,那么和上面的object()方法的行為相同:

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};

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

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

alert(person.friends);   //"Shelby,Court,Van,Rob,Barbie"

當然我們可以使用第二個參數進行修正,第二個參數與Obejct.defineProperties()方法的第二個參數格式相同:每個屬性都是通過自己的描述符定義的。以這種方式指定的任何屬性都會覆蓋原型對象上的同名屬性。

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};
                   
var anotherPerson = Object.create(person, {
    name: {
        value: "Greg"
    }
});

alert(anotherPerson.name);  //"Greg"

可以看到,這里第二個參數定義的name屬性就是新對象私有的。當然我們也可以再定義一個friends屬性將原型person中的同名屬性覆蓋掉。不要忘記:這種模式創建的對象包含引用類型值的屬性始終都會共享相應的值(除非覆蓋掉),就像原型模式一樣。

3.5 寄生式繼承

寄生式繼承是與原型式繼承緊密相關的一種思路。

function createAnother(original){
    var clone = object(original);
    clone.sayHi = function(){
        console.log("Hi");
    };
    return clone;
}

說明:這種模式就是創建一個僅用于封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最后再像真地是它做了所有工作一樣返回對象。可以這樣使用:

var person = {
    name : "Tom",
    friends : ["Shelby", "Court", "Van"]
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi();//"Hi"

說明:在主要考慮對象而不是自定義類型和構造函數的情況下,寄生式繼承也是一種有用的模式。前面示例繼承模式中使用的object()函數不是固定的,任何能夠返回新對象的函數都適用于此模式。

3.6 寄生組合式繼承(最有效方式)

前面講過,組合繼承是JS最常見的繼承模式;不過也有不足。最大的問題就是不論在什么情況下,都會調用兩次超類型構造函數。

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);//第二次調用SuperType()
    this.age = age;
}
//將子類型原型指向超類型實例,實現原型鏈繼承
SubType.prototype = new SuperType();//第一次調用SuperType()

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

說明:第一次調用SuperType()構造函數時,SubType.prototype會得到兩個屬性:namecolors;它們都是SuperType的實例屬性,只不過現在位于SubType的原型中。當調用SubType構造函數時,又會調用一次SuperType構造函數,這一次i又在新對象上創建了實例屬性namecolors。于是,這兩個屬性就屏蔽了原型中的兩個同名屬性。如圖所示:

3

4

所謂寄生組合式繼承,即通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。背后的基本思路是,不必為了指定子類型的原型而調用超類型的構造函數,我們所需要的無非就是超類型原型的一個部分而已。本質上,就是使用寄生式繼承來繼承超類型的原型,然后再將結果指定給子類型的原型。

function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype);//創建對象
    prototype.constructor = subType;//增強對象
    subType.prototype = prototype;//指定對象
}

說明:這是寄生組合式繼承的最簡單的形式。這個函數接收兩個參數:子類型構造函數和超類型構造函數。在函數內部,第一步是創建超類型原型的一個副本。第二部是為創建的副本添加constructor屬性,從而彌補因重寫原型而失去的默認的constructor屬性。最后一步將新創建的對象(即副本)賦值給子類型的原型。

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(SubType, SuperType);

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

說明:此種模式不僅有組合模式的優點,同時只調用一次超類型構造函數,還能夠正常使用instanceofisPrototypeOf()

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 本章內容 理解對象屬性 理解并創建對象 理解繼承 面向對象語言有一個標志,那就是它們都有類的概念,而通過類可以創建...
    悶油瓶小張閱讀 866評論 0 1
  • Chapter 6 面向對象的程序設計 理解對象 使用對象字面量語法創建對象var person = { n...
    云之外閱讀 597評論 0 1
  • 在生活中,發現很多事情你不可能盡善盡美,唯有苦衷自己吞下,每個人從小到大,歲月讓你經歷的事情,往往只有你們幾件讓你...
    淺白沙漏閱讀 116評論 1 1
  • 朋友,你的可愛緣自你的可笑。當我想飲下這杯酒的時候,你送來了這些佐酒的小菜——是三根細細長長的青辣椒。你說,別小看...
    海月先生閱讀 1,005評論 2 0
  • 離開故鄉的時候 是一個下雨的早晨 在泥濘的院子外 嬌小的你 佝僂著背 步履蹣跚 看著我們坐上要遠去的車子 天很冷 ...
    寧靜的煙火閱讀 332評論 2 1