JavaScript的繼承

1、繼承的概念

​ 繼承是全部的面向對象的語言最重要的特徵之一。大部分的oop語言的都支持兩種繼承:接口繼承和實現繼承。好比基於類的編程語言Java,對這兩種繼承都支持。從接口繼承抽象方法 (只有方法簽名),從類中繼承實例方法。javascript

​ 可是對JavaScript來講,沒有類和接口的概念(ES6以前),因此只支持實現繼承,並且繼承在 原型鏈 的基礎上實現的。等了解過原型鏈的概念以後,你會發現繼承實際上是發生在對象與對象之間。這是與其餘編程語言很大的不一樣。java

2、原型鏈的概念

在JavaScript中,將原型鏈做爲實現繼承的主要方法。其基本思想是利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法編程

​ 再回顧下,構造函數、原型(對象)和對象之間的關係。每一個構造函數都有一個屬性 prototype 指向一個原型對象,每一個原型對象也有一個屬性 constructor 指向函數,經過new 構造函數() 建立出來的對象內部有一個不可見的屬性[[prototype]]指向構造函數的原型。當每次訪問對象的屬性和方法的時候,老是先從p1中找,找不到則再去p1指向的原型中找。數組

下面咱們開始一步步的構造原型鏈,來實現繼承瀏覽器

2.1 更換構造函數的原型

​ 原型其實就是一個對象,只是默認狀況下原型對象是瀏覽器會自動幫咱們建立的,並且自動讓構造函數的 prototype 屬性指向這個自動建立的原型對象。app

​ 其實咱們徹底能夠把原型對象更換成一個咱們自定義類型的對象。編程語言

看下面的代碼:函數


<script type="text/javascript">
//定義一個構造函數。
function Father () {
// 添加name屬性. 默認直接賦值了。固然也能夠經過構造函數傳遞過來
this.name = "馬雲";
}
//給Father的原型添加giveMoney方法
Father.prototype.giveMoney = function () {
alert("我是Father原型中定義的方法");
}
//再定義一個構造函數。
function Son () {
//添加age屬性
this.age = 18;
}
//關鍵地方:把Son構造方法的原型替換成Father的對象。 由於原型是對象,任何對象均可以做爲原型
Son.prototype = new Father();
//給Son的原型添加getMoney方法
Son.prototype.getMoney = function () {
alert("我是Son的原型中定義的方法");
}
//建立Son類型的對象
var son1 = new Son();

//發現不只能夠訪問Son中定義屬性和Son原型中定義的方法,也能夠訪問Father中定義的屬性和Father原型中的方法。
//這樣就經過原型完成了類型之間的繼承。
// Son繼承了Father中的屬性和方法,固然還有Father原型中的屬性和方法。
son1.giveMoney();
son1.getMoney();
alert("Father定義的屬性:" + son1.name);
alert("Son中定義的屬性:" + son1.age);

</script>

上面的代碼其實就完成了Son繼承Father的過程。那麼究竟是怎麼完成的繼承呢?oop

看下面的示意圖:測試

說明:

  1. 定義Son構造函數後,咱們沒有再使用Son的默認原型,而是把他的默認原型更換成了Father類型對象。

  2. 這時,若是這樣訪問 son1.name, 則先在son1中查找name屬性,沒有而後去他的原型( Father對象)中找到了,因此是"馬雲"。

  3. 若是這樣訪問 son1.giveMoney(), 先在son1中找這個方法,找不到去他的原型中找,仍然找不到,則再去這個原型的原型中去找,而後在 Father的原型對象中 找到了。

  4. 從圖中能夠看出來,在訪問屬性和方法的時候,查找的順序是這樣的:對象->原型->原型的原型->...->原型鏈的頂端。 就像一個鏈條同樣,這樣 由原型連成的"鏈條",就是咱們常常所說的原型鏈。

  5. 從上面的分析能夠看出,經過原型鏈的形式就完成了JavaScript的繼承。

2.2 默認頂端原型

​ 其實上面原型鏈還缺乏一環。

​ 在 JavaScript 中全部的類型若是沒有指明繼承某個類型,則默認是繼承的 Object 類型。這種 默認繼承也是經過原型鏈的方式完成的。

下面的圖就是一個完整的原型鏈:

說明:

  1. 原型鏈的頂端必定是Object這個構造函數的原型對象。這也是爲何咱們隨意建立一個對象,就有不少方法能夠調用,其實這些方法都是來自Object的原型對象。

  2. 經過對象訪問屬性、方法的時候,必定是會經過原型鏈來查找的,直到原型鏈的頂端。

  3. 一旦有了繼承,就會出現多態的狀況。假設須要一個Father類型的數據,那麼你給一個Father對象,或Son對象都是沒有任何問題的。而在實際執行的過程當中,一個方法的具體執行結果,就看在原型鏈中的查找過程了。給一個實際的Father對象則從Fahter的原型鏈中查找,給一個實際的Son則從Son的原型鏈中查找。

  4. 由於繼承的存在,Son的對象,也能夠看出Father類型的對象和Object類型的對象。 子類型對象能夠看出一個特殊的父類型對象。

2.3 測試數據的類型

​ 到目前爲止,咱們有3中方法來測試數據的類型。

  1. typeof:通常用來測試簡單數據類型和函數的類型。若是用來測試對象,則會一直返回object,沒有太大意義。


<script type="text/javascript">
alert(typeof 5); // number
var v = "abc";
alert(typeof v);  // string
alert(typeof function () {

});  //funcion
function Person () {

}
alert(typeof new Person()); //object

</script>
  1. instanceof: 用來測試一個對象是否是屬於某個類型。結果爲boolean值。


<script type="text/javascript">
function Father () {
}
function Son () {
}

Son.prototype = new Father();
var son = new Son();
alert(son instanceof Son);  // true
// Son經過原型繼承了Father
alert(son instanceof Father);  // true
//Father又默認繼承了Objcet
alert(son instanceof Object); // true
</script>
  1. isPrototypeOf( 對象 ) : 這是個 原型對象 的方法,參數傳入一個對象,判斷參數對象是否是由這個原型派生出來的。 也就是判斷這個原型是否是參數對象原型鏈中的一環。


<script type="text/javascript">
function Father () {

}
function Son () {

}

Son.prototype = new Father();
var son = new Son();
alert(Son.prototype.isPrototypeOf(son));  // true
alert(Father.prototype.isPrototypeOf(son)); // true
alert(Object.prototype.isPrototypeOf(son)); // true
</script>

2.4 原型鏈在繼承中的缺陷

​ 原型鏈並不是天衣無縫,也是存在一些問題的。

2.4.1 父類型的屬性共享問題

​ 在原型鏈中,父類型的構造函數建立的對象,會成爲子類型的原型。那麼父類型中定義的實例屬性,就會成爲子類型的原型屬性。對子類型來講,這和咱們之前說的在原型中定義方法,構造函數中定義屬性是違背的。子類型原型(父類型對象)中的屬性被全部的子類型的實例所共有,若是有個一個實例去更改,則會很快反應的其餘的實例上。

看下面的代碼:


<script type="text/javascript">
function Father () {
this.girls = ["志玲", "鳳姐"];
}
function Son () {

}
// 子類的原型對象中就有一個屬性 girls ,是個數組
Son.prototype = new Father();
var son1 = new Son();
var son2 = new Son();
//給son1的girls屬性的數組添加一個元素
son1.girls.push("亦非");
//這時,發現son2中的girls屬性的數組內容也發生了改變
alert(son2.girls);  // "志玲", "鳳姐", "亦非"
</script>

2.4.2 向父類型的構造函數中傳遞參數問題

​ 在原型鏈的繼承過程當中,只有一個地方用到了父類型的構造函數,Son.prototype = new Father();。只能在這個一個位置傳遞參數,可是這個時候傳遞的參數,未來對子類型的全部的實例都有效。

​ 若是想在建立子類型對象的時候傳遞參數是沒有辦法作到的。

​ 若是想建立子類對象的時候,傳遞參數,只能另闢他法。

3、借用構造函數調用"繼承"

3.1 借用的方式

借用構造函數調用 繼承,又叫假裝調用繼承或冒充調用繼承。雖然有了繼承兩個字,可是這種方法從本質上並沒實現繼承,只是完成了構造方法的調用而已。

​ 使用 callapply 這兩個方法完成函數借調。這兩個方法的功能是同樣的,只有少量的區別(暫且無論)。功能都是更改一個構造方法內部的 this 指向到指定的對象上。

看下面的代碼:


<script type="text/javascript">
function Father (name,age) {
this.name = name;
this.age = age;
}
//若是這樣直接調用,那麼father中的this只的是 window。 由於其實這樣調用的: window.father("李四", 20)
// name 和age 屬性就添加到了window屬性上
Father("李四", 20);
alert("name:" + window.name + "\nage:" + window.age);  //能夠正確的輸出

//使用call方法調用,則能夠改變this的指向
function Son (name, age, sex) {
this.sex = sex;
    //調用Father方法(當作普通方法),第一個參數傳入一個對象this,則this(Son類型的對象)就成爲了Father中的this
Father.call(this, name, age);
}
var son = new Son("張三", 30, "男");
alert("name:" + son.name + "\nage:" + son.age + "\nsex:" + son.sex);
alert(son instanceof Father); //false
</script>

函數借調的方式還有別的實現方式,可是原理都是同樣的。可是有一點要記住,這裏其實並無真的繼承,僅僅是調用了Father構造函數而已。也就是說,son對象和Father沒有任何的關係。

3.2 借用的缺陷

Father的原型對象中的共享屬性和方法,Son沒有辦法獲取。由於這個根本就不是真正的繼承。

4、組合繼承

​ 組合函數利用了原型繼承和構造函數借調繼承的優勢,組合在一塊兒。成爲了使用最普遍的一種繼承方式。


<script type="text/javascript">
//定義父類型的構造函數
function Father (name,age) {
// 屬性放在構造函數內部
this.name = name;
this.age = age;
// 方法定義在原型中
if((typeof Father.prototype.eat) != "function"){
Father.prototype.eat = function () {
alert(this.name + " 在吃東西");
}
}  
}
// 定義子類類型的構造函數
function Son(name, age, sex){
    //借調父類型的構造函數,至關於把父類型中的屬性添加到了將來的子類型的對象中
Father.call(this, name, age);
this.sex = sex;
}
//修改子類型的原型爲父類型的對象。這樣就能夠繼承父類型中的方法了。
Son.prototype = new Father( );
var son1 = new Son("志玲", 30, "女");
alert(son1.name);
alert(son1.sex);
alert(son1.age);
son1.eat();
</script>

說明:

  1. 組合繼承是咱們實際使用中最經常使用的一種繼承方式。

  2. 可能有個地方有些人會有疑問:Son.prototype = new Father( );這不照樣把父類型的屬性給放在子類型的原型中了嗎,仍是會有共享問題呀。可是不要忘記了,咱們在子類型的構造函數中借調了父類型的構造函數,也就是說,子類型的原型(也就是Father的對象)中有的屬性,都會被子類對象中的屬性給覆蓋掉。

相關文章
相關標籤/搜索